Megosztás:


Nem biztonságos kód, mutatótípusok és függvénymutatók

Az ön által írt C#-kód nagy része ellenőrizhetően biztonságos kód. A ellenőrizhetően biztonságos kód azt jelenti, hogy a .NET-eszközök ellenőrizhetik, hogy a kód biztonságos-e. A biztonságos kód általában nem fér hozzá közvetlenül a memóriához mutatókkal. Emellett nem foglal le nyers memóriát. Ehelyett felügyelt objektumokat hoz létre.

A C# nyelv referenciadokumentuma a C# nyelv legújabb kiadású verzióját ismerteti. Emellett a közelgő nyelvi kiadás nyilvános előzetes verziójú funkcióinak kezdeti dokumentációját is tartalmazza.

A dokumentáció azonosítja azokat a funkciókat, amelyeket először a nyelv utolsó három verziójában vagy az aktuális nyilvános előzetes verziókban vezetnek be.

Jótanács

Ha meg szeretné tudni, hogy mikor jelent meg először egy funkció a C#-ban, tekintse meg a C# nyelvi verzióelőzményeiről szóló cikket.

A C# egy unsafe környezetet támogat, amelyben nem ellenőrizhető kódot írhat. Egy környezetben a unsafe kód használhat mutatókat, lefoglalhat és szabadíthat fel memóriablokkokat, és függvénymutatók használatával meghívhat metódusokat. A C# nem biztonságos kódja nem feltétlenül veszélyes; Csak olyan kód, amelynek a biztonságát nem lehet ellenőrizni.

A nem biztonságos kód a következő tulajdonságokkal rendelkezik:

  • A metódusokat, típusokat és kódblokkokat nem biztonságosként határozhatja meg.
  • Bizonyos esetekben a nem biztonságos kódok növelhetik az alkalmazások teljesítményét azáltal, hogy a mutatókkal engedélyezik a közvetlen memóriahozzáférést a tömbkorlátok ellenőrzésének elkerülése érdekében.
  • Nem biztonságos kóddal hívhatja meg a mutatót igénylő natív függvényeket.
  • A nem biztonságos kód használata biztonsági és stabilitási kockázatokat jelent.
  • A nem biztonságos blokkokat tartalmazó kód fordításához hozzá kell adnia az AllowUnsafeBlocks fordítóbeállítást.

A C# nem biztonságos kódokkal kapcsolatos ajánlott eljárásairól a Nem biztonságos kód ajánlott eljárásai című témakörben olvashat.

Mutatótípusok

Nem biztonságos környezetben a típus lehet mutatótípus, az értéktípuson vagy a referenciatípuson kívül. A mutatótípus-deklaráció az alábbi űrlapok egyikét használja:

type* identifier;
void* identifier; //allowed but not recommended

A mutatótípus előtt * megadott típus a hivatkozás típusa.

A mutatótípusok nem öröklődnek az objektumtól, és nem léteznek átalakítások a mutatótípusok és a object. Emellett a boxolás és a kicsomagolás nem támogatja a mutatók használatát. A különböző mutatótípusok, valamint a mutatótípusok és az integráltípusok között azonban konvertálható.

Ha ugyanabban a deklarációban több mutatót deklarál, csak a mögöttes típussal együtt írja be a csillagot (*). Nem minden mutatónév előtagjaként használatos. Például:

int* p1, p2, p3;   // Ok
int *p1, *p2, *p3;   // Invalid in C#

A szemétgyűjtő nem követi nyomon, hogy egy objektumra bármilyen mutató típus mutat-e. Ha a hivatkozás egy objektum a felügyelt halomban (beleértve a lambda kifejezések vagy névtelen meghatalmazottak által rögzített helyi változókat is), az objektumot mindaddig rögzítenie kell, amíg a mutatót használja.

Az MyType* típusú mutatóváltozó értéke egy MyTypetípusú változó címe. Az alábbiakban példákat láthat a mutatótípus-deklarációkra:

  • int* p: p egy egész számra mutató mutató.
  • int** p: p egy egész számra mutató mutató.
  • int*[] p: p egész számokra mutató mutatók egydimenziós tömbje.
  • char* p: p egy karakterre mutató.
  • void* p: p egy ismeretlen típusra mutató mutató.

A mutató indirekt operátorával * a mutatóváltozó által mutatott helyen érheti el a tartalmat. Vegyük például a következő deklarációt:

int* myVariable;

A *myVariable kifejezés a intcímében található myVariable változót jelöli.

A fixed utasításcímű cikkekben számos példa található a mutatókra. Az alábbi példa a unsafe kulcsszót és a fixed utasítást használja, és bemutatja, hogyan lehet növelni a belső mutatót. Ezt a kódot beillesztheti egy konzolalkalmazás fő függvényébe a futtatáshoz. Ezeket a példákat a AllowUnsafeBlocks fordítóbeállításkészlettel kell lefordítani.

// Normal pointer to an object.
int[] a = [10, 20, 30, 40, 50];
// Must be in unsafe code to use interior pointers.
unsafe
{
    // Must pin object on heap so that it doesn't move while using interior pointers.
    fixed (int* p = &a[0])
    {
        // p is pinned as well as object, so create another pointer to show incrementing it.
        int* p2 = p;
        Console.WriteLine(*p2);
        // Incrementing p2 bumps the pointer by four bytes due to its type ...
        p2 += 1;
        Console.WriteLine(*p2);
        p2 += 1;
        Console.WriteLine(*p2);
        Console.WriteLine("--------");
        Console.WriteLine(*p);
        // Dereferencing p and incrementing changes the value of a[0] ...
        *p += 1;
        Console.WriteLine(*p);
        *p += 1;
        Console.WriteLine(*p);
    }
}

Console.WriteLine("--------");
Console.WriteLine(a[0]);

/*
Output:
10
20
30
--------
10
11
12
--------
12
*/

Az indirekt operátor nem alkalmazható void*típusú mutatóra. Az üres mutatót azonban bármely más típusú mutatóvá alakíthatja át, és fordítva.

A mutató lehet null. Ha a közvetett operátort null mutatóra alkalmazza, az implementáció által definiált viselkedést okoz.

Ha mutatót ad át a metódusok között, az meghatározatlan viselkedést okozhat. Fontolja meg azt a metódust, amely egy helyi változóra mutató mutatót ad vissza egy in, outvagy ref paraméteren keresztül, vagy a függvény eredményeként. Ha a mutató rögzített blokkban volt beállítva, előfordulhat, hogy a változó, amelyre mutat, már nem lesz javítva.

Az alábbi táblázat felsorolja azokat az operátorokat és utasításokat, amelyek nem biztonságos környezetben működnek a mutatókon:

Operátor/utasítás Használ
* Mutató indirekciót hajt végre.
-> Egy struktúra egy tagját egy mutatón keresztül éri el.
[] Indexel egy mutatót.
& Egy változó címét szerzi be.
++ és -- Növekmények és csökkenő mutatók.
+ és - Mutató aritmetikai műveleteket végez.
==, !=, <, >, <=és >= Összehasonlítja a mutatókat.
stackalloc Memóriát foglal le a veremen.
fixed nyilatkozat Ideiglenesen kijavít egy változót, hogy a címe megtalálható legyen.

További információ a mutatóval kapcsolatos operátorokról: Mutatóval kapcsolatos operátorok.

Bármely mutatótípus implicit módon konvertálható void* típussá. Bármely mutatótípus hozzárendelhető a nullértékhez. Bármely mutatótípust explicit módon bármely más mutatótípussá alakíthat át egy öntött kifejezés használatával. Bármely integráltípust átalakíthat mutatótípussá, vagy bármely mutatótípust integráltípussá. Ezek az átalakítások explicit leadást igényelnek.

Az alábbi példa egy int* értéket alakít át byte*értékké. Figyelje meg, hogy a mutató a változó legalacsonyabb címzett bájtjára mutat. Az eredmény egymást követő növekménye esetén a int (4 bájt) méretig megjelenítheti a változó fennmaradó bájtját.

int number = 1024;

unsafe
{
    // Convert to byte:
    byte* p = (byte*)&number;

    System.Console.Write("The 4 bytes of the integer:");

    // Display the 4 bytes of the int variable:
    for (int i = 0 ; i < sizeof(int) ; ++i)
    {
        System.Console.Write(" {0:X2}", *p);
        // Increment the pointer:
        p++;
    }
    System.Console.WriteLine();
    System.Console.WriteLine($"The value of the integer: {number}");

    /* Output:
        The 4 bytes of the integer: 00 04 00 00
        The value of the integer: 1024
    */
}

Rögzített méretű pufferek

fixed A kulcsszóval egy rögzített méretű tömböt tartalmazó puffert hozhat létre egy adatstruktúrában. A rögzített méretű pufferek akkor hasznosak, ha olyan metódusokat ír, amelyek más nyelvekből vagy platformokról származó adatforrásokkal működnek együtt. A rögzített méretű puffer bármilyen attribútumot vagy módosító tulajdonságot képes átvenni, amely a normál strukturált tagok számára engedélyezett. Az egyetlen korlátozás, hogy a tömbtípusnak bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, floatvagy doublekell lennie.

private fixed char name[30];

A biztonságos kódban a tömböt tartalmazó C# szerkezet nem tartalmazza a tömbelemeket. A szerkezet ehelyett az elemekre mutató hivatkozást tartalmaz. Rögzített méretű tömböt beágyazhat egy struktúrába, amikor az nem biztonságos kódblokkban van használva.

A következő struct mérete nem függ a tömb elemeinek számától, mivel pathName hivatkozás:

public struct PathArray
{
    public char[] pathName;
    private int reserved;
}

A struktúra tartalmazhat beágyazott tömböt nem biztonságos kódban. Az alábbi példában a fixedBuffer tömb mérete rögzített. Egy fixed utasítással az első elemre mutató mutatót kap. Ezen a mutatón keresztül érheti el a tömb elemeit. A fixed utasítás a fixedBuffer példánymezőt egy adott helyre rögzíti a memóriában.

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

internal unsafe class Example
{
    public Buffer buffer = default;
}

private static void AccessEmbeddedArray()
{
    var example = new Example();

    unsafe
    {
        // Pin the buffer to a fixed location in memory.
        fixed (char* charPtr = example.buffer.fixedBuffer)
        {
            *charPtr = 'A';
        }
        // Access safely through the index:
        char c = example.buffer.fixedBuffer[0];
        Console.WriteLine(c);

        // Modify through the index:
        example.buffer.fixedBuffer[0] = 'B';
        Console.WriteLine(example.buffer.fixedBuffer[0]);
    }
}

A 128 elem char tömb mérete 256 bájt. A rögzített méretű karakteres pufferek karakterenként mindig 2 bájtot vesznek igénybe, a kódolástól függetlenül. Ez a tömbméret akkor is megegyezik, ha a karaktertárolók API metódusokhoz vagy struktúrákhoz történő átadáskor az CharSet = CharSet.Auto vagy CharSet = CharSet.Ansiparaméterekkel történnek. További információ: CharSet.

Az előző példa bemutatja fixed mezők rögzítés nélküli elérését. Egy másik gyakori rögzített méretű tömb a bool tömb. A bool tömb elemei mindig 1 bájt méretűek. bool tömbök nem alkalmasak bittömbök vagy pufferek létrehozására.

A rögzített méretű pufferek a System.Runtime.CompilerServices.UnsafeValueTypeAttributedirektívával vannak lefordítva, amely arra utasítja a közös nyelvi futtatókörnyezetet (CLR), hogy egy típus egy nem felügyelt tömböt tartalmaz, amely esetleg túlcsordulhat. A stackalloc használatával lefoglalt memória automatikusan engedélyezi a puffertúllépés észlelését a CLR-ben. Az előző példa bemutatja, hogyan létezhet rögzített méretű puffer egy unsafe struct.

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

A fordító által létrehozott C# kód Buffer attribútumként van megjelölve a következőképpen:

internal struct Buffer
{
    [StructLayout(LayoutKind.Sequential, Size = 256)]
    [CompilerGenerated]
    [UnsafeValueType]
    public struct <fixedBuffer>e__FixedBuffer
    {
        public char FixedElementField;
    }

    [FixedBuffer(typeof(char), 128)]
    public <fixedBuffer>e__FixedBuffer fixedBuffer;
}

A rögzített méretű pufferek a következő módokon térnek el a normál tömböktől:

  • Csak kontextusban unsafe használhatja őket.
  • Ezek csak a szerkezetek példánymezői lehetnek.
  • Ezek mindig vektorok, vagy egydimenziós tömbök.
  • A deklarációnak tartalmaznia kell a hosszt, például fixed char id[8]. A fixed char id[]nem használható.

Hogyan használjunk mutatókat egy bájttömb másolásához

Az alábbi példa mutatókkal másol bájtokat az egyik tömbből a másikba.

Ez a példa a nem biztonságos kulcsszót használja, amely lehetővé teszi a mutatók használatát a Copy metódusban. A rögzített utasítás a forrás- és céltömbök mutatóit deklarálja. A fixed utasítás rögzíti a forrás- és céltömbök helyét a memóriában, hogy a szemétgyűjtés ne helyezze át a tömböket. A fixed blokk rögzíti a blokk hatókörében lévő tömbök memóriablokkjait. Mivel a példában szereplő Copy módszer a kulcsszót unsafe használja, az AllowUnsafeBlocks fordítóbeállítással kell lefordítania.

Ez a példa a második nem felügyelt mutató helyett indexekkel fér hozzá mindkét tömb elemeihez. A pSource és pTarget mutatóinak deklarációja rögzíti a tömböket.

static unsafe void Copy(byte[] source, int sourceOffset, byte[] target,
    int targetOffset, int count)
{
    // If either array is not instantiated, you cannot complete the copy.
    if ((source == null) || (target == null))
    {
        throw new System.ArgumentException("source or target is null");
    }

    // If either offset, or the number of bytes to copy, is negative, you
    // cannot complete the copy.
    if ((sourceOffset < 0) || (targetOffset < 0) || (count < 0))
    {
        throw new System.ArgumentException("offset or bytes to copy is negative");
    }

    // If the number of bytes from the offset to the end of the array is
    // less than the number of bytes you want to copy, you cannot complete
    // the copy.
    if ((source.Length - sourceOffset < count) ||
        (target.Length - targetOffset < count))
    {
        throw new System.ArgumentException("offset to end of array is less than bytes to be copied");
    }

    // The following fixed statement pins the location of the source and
    // target objects in memory so that they will not be moved by garbage
    // collection.
    fixed (byte* pSource = source, pTarget = target)
    {
        // Copy the specified number of bytes from source to target.
        for (int i = 0; i < count; i++)
        {
            pTarget[targetOffset + i] = pSource[sourceOffset + i];
        }
    }
}

static void UnsafeCopyArrays()
{
    // Create two arrays of the same length.
    int length = 100;
    byte[] byteArray1 = new byte[length];
    byte[] byteArray2 = new byte[length];

    // Fill byteArray1 with 0 - 99.
    for (int i = 0; i < length; ++i)
    {
        byteArray1[i] = (byte)i;
    }

    // Display the first 10 elements in byteArray1.
    System.Console.WriteLine("The first 10 elements of the original are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray1[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of byteArray1 to byteArray2.
    Copy(byteArray1, 0, byteArray2, 0, length);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of the last 10 elements of byteArray1 to the
    // beginning of byteArray2.
    // The offset specifies where the copying begins in the source array.
    int offset = length - 10;
    Copy(byteArray1, offset, byteArray2, 0, length - offset);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");
    /* Output:
        The first 10 elements of the original are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        90 91 92 93 94 95 96 97 98 99
    */
}

Függvénymutatók

A C# delegate típusokat biztosít a biztonságos függvénymutató-objektumok definiálásához. Egy delegált meghívása magában foglalja a System.Delegate-ból származtatott típus példányosítását, valamint egy virtuális metódus meghívását a Invoke metódusra. Ez a virtuális hívás a callvirt IL utasítást használja. A teljesítmény szempontjából kritikus kódútvonalak esetén hatékonyabb az calli IL-utasítás használata.

A függvénymutatót a delegate* szintaxis használatával határozhatja meg. A fordító az utasítással hívja meg a függvényt delegate az calli objektum példányosítása és hívása Invokehelyett. Az alábbi kód két metódust deklarál, amelyek egy delegate vagy egy delegate* használnak két azonos típusú objektum kombinálásához. Az első metódus egy System.Func<T1,T2,TResult> delegált típust használ. A második módszer egy delegate* deklarációt használ ugyanazokkal a paraméterekkel és visszatérési típussal:

public static T Combine<T>(Func<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

public static unsafe T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

Az alábbi kód bemutatja, hogyan deklarál egy statikus helyi függvényt, és hogyan hívhatja meg a UnsafeCombine metódust az adott helyi függvényre mutató mutatóval:

int product = 0;
unsafe
{
    static int localMultiply(int x, int y) => x * y;
    product = UnsafeCombine(&localMultiply, 3, 4);
}

Az előző kód a függvénymutatóként elért függvény számos szabályát szemlélteti:

  • A függvénymutatók csak egy unsafe környezetben deklarálhatók.
  • Csak olyan metódusokat hívhat meg, amelyek kontextusban vesznek fel (delegate*vagy a ) értéket unsafe ad visszadelegate*.
  • A függvény címének lekérésére & operátor csak static függvényeken engedélyezett. Ez a szabály a tagfüggvényekre és a helyi függvényekre egyaránt vonatkozik.

A szintaxis párhuzamot mutat az delegate típus deklarálásával és a mutatók használatával. A * utótag a delegate-n azt jelzi, hogy a deklaráció egy függvénymutató. A &, amikor metóduscsoportot rendel egy függvénymutatóhoz, azt jelzi, hogy a művelet a metódus címét veszi át.

A hívási konvenciót delegate* a kulcsszavak managed és unmanageda . Emellett unmanaged függvénymutatók esetében megadhatja a hívási konvencióciót. Az alábbi deklarációk példákat mutatnak mindegyikre. Az első deklaráció a managed hívási konvenciót használja, amely az alapértelmezett. A következő négy egy unmanaged hívási konvenciót használ. Mindegyik megadja az ECMA 335 hívási konvencióinak egyikét: Cdecl, Stdcall, Fastcallvagy Thiscall. Az utolsó deklaráció a unmanaged hívási konvenciót használja, amely arra utasítja a CLR-t, hogy válassza ki a platform alapértelmezett hívási konvenciót. A CLR futásidőben választja ki a hívási konvenciót.

public static unsafe T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
    combinator(left, right);

A függvénymutatókról a függvénymutató funkciós specifikációjában olvashat bővebben.

C# nyelvspecifikáció

További információt a C# nyelvi specifikációjánaknem biztonságos kód fejezetében talál.

Lásd még