Onveilige code, aanwijzertypen en functie-aanwijzers

De meeste C#-code die u schrijft, is verifieerbaar veilige code. Verifiably safe code betekent dat .NET hulpprogramma's kunnen controleren of de code veilig is. Over het algemeen heeft veilige code geen rechtstreekse toegang tot het geheugen met behulp van aanwijzers. Er wordt ook geen onbewerkt geheugen toegewezen. In plaats daarvan worden beheerde objecten gemaakt.

De C#-taalreferentiedocumenten de laatst uitgebrachte versie van de C#-taal. Het bevat ook de eerste documentatie voor functies in openbare previews voor de aanstaande taalrelease.

De documentatie identificeert alle functies die voor het eerst zijn geïntroduceerd in de laatste drie versies van de taal of in de huidige openbare previews.

Aanbeveling

Raadpleeg het artikel over de versiegeschiedenis van de C#-taal om te achterhalen wanneer een functie voor het eerst is geïntroduceerd in C#.

C# biedt ook ondersteuning voor een unsafe context waarin u niet-verifieerbare code kunt schrijven. Onveilige code is niet noodzakelijkerwijs gevaarlijk; het is alleen code waarvan de veiligheid niet kan worden geverifieerd door .NET hulpprogramma's. U gebruikt onveilige code om systeemeigen functies aan te roepen waarvoor aanwijzers nodig zijn en in sommige gevallen om de prestaties te verbeteren via directe geheugentoegang die controles van matrixgrenzen voorkomt. Onveilige code introduceert ook beveiligings- en stabiliteitsrisico's. Als u code wilt compileren die een unsafe context bevat, voegt u de optie AllowUnsafeBlocks-compiler toe.

C# definieert twee modellen voor wat telt als onveilige code: het oorspronkelijke model en een bijgewerkt model voor geheugenveiligheid dat in preview is in C# 15 en .NET 11. Zie Twee modellen voor onveilige code voor informatie over hoe de twee modellen verschillen.

Zie best practices voor onveilige code in C# voor informatie over aanbevolen procedures voor onveilige code.

Twee modellen voor onveilige code

C# definieert twee modellen voor onveilige code. Het model bepaalt welke bewerkingen een unsafe context vereisen en hoe de unsafe wijzigingsfunctie voor een lid van invloed is op bellers.

  • Oorspronkelijk onveilig model: de unsafe context heeft betrekking op het bestaan van aanwijzerfuncties. U declareert een type aanwijzer, neemt het adres van een variabele, deductie van een aanwijzer, converteert een stackalloc expressie naar een aanwijzer of past alleen sizeof toe op een willekeurig type binnen een unsafe context. (Een stackalloc expressie die is toegewezen aan een Span<T> of ReadOnlySpan<T> is toegestaan in veilige code.) Met de unsafe wijzigingsfunctie voor een type, lid of blok wordt die context vastgelegd, maar worden bellers niet verplicht. C# 1.0 heeft dit model geïntroduceerd en blijft de standaardwaarde.
  • Bijgewerkt model voor geheugenveiligheid: de unsafe context behandelt de bewerkingen die toegang hebben tot het geheugen dat de runtime niet beheert. Het bestaan van een aanwijzer is niet onveilig; de deductie van een aanwijzer is. De unsafe wijzigingsfunctie van een lid wordt een contract dat de verplichting om de veiligheid aan de beller te controleren doordraagt. Dit model is in preview in C# 15 en .NET 11.

De volgende tabel vergelijkt welke bewerkingen een unsafe context in elk model vereisen.

Operation Oorspronkelijk model Bijgewerkt model
Een type aanwijzer declareren of een adres met & Vereist unsafe Toegestaan in veilige code
De fixed-verklaring Vereist unsafe Toegestaan in veilige code
stackalloc Een expressie converteren naar een aanwijzer Vereist unsafe Toegestaan in veilige code
De sizeof operator voor een niet-beheerd type Vereist unsafe Toegestaan in veilige code
Indirectie van aanwijzer (*p), lidtoegang (p->m) of elementtoegang (p[i]) Vereist unsafe Vereist unsafe
Aanroep van functie aanwijzer Vereist unsafe Vereist unsafe
Elementtoegang op een buffer met vaste grootte Vereist unsafe Vereist unsafe
Een lid aanroepen dat is gemarkeerd unsafe Geen bellervereiste Vereist unsafe

Als u het bijgewerkte model wilt proberen, gebruikt u de .NET 11 SDK (in preview) en stelt u de LangVersion compileroptie in op preview. De aanwijzer-ontspanningen gelden wanneer u compileert met de C# 15-compiler en de preview taalversie. De volledige handhaving, inclusief aanroeperverplichtingen en de opt-in voor de assembly, is nog in ontwikkeling. Zie Het bijgewerkte model voor geheugenveiligheid (preview) voor meer informatie.

Het oorspronkelijke onveilige model

In het oorspronkelijke model brengt het unsafe trefwoord een onveilige context tot stand op een type, een lid of een blok, en die context ontgrendelt de aanwijzerfuncties die in de volgende secties worden beschreven. De unsafe wijzigingsfunctie wijzigt alleen wat de gemarkeerde code kan doen; er is geen vereiste voor bellers. Als u een van deze voorbeelden wilt compileren, stelt u de optie AllowUnsafeBlocks-compiler in .

Aanwijzertypen

In een onveilige context kan een type een aanwijzer zijn, naast een waardetype of een verwijzingstype. Een declaratie van het type aanwijzer heeft een van de volgende vormen:

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

Het type dat u opgeeft vóór het * type aanwijzer, is het verwijzingstype.

Aanwijzertypen nemen niet over van object en er zijn geen conversies tussen aanwijzertypen en object. Bovendien bieden boxing en unboxing geen ondersteuning voor pointers. U kunt echter converteren tussen verschillende typen aanwijzers en tussen aanwijzertypen en integrale typen.

Wanneer u meerdere aanwijzers in dezelfde declaratie declareert, schrijft u het sterretje (*) samen met het onderliggende type. Het wordt niet gebruikt als voorvoegsel voor elke aanwijzernaam. Bijvoorbeeld:

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

De garbagecollector houdt niet bij of een object wordt verwezen door eventuele aanwijzertypen. Als de referent een object is in de beheerde heap (inclusief lokale variabelen die zijn vastgelegd door lambda-expressies of anonieme gemachtigden), moet u het object vastmaken zolang de aanwijzer wordt gebruikt.

De waarde van de aanwijzervariabele van het type MyType* is het adres van een variabele van het type MyType. Hier volgen voorbeelden van declaraties van aanwijzertypen:

  • int* p: p is een aanwijzer naar een geheel getal.
  • int** p: p is een aanwijzer naar een geheel getal.
  • int*[] p: p is een eendimensionale matrix met aanwijzers naar gehele getallen.
  • char* p: p is een aanwijzer naar een teken.
  • void* p: p is een aanwijzer naar een onbekend type.

U kunt de operator * voor aanwijzer indirectie gebruiken om toegang te krijgen tot de inhoud op de locatie waarnaar wordt verwezen door de aanwijzervariabele. Denk bijvoorbeeld aan de volgende declaratie:

int* myVariable;

De expressie *myVariable geeft de int variabele aan die is gevonden op het adres in myVariable.

Er zijn verschillende voorbeelden van verwijzingen in de artikelen over de fixed verklaring. In het volgende voorbeeld worden het unsafe trefwoord en de fixed-instructie gebruikt en wordt uitgelegd hoe u een binnenaanwijzer kunt verhogen. U kunt deze code in de hoofdfunctie van een consoletoepassing plakken om deze uit te voeren. Deze voorbeelden moeten worden gecompileerd met de AllowUnsafeBlocks compileroptie.

// 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
*/

U kunt de indirectieoperator niet toepassen op een aanwijzer van het type void*. U kunt echter een cast gebruiken om een void-pointer te converteren naar elk ander type aanwijzer en omgekeerd.

Een aanwijzer kan nullzijn. Het toepassen van de indirectieoperator op een null-aanwijzer veroorzaakt een door de implementatie gedefinieerd gedrag.

Het doorgeven van aanwijzers tussen methoden kan niet-gedefinieerd gedrag veroorzaken. Overweeg een methode die een aanwijzer naar een lokale variabele retourneert via een in, outof ref parameter of als het functieresultaat. Als de aanwijzer is ingesteld in een vast blok, kan de variabele waarop deze wijst mogelijk niet meer worden opgelost.

De volgende tabel bevat de operators en instructies die kunnen worden uitgevoerd op aanwijzers in een onveilige context:

Operator/instructie Gebruik
* Voert aanwijzer indirectie uit.
-> Verwijst naar een lid van een struct via een aanwijzer.
[] Indexeert een aanwijzer.
& Hiermee haalt u het adres van een variabele op.
++ en -- Aanwijzers verhogen en verlagen.
+ en - Voert pointer-arithmetic uit.
==, !=, <, >, <=en >= Vergelijkt aanwijzers.
stackalloc Wijst geheugen toe aan de stack.
fixed verklaring Corrigeert tijdelijk een variabele, zodat het adres ervan kan worden gevonden.

Voor meer informatie over aanwijzer-gerelateerde operatoren, zie .

Elk type aanwijzer kan impliciet worden geconverteerd naar een void* type. Elke aanwijzertype kan de waarde nulltoegewezen krijgen. U kunt elk type aanwijzer expliciet converteren naar een ander type aanwijzer met behulp van een cast-expressie. U kunt ook elk integraal type converteren naar een pointertype, of een pointertype naar een integraal type. Voor deze conversies is een expliciete cast vereist.

In het volgende voorbeeld wordt een int* geconverteerd naar een byte*. U ziet dat de aanwijzer verwijst naar de laagst geadresseerde byte van de variabele. Wanneer u het resultaat achtereenvolgens vergroot tot de grootte van int (4 bytes), kunt u de resterende bytes van de variabele weergeven.

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
    */
}

Buffers met vaste grootte

Matrices zijn verwijzingstypen, dus in veilige code slaat een structveld dat een matrix is alleen een verwijzing naar de elementen van de matrix op, niet naar de elementen zelf. De grootte van het volgende struct is niet afhankelijk van het aantal elementen in de matrix, omdat pathName dit een verwijzing is:

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

Als u de inhoud van de matrix in de struct zelf wilt opslaan, gebruikt u het fixed trefwoord om een buffer met vaste grootte te declareren. Voor het fixed trefwoord is een unsafe context vereist. Buffers met een vaste grootte zijn handig wanneer u methoden schrijft die samenwerken met gegevensbronnen uit andere talen of platforms. Een buffer met vaste grootte kan alle kenmerken of modifiers aannemen die zijn toegestaan voor gewone struct-leden. De enige beperking is dat het matrixtype moet zijnbool: , byte, char, , short, sbyteuintlongintushort, ulong, of : floatdouble

private fixed char name[30];

In het volgende voorbeeld heeft de fixedBuffer matrix een vaste grootte. U gebruikt een fixed instructie om een aanwijzer naar het eerste element te krijgen en vervolgens toegang te krijgen tot de elementen van de matrix via die aanwijzer. Met fixed de instructie wordt het exemplaarveld vastgemaakt aan een specifieke locatie in het fixedBuffer geheugen:

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]);
    }
}

De grootte van de 128-element char array is 256 bytes. Vaste grootte teken buffers nemen altijd 2 bytes per teken in beslag, ongeacht de codering. Deze arraygrootte is hetzelfde, zelfs wanneer char-buffers naar API-methoden of structs met CharSet = CharSet.Auto of CharSet = CharSet.Ansiworden gemarshald. Zie CharSetvoor meer informatie.

In het voorgaande voorbeeld wordt getoond hoe je toegang krijgt tot fixed velden zonder vastzetten. Een andere veelgebruikte matrix met vaste grootte is de bool matrix. De elementen in een bool matrix zijn altijd 1 byte groot. bool matrices zijn niet geschikt voor het maken van bitmatrices of buffers.

Buffers met vaste grootte worden gecompileerd met het System.Runtime.CompilerServices.UnsafeValueTypeAttribute, waarmee de Common Language Runtime (CLR) wordt geïnstrueerd dat een type een onbeheerde array bevat die mogelijk kan overstromen. Geheugen dat is toegewezen met behulp van stackalloc schakelt ook automatisch detectiefuncties voor bufferoverschrijding in de CLR in. In het voorgaande voorbeeld ziet u hoe een buffer met vaste grootte in een unsafe struct.

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

De door de compiler gegenereerde C# voor Buffer wordt als volgt toegeschreven:

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;
}

Buffers met vaste grootte verschillen van reguliere matrices op de volgende manieren:

  • U kunt ze alleen in een unsafe context gebruiken.
  • Ze kunnen alleen instantievelden van structs zijn.
  • Ze zijn altijd vectoren of eendimensionale matrices.
  • De declaratie moet de lengte bevatten, zoals fixed char id[8]. U kunt fixed char id[]niet gebruiken.

Functiewijzers

C# biedt delegate typen voor het definiëren van veilige functiepointerobjecten. Het aanroepen van een gemachtigde omvat het instantiëren van een type dat is afgeleid van System.Delegate en het aanroepen van een virtuele methode naar de Invoke methode. Deze virtuele aanroep maakt gebruik van de callvirt IL-instructie. In prestatiekritieke codepaden is het gebruik van de calli IL-instructie efficiënter.

U kunt een functiepointer definiëren met behulp van de delegate* syntaxis. De compiler roept de functie aan met behulp van de calli instructie in plaats van een delegate object te instantiëren en aan te roepen Invoke. De volgende code declareert twee methoden die gebruikmaken van een delegate of een delegate* om twee objecten van hetzelfde type te combineren. De eerste methode maakt gebruik van een System.Func<T1,T2,TResult> type gemachtigde. De tweede methode maakt gebruik van een delegate*-declaratie met dezelfde parameters en retourtype:

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);

De volgende code laat zien hoe u een statische lokale functie declareert en de UnsafeCombine methode aanroept met behulp van een aanwijzer naar die lokale functie:

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

De voorgaande code illustreert een aantal van de regels voor de functie die als functiepointer wordt geopend:

  • U kunt functieaanwijzers alleen declareren in een unsafe context.
  • U kunt alleen methoden aanroepen die een delegate* (of een delegate*) in een unsafe context gebruiken.
  • De operator & voor het verkrijgen van het adres van een functie is alleen toegestaan voor static functies. Deze regel is van toepassing op zowel lidfuncties als lokale functies.

De syntaxis bevat parallellen met het declareren van delegate typen en het gebruik van aanwijzers. Het achtervoegsel * op delegate geeft aan dat de declaratie een functieaanwijzer is. De & bij het toewijzen van een methodegroep aan een functiepointer geeft aan dat de bewerking het adres van de methode gebruikt.

U kunt de aanroepconventie voor een delegate* opgeven met behulp van de trefwoorden managed en unmanaged. Daarnaast kunt u voor unmanaged functie-aanwijzers de aanroepconventie opgeven. In de volgende declaraties ziet u voorbeelden van elk van deze declaraties. De eerste declaratie maakt gebruik van de managed oproepconventie. Dit is de standaardinstelling. De volgende vier gebruiken een unmanaged-aanroepconventie. Elk geeft een van de ECMA 335-aanroepconventies op: Cdecl, Stdcall, Fastcallof Thiscall. De laatste declaratie maakt gebruik van de unmanaged oproepconventie, waarbij de CLR wordt geïnstrueerd om de standaardaanroepconventie voor het platform te kiezen. De CLR kiest de oproepconventie tijdens runtime.

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);

Meer informatie over functie-aanwijzers vindt u in de sectie Functie-aanwijzers van de C#-taalspecificatie.

Voorbeeld: Aanwijzers gebruiken om een matrix van bytes te kopiëren

In het volgende voorbeeld worden aanwijzers gebruikt om bytes van de ene matrix naar de andere te kopiëren.

In dit voorbeeld wordt het unsafe trefwoord gebruikt, waarmee u aanwijzers in de Copy methode kunt gebruiken. De fixed instructie declareert aanwijzers naar de bron- en doelmatrices. De fixed-instructie pint de locatie van de bron- en doellijsten in het geheugen, zodat garbage collection de lijsten niet verplaatst. Het fixed blok maakt de geheugenblokken vast voor de matrices in het bereik van het blok. Omdat de Copy methode in dit voorbeeld het unsafe trefwoord gebruikt, moet u het compileren met behulp van de optie AllowUnsafeBlocks compileren.

In dit voorbeeld worden de elementen van beide matrices geopend met behulp van indexen in plaats van een tweede onbeheerde aanwijzer. Met de declaratie van de pSource en pTarget aanwijzers worden de matrices vastgemaakt.

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
    */
}

Het bijgewerkte model voor geheugenveiligheid (preview)

Important

Het bijgewerkte model voor geheugenveiligheid is een preview-functie in C# 15 en .NET 11. Het blijft zich ontwikkelen op basis van feedback tijdens de preview-releases. Als u het model wilt proberen, gebruikt u de SDK .NET 11 (preview) en stelt u de LangVersion compileroptie in op preview. De compiler in .NET 11 Preview 5 implementeert de aanwijzerversoepeling, maar dwingt de aanroeperverplichtingen, de opt-in van de assembly of het safe trefwoord nog niet af. Zie de specificatie van de geheugenveiligheidsfunctie voor het volledige ontwerp.

Het bijgewerkte model scheidt twee dingen die het oorspronkelijke model behandelt als één: het bestaan van aanwijzercode en het doorgeven van veiligheidsverplichtingen aan bellers. Het markeren van een lid unsafe staat niet langer alleen aanwijzers in de hoofdtekst toe; het maakt de lidaanroeper onveilig, dus elke beller moet die verplichting doorgeven of het ontladen achter een gevalideerde, veilige bellergrens. Ter ondersteuning van deze scheiding beperkt het model ook de onveilige context: het bestaan van een aanwijzer is niet onveilig, alleen de bewerkingen die toegang krijgen tot het geheugen dat de runtime niet beheert. Met de vermaling kunt u aanwijzers in veilige code vasthouden, doorgeven en retourneren, terwijl unsafe de bewerkingen en leden worden gemarkeerd die de geheugenveiligheid daadwerkelijk kunnen schenden.

Beller onveilige leden

In het oorspronkelijke model staat de unsafe wijzigingsfunctie op een lid alleen aanwijzers toe in de handtekening en hoofdtekst van het lid. Het informeert bellers niet over veiligheid. Het bijgewerkte model geeft de modifier betekenis voor bellers. Wanneer u een lid unsafemarkeert, behandelt de compiler het als aanroeper-onveilig (ook wel vereist-onveilig genoemd): elke beller moet het aanroepen vanuit een unsafe context en de verplichting om de veiligheid te controleren naar die beller verplaatst.

De unsafe wijzigingsfunctie op een handtekening van een lid brengt geen onveilige context meer tot stand voor de hoofdtekst. De twee rollen zijn gesplitst:

  • De unsafe wijzigingsfunctie op de handtekening geeft de verplichting aan bellers door.
  • Een binnenste unsafe blok beperkt de bewerkingen die toegang hebben tot niet-beheerd geheugen.

In de volgende preview-mockup ReadInt32 is de aanroeper onveilig. De handtekening draagt de unsafe wijzigingsfunctie en een binnenste unsafe blok verpakt de deductie:

// 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;
    }
}

Een beller verpakt de oproep in een eigen unsafe blok:

// Preview
unsafe
{
    int value = ReadInt32(buffer);
}

Het bijgewerkte model verscherpt ook enkele gerelateerde regels:

  • De unsafe wijzigingsfunctie produceert een fout bij een typedeclaratie, een statische constructor en een finalizer, omdat de wijzigingsfunctie geen aanroeper heeft om te informeren.
  • Gemachtigden kunnen niet zijn unsafe, omdat een gemachtigde typevormig is.
  • Een type waarvan de constructor parameterloos is unsafe , voldoet niet aan de new() beperking.

Bewerkingen waarvoor een onveilige context is vereist

Voor bewerkingen die toegang hebben tot het puntige geheugen, is een unsafe context vereist:

  • Pointer indirection (*p), pointer member access (p->member) en pointer element access (p[i]).
  • Aanroep van functie aanwijzer.
  • Elementtoegang op een buffer met een vaste grootte.

In het volgende voorbeeld wordt een matrix zonder context vastgemaakt, unsafe maar wordt de aanwijzer in een context gedeductied:

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;
        }
    }
}

Ontspannen bewerkingen

Bewerkingen die geen toegang hebben tot puntig geheugen, hebben geen context meer nodig unsafe :

  • Het declareren van een type aanwijzer en het adres van een variabele met de & operator.
  • De fixed instructie waarmee een variabele wordt vastgemaakt.
  • stackalloc Een expressie converteren naar een aanwijzer.
  • De sizeof operator die wordt toegepast op een niet-beheerd type.

In het volgende voorbeeld worden aanwijzers zonder unsafe context gemaakt en vastgemaakt:

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;
    }
}

Deze ontspanningen gelden wanneer u compileert met de preview taalversie, ongeacht of een assembly zich aanmeldt voor de bijgewerkte regels voor geheugenveiligheid.

Aanroeper-onveilige verplichtingen ontslaan

Een lid dat een beller-onveilige bewerking aanroept, heeft twee keuzes: de verplichting doorgeven of ontslaan.

  • Doorgeven: Markeer uw eigen lid unsafe. De verplichting wordt doorgegeven aan uw bellers. Gebruik doorgifte wanneer u de verplichting zelf niet volledig kunt valideren.
  • Ontlading: Laat de handtekening van uw lid veilig. Valideer de verplichting binnen het lid, meestal met runtimebeveiligingen, en voer vervolgens de onveilige bewerking uit in een binnenste unsafe blok. Een lid dat een binnenste unsafe blok bevat, maar geen eigen handtekening unsafe markeert, is een onveilige grens: hiermee wordt onveilige code omgezet in een veilig aanroepbaar oppervlak.

Met de volgende preview-mockup wordt de invoer gevalideerd met een bewaker, wordt een beheerde matrix vastgemaakt en wordt de aanwijzer gelezen. Bellers hebben geen context nodig unsafe , omdat de methode de verplichting ontlost:

// 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;
        }
    }
}

De null-controle en de matrixlengte sluiten de invoer uit waarmee een leesbewerking voorbij de buffer zou worden uitgevoerd, zodat de deductie binnen het unsafe blok geluid is. De methode laat geen restverplichting achter, dus er wordt een veilige aanroepbare handtekening weergegeven.

Veiligheidsdocumentatie

Een beller-onveilig lid moet vastleggen wat de beller moet garanderen. Het bijgewerkte model moedigt twee complementaire opmerkingenstijlen aan:

  • Een /// <safety> documentatieblok boven de handtekening vermeldt het formele contract: de voorwaarden waaraan een beller moet voldoen. Een analyzer kan een aanroeper-onveilig lid markeren dat er een ontbreekt.
  • Een // SAFETY: opmerking in een unsafe blok registreert waarom de bewerking op die plek klinkt, voor de ontwikkelaars en auditors die de hoofdtekst lezen.

In de volgende preview-mockup ziet u beide stijlen voor een aanroeper onveilige ReadByte methode:

// 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];
    }
}

Het /// <safety> blok vertelt u het contract. Het contract behoort tot de documentatie waar elke beller en revisor het zien.

Onveilige velden

Gebruik de unsafe wijzigingsfunctie voor een veld wanneer het gedeclareerde type geen contracten uitdrukt die het omsluitende type onderhoudt en andere code afhankelijk is van. De onveiligheid bestaat in de kloof tussen wat het typesysteem ziet en wat het type belooft. De wijzigingsfunctie dwingt elke schrijfbewerking naar het veld in een unsafe blok, waardoor de schrijfbewerkingen op één plaats worden beoordeeld.

Het meest duidelijke geval is een veld met een systeemeigen aanwijzer. De aanwijzer declareert niet hoeveel bytes het adres heeft als een System.Span<T> doet, zodat het betreffende type die informatie zelf onderhoudt:

// 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];
        }
    }
}

Een readonly unsafe veld paren het contract met een ingebouwde bewaker: unsafe noemt de invariant en readonly voorkomt een schrijfbewerking die het na de bouw kan breken. Als u een eigenschap markeert of een gebeurtenis unsafe , wordt de aanroeper van het back-upveld niet onveilig. In een struct met [StructLayout(LayoutKind.Explicit)]markeert u elk veld of safeunsafe.

Het veilige trefwoord

Het bijgewerkte model voegt een safe contextueel trefwoord toe waarmee een declaratie wordt bevestigd, waarbij de compiler vereist dat u de keuze expliciet maakt.

Een extern lid roept systeemeigen code aan, zodat de compiler de veiligheid niet kan classificeren. Onder het bijgewerkte model markeert u elke extern declaratie, inclusief een LibraryImport gedeeltelijke methode, safe of unsafe:

// Preview
[LibraryImport("libc")]
internal static safe partial int getpid();

[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)]
internal static unsafe partial nint strlen(byte* str);

getpid neemt geen parameters en retourneert een primitieve, dus de auteur bevestigt dat de oproep veilig is en bellers gebruiken deze zonder ceremonie. strlen neemt een onbewerkte aanwijzer op dat de systeemeigen codededucties, dus de declaratie is unsafe en de verplichting doorgeeft aan bellers. Het weglaten van beide modifiers is een fout, waardoor u de veiligheidsbeslissing moet nemen. Een veld in een struct met expliciete indeling maakt gebruik van dezelfde regel.

Gedrag van opt-in en cross-assembly

Het bijgewerkte model heeft twee onafhankelijke switches op projectniveau:

  • Met een nieuwe eigenschap voor aanmelden worden de bijgewerkte regels ingeschakeld. Wanneer de eigenschap is uitgeschakeld, zijn de oorspronkelijke regels van toepassing. Wanneer dit is ingeschakeld, unsafe wordt op een lid doorgegeven aan aanroepers en wordt de keuze in de compiler vastgelegd in de assembly met het MemorySafetyRulesAttribute kenmerk.
  • De bestaande eigenschap AllowUnsafeBlockspoorten elk uiterlijk van het unsafe trefwoord, inclusief de binnenste blokken op oproepsites. Het is standaard ingesteld falseop , zodat een project op de standaardinstelling geen onveilige API kan aanroepen.

De twee eigenschappen combineren als volgt:

Aanmeldingseigenschap AllowUnsafeBlocks Result
Aan Uit (standaard) De veiligste configuratie. Het project maakt gebruik van het bijgewerkte model en staat geen onveilige code toe.
Aan Aan Het project maakt gebruik van het bijgewerkte model en staat onveilige code toe.
Off Off Het oorspronkelijke model is van toepassing en het project kan geen aanwijzertypen gebruiken.
Off Aan Het oorspronkelijke model is van toepassing en het project kan pointertypen gebruiken.

Of de ene assembly de bijgewerkte regels afdwingt op basis van een andere, hangt af van de kant waarin wordt gekozen:

  • Bijgewerkte modelaanroeper, bijgewerkte modeloproep: de markeringen van unsafe de gebelde gebruiker worden door metagegevens verzonden. De beller verpakt elke aanroep naar een beller onveilig lid in een unsafe blok.
  • Bijgewerkte modelaanroeper, original-model callee: een compatibiliteitsmodus behandelt een lid met een aanwijzer in de handtekening als aanroeper onveilig, zodat de oproepsite een omsluitblok unsafe nodig heeft. In deze modus blijft een api op basis van een aanwijzer op de achtergrond verloren unsafe .
  • Caller van origineel model, bijgewerkte modelaanroep: de oorspronkelijke aanwijzerregels zijn nog steeds van toepassing. Een aanroeper-onveilig lid dat geen aanwijzertype in de handtekening heeft, kan worden aangeroepen vanuit veilige code, omdat de aanroeper van het oorspronkelijke model de nieuwe markeringen niet kan lezen.

C#-taalspecificatie

Zie het hoofdstuk Onveilige code van de C#-taalspecificatievoor meer informatie.

Zie de specificatie van de functie voor geheugenveiligheid voor het ontwerp van het bijgewerkte model voor geheugenveiligheid.

Zie ook