Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
Az ön által írt C#-kód nagy része ellenőrizhetően biztonságos kód. A biztonságos kód azt jelenti, hogy .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# olyan unsafe környezetet is támogat, amelyben ellenőrizhető kódot írhat. A nem biztonságos kód nem feltétlenül veszélyes; csak olyan kód, amelynek biztonságát nem lehet ellenőrizni .NET eszközökkel. Nem biztonságos kóddal hívhatja meg a mutatót igénylő natív függvényeket, és bizonyos esetekben a teljesítmény javítása közvetlen memóriahozzáférés révén, amely elkerüli a tömbhatárok ellenőrzését. A nem biztonságos kód biztonsági és stabilitási kockázatokat is jelent. Egy környezetet tartalmazó unsafe kód fordításához adja hozzá az AllowUnsafeBlocks fordítóbeállítást.
A C# két modellt határoz meg, amelyek nem biztonságos kódnak számítanak: az eredeti modell és egy frissített memóriabiztonsági modell, amely előzetes verzióban látható a C# 15-ben és .NET 11-ben. A két modell különbségeiről a két nem biztonságos kód két modellje című témakörben olvashat.
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.
Két modell a nem biztonságos kódhoz
A C# két modellt határoz meg a nem biztonságos kódhoz. Az érvényben lévő modell meghatározza, hogy mely műveletek igényelnek kontextust unsafe , és hogy a tag módosítója hogyan befolyásolja a unsafe hívókat.
-
Eredeti nem biztonságos modell: A
unsafekörnyezet lefedi a mutatófunkciók meglétét. Deklarálhat egy mutatótípust, megadhatja egy változó címét, elhalaszthatja a mutatót, mutatóvá alakíthat egystackallockifejezést, vagy tetszőleges típusra alkalmazhatjasizeofcsak aunsafekörnyezeten belül. (Egystackallocbiztonságos kódhoz hozzárendeltSpan<T>vagyReadOnlySpan<T>engedélyezett kifejezés.) Egyunsafetípus, tag vagy blokk módosítója létrehozza ezt a kontextust, de nem kötelezi a hívókat. A C# 1.0 bevezette ezt a modellt, és továbbra is az alapértelmezett. -
Frissített memóriabiztonsági modell: A
unsafekörnyezet azokat a műveleteket ismerteti, amelyek a futtatókörnyezet által nem kezelt memóriához férnek hozzá. A mutató megléte nem biztonságos; az egérmutató hareferenssége. Aunsafetag módosítója olyan szerződéssé válik, amely propagálja a hívó számára a biztonsági naplózási kötelezettséget. Ez a modell előzetes verzióban érhető el a C# 15-ben és .NET 11-ben.
Az alábbi táblázat összehasonlítja, hogy mely műveletek igényelnek kontextust unsafe az egyes modellekben.
| Operation | Eredeti modell | Frissített modell |
|---|---|---|
Mutatótípus deklarálása vagy cím felvétele & |
Megköveteli unsafe |
Biztonságos kódban engedélyezett |
A fixed utasítás |
Megköveteli unsafe |
Biztonságos kódban engedélyezett |
stackalloc Kifejezés átalakítása mutatóvá |
Megköveteli unsafe |
Biztonságos kódban engedélyezett |
Az sizeof operátor bármilyen nem felügyelt típuson |
Megköveteli unsafe |
Biztonságos kódban engedélyezett |
Mutató indirekt (*p), taghozzáférés (p->m) vagy elemhozzáférés (p[i]) |
Megköveteli unsafe |
Megköveteli unsafe |
| Függvénymutató meghívása | Megköveteli unsafe |
Megköveteli unsafe |
| Elemhozzáférés rögzített méretű pufferen | Megköveteli unsafe |
Megköveteli unsafe |
Tag meghívása megjelölt unsafe |
Nincs hívókövetelmény | Megköveteli unsafe |
A frissített modell kipróbálásához használja a .NET 11 SDK-t (előzetes verzióban), és állítsa a fordítót a LangVersion következőrepreview: . A mutatólazítások a C# 15 fordítóval és a preview nyelvi verzióval történő fordításkor érvényesek. A teljes végrehajtás, beleértve a hívói kötelezettségeket és a közgyűlési jóváhagyást, még fejlesztés alatt áll. További információt a frissített memóriabiztonsági modell (előzetes verzió) című témakörben talál.
Az eredeti nem biztonságos modell
Az eredeti modellben a unsafe kulcsszó nem biztonságos környezetet hoz létre egy típuson, tagon vagy blokkon, és ez a környezet feloldja a következő szakaszokban ismertetett mutatófunkciókat. A unsafe módosító csak azt módosítja, amit a megjelölt kód képes elvégezni; a hívók számára nem tesz kötelezővé. Ezen példák bármelyikének fordításához állítsa be az AllowUnsafeBlocks fordítóbeállítást.
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:pegy egész számra mutató mutató. -
int** p:pegy egész számra mutató mutató. -
int*[] p:pegész számokra mutató mutatók egydimenziós tömbje. -
char* p:pegy karakterre mutató. -
void* p:pegy 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
A tömbök referenciatípusok, ezért a biztonságos kódban egy tömbként használt strukturáló mező csak a tömb elemeire mutató hivatkozást tárol, magukra az elemekre nem. A következők struct mérete nem függ a tömb elemeinek számától, mert pathName ez egy hivatkozás:
public struct PathArray
{
public char[] pathName;
private int reserved;
}
A tömb tartalmának a szerkezeten belüli tárolásához használja a fixed kulcsszót egy rögzített méretű puffer deklarálásához. A fixed kulcsszónak környezetre unsafe van szüksége. 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ű pufferek bármilyen attribútumot vagy módosító tulajdonságot tartalmazhatnak, amelyek a normál strukturált tagok számára engedélyezettek. Az egyetlen korlátozás az, hogy a tömbtípusnak a következőnek kell lenniebool: , byte, char, shortint, long, sbyte, ushort, , uint, ulong, float, vagy double:
private fixed char name[30];
Az alábbi példában a fixedBuffer tömb mérete rögzített. Egy utasítássalfixed mutatót kap az első elemhez, majd ezen a mutatón keresztül éri el a tömb elemeit. Az fixed utasítás a példánymezőt egy fixedBuffer 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
unsafehaszná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]. Afixed char id[]nem használható.
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 calli az delegate 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
unsafekörnyezetben deklarálhatók. - Csak olyan metódusokat hívhat meg, amelyek kontextusban vesznek fel (
delegate*vagy a ) értéketdelegate*ad visszaunsafe. - A függvény címének lekérésére
&operátor csakstaticfü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 C# nyelv specifikációjának Függvénymutatók szakaszában olvashat bővebben.
Példa: Bájtok tömbjének másolása mutatókkal
Az alábbi példa mutatókkal másol bájtokat az egyik tömbből a másikba.
Ez a példa a kulcsszót unsafe használja, amely lehetővé teszi a mutatók használatát a Copy metódusban. Az fixed 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
*/
}
A frissített memóriabiztonsági modell (előzetes verzió)
Important
A frissített memóriabiztonsági modell a C# 15 és .NET 11 előzetes verziója. Az előzetes kiadások során a visszajelzések alapján tovább fejlődik. A modell kipróbálásához használja a .NET 11 (előzetes verzió) SDK-t, és állítsa a fordítót a LangVersion következőrepreview. A .NET 11 Preview 5 fordítója végrehajtja a mutató lazítását, de még nem érvényesíti a hívói kötelezettségeket, a szerelvény-jóváhagyást vagy a kulcsszótsafe. A teljes kialakításért tekintse meg a memóriabiztonsági funkciók specifikációját.
A frissített modell két dolgot választ el egymástól, amelyeket az eredeti modell egyként kezel: a mutatókód meglétét és a hívókra vonatkozó biztonsági kötelezettségek propagálását . A tag unsafe megjelölése már nem csak lehetővé teszi a mutatókat a törzsében; nem biztonságossá teszi a tag hívóját, így minden hívónak propagálja ezt a kötelezettséget, vagy egy érvényesített, biztonságosan hívható határ mögé kell ürítenie. Az elkülönítés támogatásához a modell a nem biztonságos környezetet is szűkíti: a mutató megléte nem biztonságos, csak a futtatókörnyezet memóriájához hozzáférő műveletek nem kezelhetők. A szűkítés lehetővé teszi a mutatók biztonságos kódban való tárolását, átadását és visszaadását, miközben unsafe megjelöli azokat a műveleteket és tagokat, amelyek ténylegesen megsérthetik a memóriabiztonságot.
Hívó nem biztonságos tagjai
Az eredeti modellben a unsafe tag módosítója csak a tag aláírásában és törzsében engedélyezi a mutatókat. Nem tájékoztatja a hívókat a biztonságról. A frissített modell a hívók módosító jelentését adja. Ha megjelöl egy tagot unsafe, a fordító hívó-nem biztonságosnak (más néven nem biztonságosnak) kezeli: minden hívónak meg kell hívnia egy unsafe környezetből, és a biztonsági naplózási kötelezettség a hívóra kerül.
A unsafe tagajándék módosítója már nem hoz létre nem biztonságos környezetet a szervezet számára. A két szerepkör felosztása:
- Az
unsafealáírás módosítója propagálja a hívókra vonatkozó kötelezettséget. - A belső
unsafeblokk hatóköre a nem felügyelt memóriához hozzáférő műveletekre terjed ki.
Az alábbi mintaképben ReadInt32 a hívó nem biztonságos. Az aláírás hordozza a unsafe módosító, és egy belső unsafe blokk burkolja a dereference:
// Preview: illustrates the updated model, which the current compiler doesn't fully enforce yet.
public static unsafe int ReadInt32(byte* source)
{
unsafe
{
return *(int*)source;
}
}
A hívó a hívást a saját unsafe blokkjában burkolja:
// Preview
unsafe
{
int value = ReadInt32(buffer);
}
A frissített modell néhány kapcsolódó szabályt is szigorít:
- A
unsafemódosító hibát okoz egy típusdeklaráción, egy statikus konstruktoron és egy véglegesítőn, mert a módosítónak nincs értesítendő hívója. - A meghatalmazottak nem lehetnek
unsafe, mert a meghatalmazott típusalakzatú. - Az a típus, amelynek paraméter nélküli konstruktora
unsafenem felel meg a kényszerneknew().
Nem biztonságos környezetet igénylő műveletek
A kiemelt memóriához hozzáférő műveletekhez környezet szükséges unsafe :
- Mutató indirekt (
*p), mutatótag-hozzáférés (p->member) és mutatóelem-hozzáférés (p[i]). - Függvénymutató meghívása.
- Elemhozzáférés rögzített méretű pufferen.
Az alábbi példa egy környezet nélküli unsafe tömböt rögzít, de az egérmutatót az egyiken belülre halasztja:
public static int ReadValue(int[] numbers)
{
fixed (int* first = numbers)
{
// Dereferencing a pointer accesses unmanaged memory, so it still
// requires an unsafe context.
unsafe
{
return *first;
}
}
}
Nyugodt műveletek
Azok a műveletek, amelyek nem férnek hozzá a kiemelt memóriához, már nem igényelnek kontextust unsafe :
- Mutatótípus deklarálása és egy változó címének felvétele az
&operátorral. - Az
fixedaz utasítás, amely rögzít egy változót. - Kifejezés átalakítása
stackallocmutatóvá. - Az
sizeofoperátor bármilyen nem felügyelt típusra alkalmazva.
Az alábbi példa kontextus nélkül hoz létre és rögzít mutatókat unsafe :
public static void CreatePointer()
{
int value = 42;
// Creating a pointer doesn't require an unsafe context.
int* pointer = &value;
int** pointerToPointer = &pointer;
}
public static void PinArray(int[] numbers)
{
// The fixed statement no longer requires an unsafe context.
fixed (int* first = numbers)
{
int* current = first;
}
}
Ezek a lazítások a nyelvi verzióval preview történő fordításkor érvényesek, függetlenül attól, hogy egy szerelvény a frissített memóriabiztonsági szabályok mellett dönt-e.
A hívó nem biztonságos kötelezettségeinek teljesítése
A hívó nem biztonságos műveletet hívó tagnak két lehetősége van: propagálja a kötelezettséget, vagy mentesíti azt.
-
Propagálás: Saját tag
unsafemegjelölése. A kötelezettség átmegy a hívókra. Propagálást akkor használjon, ha nem tudja teljes mértékben érvényesíteni a kötelezettséget. -
Mentesítés: Hagyja biztonságban a tag aláírását. Ellenőrizze a tagon belüli kötelezettséget, általában futtatókörnyezeti őrökkel, majd hajtsa végre a nem biztonságos műveletet egy belső
unsafeblokkban. A belsőunsafeblokkot tartalmazó, de nem saját aláírástunsafetartalmazó tag nem biztonságos határ: biztonságosan hívható felületté alakítja a nem biztonságos kódot.
Az alábbi mintakép egy védővel ellenőrzi a bemenetét, rögzít egy felügyelt tömböt, és végigolvassa az egérmutatót. A hívóknak nincs szükségük kontextusra unsafe , mert a metódus teljesíti a kötelezettséget:
// Preview
public static int SumBytes(byte[] source)
{
ArgumentNullException.ThrowIfNull(source);
fixed (byte* first = source)
{
unsafe
{
// SAFETY: the null check and source.Length bound every read to the pinned array.
int total = 0;
for (int i = 0; i < source.Length; i++)
{
total += first[i];
}
return total;
}
}
}
A null értékű ellenőrzés és a tömb hossza kizárja azokat a bemeneteket, amelyek lehetővé teszik az olvasás futtatását a pufferen, így a unsafe blokkon belüli dereferencia hang. A módszer nem hagy hátralevő kötelezettséget, ezért biztonságosan hívható aláírást tesz elérhetővé.
Biztonsági dokumentáció
A hívó nem biztonságos tagjának dokumentálnia kell, hogy mit kell garantálnia a hívónak. A frissített modell két kiegészítő megjegyzésstílust támogat:
- Az
/// <safety>aláírás feletti dokumentációs blokk a hivatalos szerződést írja elő: a hívónak meg kell felelnie a feltételeknek. Az elemző megjelölhet egy hívó nem biztonságos tagot, aki hiányzik. - A
// SAFETY:blokkon belüliunsafemegjegyzés rögzíti, hogy a művelet miért hangzik el ezen a helyen, a szervezetet olvasott fejlesztők és auditorok számára.
Az alábbi mintakép mindkét stílust megjeleníti egy hívó-nem biztonságos ReadByte metóduson:
// Preview
/// <summary>Reads a single byte from unmanaged memory.</summary>
/// <safety>
/// The sum of <paramref name="ptr"/> and <paramref name="offset"/> must address a byte
/// the caller is permitted to read.
/// </safety>
public static unsafe byte ReadByte(IntPtr ptr, int offset)
{
byte* address = (byte*)ptr;
unsafe
{
// SAFETY: relies on the caller obligation stated in the <safety> block.
return address[offset];
}
}
A /// <safety> blokk közli a szerződést. A szerződés abban a dokumentációban szerepel, amelyben minden hívó és véleményező látja.
Nem biztonságos mezők
Akkor használja a unsafe módosítót egy mezőhöz, ha a deklarált típusa nem fejezi ki azokat a szerződéseket, amelyeket a beágyazási típus tart fenn, és más kód függ. A nem biztonságosság abban a résben van, amit a típusrendszer lát, és amit a típus ígér. A módosító minden írást blokkba unsafe kényszerít a mezőre, így az írások egy helyen áttekinthetőek maradnak.
A legtisztább eset egy natív mutatót tartalmazó mező. Az egérmutató nem deklarálja, hogy hány bájtot ad System.Span<T> meg, így az azt tartalmazó típus megőrzi magát az információt:
// Preview
public class NativeBuffer
{
/// <safety>
/// Null, or points to a buffer of Length bytes.
/// </safety>
private unsafe byte* _pointer;
public int Length { get; }
public byte ReadAt(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Length);
unsafe
{
// SAFETY: the bounds checks confine the read to the buffer that _pointer addresses.
return _pointer[index];
}
}
}
A readonly unsafe mező egy beépített védővel párosítja a szerződést: unsafe elnevezi az invariánst, és readonly megakadályozza az írást, amely a építés után megszakíthatja azt. Egy tulajdonság vagy esemény unsafe megjelölése nem teszi biztonságossá a háttérbeli hívót. A struct with [StructLayout(LayoutKind.Explicit)], akkor jelölje meg az összes mezőt vagy safeunsafe.
A biztonságos kulcsszó
A frissített modell egy safe olyan környezeti kulcsszót ad hozzá, amely tanúsítja, hogy a deklaráció megfelelő, ahol a fordító megköveteli, hogy a választás explicit legyen.
Egy extern tag natív kódba hív, így a fordító nem tudja besorolni a biztonságát. A frissített modellben minden extern deklarációt megjelölhet, beleértve egy részleges metódust LibraryImport is, vagy safeunsafe:
// Preview
[LibraryImport("libc")]
internal static safe partial int getpid();
[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)]
internal static unsafe partial nint strlen(byte* str);
getpid nem használ paramétereket, és primitív értéket ad vissza, így a szerző igazolja, hogy a hívás biztonságos, és a hívók ceremónia nélkül használják.
strlen a natív kód dereferenseinek nyers mutatója, így a deklaráció unsafe a hívókkal kapcsolatos kötelezettség propagálása és propagálása. Mindkét módosító kihagyása hiba, ami arra kényszeríti, hogy a biztonsági döntést meghozhassa. Az explicit elrendezésű struktúra mezői ugyanazt a szabályt használják.
A bejelentkezés és a szerelvényközi működés
A frissített modell két független projektszintű kapcsolóval rendelkezik:
- Egy új opt-in tulajdonság bekapcsolja a frissített szabályokat. Ha a tulajdonság ki van kapcsolva, az eredeti szabályok érvényesek. Ha be van kapcsolva,
unsafeegy tag propagálja a hívóknak, és a fordító rögzíti a választott szerelvényt az MemorySafetyRulesAttribute attribútummal. - A meglévő AllowUnsafeBlocks tulajdonság a kulcsszó minden megjelenését
unsafe, beleértve a hívási helyek belső blokkjait is. Alapértelmezés szerint ígyfalseegy alapértelmezett projekt nem hívhat meg nem biztonságos API-t.
A két tulajdonság a következőképpen kombinálható:
| Opt-in tulajdonság | AllowUnsafeBlocks |
Result |
|---|---|---|
| Bekapcsolva | Kikapcsolva (alapértelmezett) | A legbiztonságosabb konfiguráció. A projekt a frissített modellt használja, és nem engedélyezi a nem biztonságos kódot. |
| Bekapcsolva | Bekapcsolva | A projekt a frissített modellt használja, és engedélyezi a nem biztonságos kódot. |
| Off | Off | Az eredeti modell érvényes, és a projekt nem használhat mutatótípusokat. |
| Off | Bekapcsolva | Az eredeti modell érvényes, és a projekt használhat mutatótípusokat. |
Attól függ, hogy az egyik szerelvény kikényszeríti-e a frissített szabályokat egy másikkal szemben, attól függ, hogy melyik oldal dönti el:
-
Frissített modell hívója, frissített modell hívója: A hívó jelölői
unsafemetaadatokon haladnak át. A hívó egy blokk hívó nem biztonságos tagjának minden hívását körbefuttatjaunsafe. -
Frissített modell hívója, eredeti modellbeli hívó: A kompatibilitási mód minden olyan hívó tagot kezel, akinek az aláírásában mutatótípus szerepel, nem biztonságos, ezért a hívóhelynek elzárt
unsafeblokkra van szüksége. Ez a mód megakadályozza, hogy a mutatóalapú API csendben elveszítse a követelményétunsafe. - Eredeti modell hívója, frissített modell hívója: Az eredeti mutatószabályok továbbra is érvényesek. A hívó nem biztonságos tagja, amelynek nincs mutatótípusa az aláírásában, biztonságos kódból hívhatóvá válik, mert az eredeti modell hívója nem tudja beolvasni az új jelölőket.
C# nyelvspecifikáció
További információt a C# nyelvi specifikációjánaknem biztonságos kód fejezetében talál.
A frissített memóriabiztonsági modell kialakításához tekintse meg a memóriabiztonsági funkció specifikációját.