Codice non sicuro, tipi di puntatore e puntatori a funzione

La maggior parte del codice C# scritto è un codice verificabilemente sicuro. Codice sicuro significa che .NET strumenti possono verificare che il codice sia sicuro. In generale, il codice sicuro non accede direttamente alla memoria usando puntatori. Inoltre, non alloca memoria grezza. Crea invece oggetti gestiti.

Il riferimento al linguaggio C# documenta la versione rilasciata più di recente del linguaggio C#. Contiene anche la documentazione iniziale per le funzionalità nelle anteprime pubbliche per la versione futura del linguaggio.

La documentazione identifica tutte le funzionalità introdotte nelle ultime tre versioni della lingua o nelle anteprime pubbliche correnti.

Suggerimento

Per trovare quando una funzionalità è stata introdotta per la prima volta in C#, vedere l'articolo sulla cronologia delle versioni del linguaggio C#.

C# supporta anche un unsafe contesto in cui è possibile scrivere codice non verificabile . Il codice non sicuro non è necessariamente pericoloso; è solo codice la cui sicurezza non può essere verificata dagli strumenti di .NET. È possibile usare codice unsafe per chiamare funzioni native che richiedono puntatori e in alcuni casi per migliorare le prestazioni tramite l'accesso diretto alla memoria che evita controlli con limiti di matrice. Il codice unsafe introduce anche rischi per la sicurezza e la stabilità. Per compilare il codice che contiene un unsafe contesto, aggiungere l'opzione del compilatore AllowUnsafeBlocks .

C# definisce due modelli per ciò che conta come codice non sicuro: il modello originale e un modello di sicurezza della memoria aggiornato disponibile in anteprima in C# 15 e .NET 11. Per informazioni sulle differenze tra i due modelli, vedere Due modelli per il codice non sicuro.

Per informazioni sulle procedure consigliate per il codice unsafe in C#, vedere Procedure consigliate per il codice unsafe.

Due modelli per il codice unsafe

C# definisce due modelli per il codice non sicuro. Il modello determina in effetti quali operazioni richiedono un unsafe contesto e il modo in cui il unsafe modificatore di un membro influisce sui chiamanti.

  • Modello unsafe originale: il unsafe contesto illustra l'esistenza di funzionalità del puntatore. Si dichiara un tipo di puntatore, si accetta l'indirizzo di una variabile, si dereferenzia un puntatore, si converte un'espressione stackalloc in un puntatore o si applica sizeof a un tipo arbitrario solo all'interno di un unsafe contesto. Un'espressione stackalloc assegnata a un Span<T> oggetto o ReadOnlySpan<T> è consentita nel codice sicuro. Il unsafe modificatore in un tipo, un membro o un blocco stabilisce tale contesto, ma non impone alcun obbligo ai chiamanti. C# 1.0 ha introdotto questo modello e rimane l'impostazione predefinita.
  • Modello di sicurezza della memoria aggiornato: il unsafe contesto illustra le operazioni che accedono alla memoria che il runtime non gestisce. L'esistenza di un puntatore non è sicura; la dereferenziazione di un puntatore è . Il unsafe modificatore di un membro diventa un contratto che propaga l'obbligo di controllare la sicurezza al chiamante. Questo modello è disponibile in anteprima in C# 15 e .NET 11.

Nella tabella seguente vengono confrontate le operazioni che richiedono un unsafe contesto in ogni modello.

Operation Modello originale Modello aggiornato
Dichiarare un tipo di puntatore o accettare un indirizzo con & Richiede unsafe Consentito nel codice sicuro
Istruzione fixed Richiede unsafe Consentito nel codice sicuro
Convertire un'espressione stackalloc in un puntatore Richiede unsafe Consentito nel codice sicuro
sizeof Operatore in qualsiasi tipo non gestito Richiede unsafe Consentito nel codice sicuro
Riferimento indiretto puntatore (*p), accesso ai membri (p->m) o accesso agli elementi (p[i]) Richiede unsafe Richiede unsafe
Chiamata del puntatore di funzione Richiede unsafe Richiede unsafe
Accesso degli elementi in un buffer a dimensione fissa Richiede unsafe Richiede unsafe
Chiamare un membro contrassegnato unsafe Nessun requisito del chiamante Richiede unsafe

Per provare il modello aggiornato, usare .NET 11 SDK (in anteprima) e impostare l'opzione del LangVersion compilatore su preview. I relax del puntatore si applicano ogni volta che si compila con il compilatore C# 15 e la versione del preview linguaggio. L'applicazione completa, inclusi gli obblighi del chiamante e il consenso esplicito dell'assemblea, è ancora in fase di sviluppo. Per altre informazioni, vedere Il modello di sicurezza della memoria aggiornato (anteprima).

Modello non sicuro originale

Nel modello originale la unsafe parola chiave stabilisce un contesto non sicuro su un tipo, un membro o un blocco e tale contesto sblocca le funzionalità del puntatore descritte nelle sezioni seguenti. Il unsafe modificatore modifica solo le operazioni che il codice contrassegnato può eseguire. Non impone alcun requisito ai chiamanti. Per compilare uno di questi esempi, impostare l'opzione del compilatore AllowUnsafeBlocks .

Tipi di puntatore

In un contesto non sicuro, un tipo può essere un tipo puntatore, oltre a un tipo valore o a un tipo riferimento. Una dichiarazione di tipo puntatore assume una delle forme seguenti:

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

Il tipo specificato prima di * in un tipo di puntatore è il tipo referenziale.

I tipi puntatore non ereditano dall'oggetto e non esistono conversioni tra i tipi di puntatore e object. Inoltre, la conversione boxing e unboxing non supporta i puntatori. Tuttavia, è possibile eseguire la conversione tra tipi di puntatore diversi e tra tipi puntatore e tipi integrali.

Quando si dichiarano più puntatori nella stessa dichiarazione, scrivere l'asterisco (*) insieme al tipo sottostante. Non viene usato come prefisso per ogni nome del puntatore. Per esempio:

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

Il Garbage Collector non tiene traccia del fatto che un oggetto venga puntato da qualsiasi tipo di puntatore. Se il referenziale è un oggetto nell'heap gestito (incluse le variabili locali acquisite da espressioni lambda o delegati anonimi), è necessario aggiungere l'oggetto per tutto il tempo in cui viene usato il puntatore.

Il valore della variabile puntatore di tipo MyType* è l'indirizzo di una variabile di tipo MyType. Di seguito sono riportati esempi di dichiarazioni di tipo puntatore:

  • int* p: p è un puntatore a un numero intero.
  • int** p: p è un puntatore a un puntatore a un numero intero.
  • int*[] p: p è una matrice unidimensionale di puntatori a interi.
  • char* p: p è un puntatore a un carattere.
  • void* p: p è un puntatore a un tipo sconosciuto.

È possibile usare l'operatore * di riferimento indiretto del puntatore per accedere al contenuto nella posizione a cui punta la variabile puntatore. Si consideri ad esempio la dichiarazione seguente:

int* myVariable;

L'espressione *myVariable indica la variabile int trovata nell'indirizzo contenuto in myVariable.

Esistono diversi esempi di puntatori negli articoli sull'istruzione fixed. Nell'esempio seguente viene usata la parola chiave unsafe e l'istruzione fixed e viene illustrato come incrementare un puntatore interno. È possibile incollare questo codice nella funzione Main di un'applicazione console per eseguirlo. Questi esempi devono essere compilati con il set di opzioni del compilatore AllowUnsafeBlocks.

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

Non è possibile applicare l'operatore di riferimento indiretto a un puntatore di tipo void*. Tuttavia, è possibile usare un cast per convertire un puntatore void in qualsiasi altro tipo di puntatore e viceversa.

Un puntatore può essere null. L'applicazione dell'operatore di riferimento indiretto a un puntatore Null causa un comportamento definito dall'implementazione.

Il passaggio di puntatori tra metodi può causare un comportamento non definito. Si consideri un metodo che restituisce un puntatore a una variabile locale tramite un parametro in, outo ref o come risultato della funzione. Se il puntatore è stato impostato in un blocco fisso, la variabile a cui punta potrebbe non essere più fissa.

La tabella seguente elenca gli operatori e le istruzioni che possono operare sui puntatori in un contesto non sicuro:

Operatore/Istruzione Usare
* Esegue un'indirezione del puntatore.
-> Accede a un membro di uno struct tramite un puntatore.
[] Indicizza un puntatore.
& Ottiene l'indirizzo di una variabile.
++ e -- Incrementa e decrementa i puntatori.
+ e - Esegue l'aritmetica del puntatore.
==, !=, <, >, <=e >= Confronta i puntatori.
stackalloc Alloca memoria nello stack.
fixed dichiarazione Corregge temporaneamente una variabile in modo che sia possibile trovare il relativo indirizzo.

Per altre informazioni sugli operatori correlati al puntatore, vedere operatori correlati al puntatore.

Qualsiasi tipo di puntatore può essere convertito in modo implicito in un tipo void*. A qualsiasi tipo di puntatore è possibile assegnare il valore null. È possibile convertire in modo esplicito qualsiasi tipo di puntatore in qualsiasi altro tipo di puntatore usando un'espressione cast. È anche possibile convertire qualsiasi tipo integrale in un tipo puntatore o qualsiasi tipo di puntatore in un tipo integrale. Queste conversioni richiedono un cast esplicito.

Nell'esempio seguente viene convertito un int* in un byte*. Si noti che il puntatore punta al byte indirizzato più basso della variabile. Quando si incrementa successivamente il risultato, fino alle dimensioni di int (4 byte), è possibile visualizzare i byte rimanenti della variabile.

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

Buffer a dimensione fissa

Le matrici sono tipi di riferimento, quindi nel codice sicuro, un campo struct che è una matrice archivia solo un riferimento agli elementi della matrice, non agli elementi stessi. Le dimensioni delle seguenti struct non dipendono dal numero di elementi nella matrice, perché pathName è un riferimento:

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

Per archiviare il contenuto della matrice all'interno dello struct stesso, usare la fixed parola chiave per dichiarare un buffer a dimensione fissa. La fixed parola chiave richiede un unsafe contesto. I buffer a dimensione fissa sono utili quando si scrivono metodi che interagiscono con le origini dati di altre lingue o piattaforme. Un buffer a dimensione fissa può accettare qualsiasi attributo o modificatori consentiti per i normali membri dello struct. L'unica restrizione è che il tipo di matrice deve essere bool, intshortcharbyte, longsbyteuintulongushortfloato :double

private fixed char name[30];

Nell'esempio seguente la matrice di fixedBuffer ha una dimensione fissa. Usare un'istruzionefixed per ottenere un puntatore al primo elemento, quindi accedere agli elementi della matrice tramite tale puntatore. L'istruzione fixed aggiunge il campo dell'istanza fixedBuffer a una posizione specifica in memoria:

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

La dimensione della matrice char di 128 elementi è 256 byte. I buffer char a dimensione fissa occupano sempre 2 byte per carattere, indipendentemente dalla codifica. Questa dimensione della matrice rimane la stessa anche quando i buffer char vengono sottoposti a marshalling verso metodi o struct API con CharSet = CharSet.Auto o CharSet = CharSet.Ansi. Per altre informazioni, vedere CharSet.

Nell'esempio precedente viene illustrato l'accesso ai campi fixed senza fissare. Un'altra matrice a dimensione fissa comune è la matrice bool bool. Gli elementi di una matrice di bool sono sempre di 1 byte. Gli array bool non sono appropriati per la creazione di array di bit o buffer.

I buffer a dimensione fissa vengono compilati con la System.Runtime.CompilerServices.UnsafeValueTypeAttribute, che indica al Common Language Runtime (CLR) che un tipo contiene un array non gestito che può potenzialmente overflow. La memoria allocata tramite stackalloc abilita automaticamente anche le funzionalità di rilevamento dell'overrun del buffer in CLR. Nell'esempio precedente viene illustrato come può esistere un buffer a dimensione fissa in un oggetto unsafe struct.

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

L'attributo C# generato dal compilatore per Buffer è il seguente:

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

I buffer a dimensione fissa differiscono dalle matrici normali nei modi seguenti:

  • È possibile usarli solo in un unsafe contesto.
  • Possono essere solo campi di istanza di struct.
  • Sono sempre vettori o matrici unidimensionali.
  • La dichiarazione deve includere la lunghezza, ad esempio fixed char id[8]. Non è possibile usare fixed char id[].

Puntatori a funzione

C# fornisce delegate tipi per definire oggetti puntatore a funzione sicuri. La chiamata di un delegato comporta la creazione di un'istanza di un tipo derivato da System.Delegate e l'esecuzione di una chiamata di metodo virtuale al relativo metodo Invoke. Questa chiamata virtuale usa l'istruzione callvirt IL. Nei percorsi di codice critici per le prestazioni, l'uso dell'istruzione calli IL è più efficiente.

È possibile definire un puntatore a funzione usando la delegate* sintassi . Il compilatore chiama la funzione usando l'istruzione anziché creare un'istanza calli di un delegate oggetto e chiamando Invoke. Il codice seguente dichiara due metodi che usano un delegate o un delegate* per combinare due oggetti dello stesso tipo. Il primo metodo usa un tipo delegato System.Func<T1,T2,TResult>. Il secondo metodo usa una dichiarazione delegate* con gli stessi parametri e tipo restituito:

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

Il codice seguente illustra come dichiarare una funzione locale statica e richiamare il UnsafeCombine metodo usando un puntatore a tale funzione locale:

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

Il codice precedente illustra alcune delle regole relative alla funzione a cui si accede tramite un puntatore a funzione.

  • È possibile dichiarare puntatori a funzione solo in un unsafe contesto.
  • È possibile chiamare solo metodi che accettano ( delegate* o restituiscono ) delegate*in un unsafe contesto.
  • L'operatore & per ottenere l'indirizzo di una funzione è consentito solo nelle funzioni di static. Questa regola si applica sia alle funzioni membro che alle funzioni locali.

La sintassi presenta paralleli con la dichiarazione dei tipi delegate e l'uso di puntatori. Il suffisso * in delegate indica che la dichiarazione è un puntatore a funzione . Il & quando si assegna un gruppo di metodi a un puntatore a una funzione indica che l'operazione prende l'indirizzo del metodo.

È possibile specificare la convenzione di chiamata per un delegate* oggetto usando le parole chiave managed e unmanaged. Inoltre, per i puntatori a funzione unmanaged, è possibile specificare la convenzione di chiamata. Le dichiarazioni seguenti mostrano esempi di ognuno di essi. La prima dichiarazione usa la convenzione di chiamata managed, ovvero l'impostazione predefinita. Le quattro successive usano una convenzione di chiamata unmanaged. Ognuno specifica una delle convenzioni di chiamata ECMA 335: Cdecl, Stdcall, Fastcallo Thiscall. L'ultima dichiarazione usa la convenzione di chiamata unmanaged, che indica a CLR di selezionare la convenzione di chiamata predefinita per la piattaforma. Il CLR sceglie la convenzione di chiamata in fase di esecuzione.

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

Per altre informazioni sui puntatori a funzione, vedere la sezione Puntatori a funzioni della specifica del linguaggio C#.

Esempio: usare i puntatori per copiare una matrice di byte

Nell'esempio seguente vengono usati puntatori per copiare i byte da una matrice a un'altra.

In questo esempio viene usata la unsafe parola chiave , che consente di usare i puntatori nel Copy metodo . L'istruzione fixed dichiara puntatori alle matrici di origine e di destinazione. L'istruzione fixedi pin la posizione delle matrici di origine e di destinazione in memoria in modo che Garbage Collection non sposti le matrici. Il fixed blocco aggiunge i blocchi di memoria per le matrici nell'ambito del blocco. Poiché il Copy metodo in questo esempio usa la unsafe parola chiave , è necessario compilarlo usando l'opzione del compilatore AllowUnsafeBlocks .

Questo esempio accede agli elementi di entrambe le matrici usando indici anziché un secondo puntatore non gestito. La dichiarazione dei puntatori pSource e pTarget fissa le matrici.

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

Modello di sicurezza della memoria aggiornato (anteprima)

Importante

Il modello di sicurezza della memoria aggiornato è una funzionalità di anteprima in C# 15 e .NET 11. Continua a evolversi in base al feedback durante le versioni di anteprima. Per provare il modello, usare l'SDK .NET 11 (anteprima) e impostare l'opzione del LangVersion compilatore su preview. Il compilatore in .NET 11 Preview 5 implementa i relax del puntatore, ma non applica ancora gli obblighi del chiamante, il consenso esplicito dell'assembly o la safe parola chiave . Per la progettazione completa, vedere la specifica della funzionalità di sicurezza della memoria.

Il modello aggiornato separa due aspetti che il modello originale considera come uno: l'esistenza del codice puntatore e la propagazione degli obblighi di sicurezza ai chiamanti. Contrassegnare un membro unsafe non consente più solo puntatori nel suo corpo; rende il chiamante membro non sicuro, quindi ogni chiamante deve propagare tale obbligo o scaricarlo dietro un limite convalidato e sicuro chiamabile. Per supportare tale separazione, il modello restringe anche il contesto non sicuro: l'esistenza di un puntatore non è sicura, ma solo le operazioni che accedono alla memoria non vengono gestite dal runtime. Il restringimento consente di contenere, passare e restituire puntatori nel codice sicuro, mentre unsafe contrassegna le operazioni e i membri che possono effettivamente violare la sicurezza della memoria.

Membri non sicuri del chiamante

Nel modello originale, il unsafe modificatore di un membro consente solo puntatori nella firma e nel corpo del membro. Non informa i chiamanti sulla sicurezza. Il modello aggiornato fornisce il significato del modificatore per i chiamanti. Quando si contrassegna un membro unsafe, il compilatore lo considera come chiamante-unsafe (detto anche requires-unsafe): ogni chiamante deve richiamarlo da un unsafe contesto e l'obbligo di controllare il passaggio di sicurezza a tale chiamante.

Il unsafe modificatore in una firma membro non stabilisce più un contesto non sicuro per il corpo. I due ruoli sono suddivisi:

  • Il unsafe modificatore sulla firma propaga l'obbligo ai chiamanti.
  • Un blocco interno unsafe definisce l'ambito delle operazioni che accedono alla memoria non gestita.

Nella simulazione di anteprima seguente il ReadInt32 chiamante non è sicuro. La firma contiene il unsafe modificatore e un blocco interno unsafe esegue il wrapping della dereferenziazione:

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

Un chiamante esegue il wrapping della chiamata nel proprio unsafe blocco:

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

Il modello aggiornato applica anche alcune regole correlate:

  • Il unsafe modificatore genera un errore in una dichiarazione di tipo, un costruttore statico e un finalizzatore, perché il modificatore non dispone di alcun chiamante da informare.
  • I delegati non possono essere unsafe, perché un delegato è a forma di tipo.
  • Tipo il cui costruttore senza parametri non soddisfa unsafe il new() vincolo.

Operazioni che richiedono un contesto non sicuro

Le operazioni che accedono alla memoria puntata richiedono un unsafe contesto:

  • Riferimento indiretto puntatore (*p), accesso ai membri del puntatore (p->member) e accesso agli elementi del puntatore (p[i]).
  • Chiamata del puntatore di funzione.
  • Accesso degli elementi in un buffer a dimensione fissa.

L'esempio seguente aggiunge una matrice senza un unsafe contesto, ma dereferenzia il puntatore all'interno di uno:

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

Operazioni rilassate

Le operazioni che non accedono alla memoria puntata non richiedono più un unsafe contesto:

  • Dichiarazione di un tipo di puntatore e acquisizione dell'indirizzo di una variabile con l'operatore & .
  • Istruzione fixed che aggiunge una variabile.
  • Conversione di un'espressione stackalloc in un puntatore.
  • sizeof Operatore applicato a qualsiasi tipo non gestito.

Nell'esempio seguente vengono creati e aggiunti puntatori senza contesto 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;
    }
}

Questi relax si applicano ogni volta che si compila con la versione del preview linguaggio, indipendentemente dal fatto che un assembly acconsenta alle regole di sicurezza della memoria aggiornate.

Obblighi non sicuri per il chiamante di scarico

Un membro che chiama un'operazione non sicura del chiamante ha due opzioni: propagare l'obbligo o scaricarlo.

  • Propaga: contrassegnare il proprio membro unsafe. L'obbligo passa ai chiamanti. Usare la propagazione quando non è possibile convalidare completamente l'obbligo.
  • Scarica: lascia la firma del tuo membro al sicuro. Convalidare l'obbligo all'interno del membro, in genere con guardie di runtime, quindi eseguire l'operazione non sicura in un blocco interno unsafe . Un membro che contiene un blocco interno unsafe ma non contrassegna la propria firma unsafe è un limite non sicuro: trasforma il codice non sicuro in una superficie chiamabile sicura.

Il mockup di anteprima seguente convalida l'input con una protezione, aggiunge una matrice gestita e legge il puntatore. I chiamanti non hanno bisogno di un unsafe contesto, perché il metodo scarica l'obbligo:

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

Il controllo Null e la lunghezza della matrice regolano gli input che consentono l'esecuzione di una lettura oltre il buffer, quindi la dereferenziazione all'interno del unsafe blocco è audio. Il metodo non lascia alcun obbligo residuo, quindi espone una firma chiamabile sicura.

Documentazione sulla sicurezza

Un membro chiamante non sicuro deve documentare ciò che il chiamante deve garantire. Il modello aggiornato incoraggia due stili di commento complementari:

  • Un /// <safety> blocco di documentazione sopra la firma indica il contratto formale: le condizioni che un chiamante deve soddisfare. Un analizzatore può contrassegnare un membro non sicuro del chiamante mancante.
  • Un // SAFETY: commento all'interno di un unsafe blocco registra il motivo per cui l'operazione è sonora in quel punto, per gli sviluppatori e i revisori che leggono il corpo.

Il mockup di anteprima seguente mostra entrambi gli stili in un metodo unsafe ReadByte del chiamante:

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

Il /// <safety> blocco indica il contratto. Il contratto appartiene alla documentazione in cui ogni chiamante e revisore lo vede.

Campi non sicuri

Usare il unsafe modificatore per un campo quando il tipo dichiarato non esprime contratti da cui dipende il tipo contenitore e da cui dipende altro codice. L'unsafety esiste nel divario tra ciò che il sistema dei tipi vede e ciò che il tipo promette. Il modificatore forza ogni scrittura nel campo in un unsafe blocco, che mantiene le scritture rivedibili in un'unica posizione.

Il caso più chiaro è un campo che contiene un puntatore nativo. Il puntatore non dichiara il numero di byte a cui punta, System.Span<T> quindi il tipo contenitore mantiene tali informazioni:

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

Un readonly unsafe campo associa il contratto a una protezione predefinita: unsafe assegna un nome all'invariante e readonly impedisce una scrittura che potrebbe interromperla dopo la costruzione. Se si contrassegna una proprietà o un evento unsafe , il chiamante del campo sottostante non è sicuro. In uno struct con [StructLayout(LayoutKind.Explicit)]contrassegnare ogni campo safe o unsafe.

Parola chiave safe

Il modello aggiornato aggiunge una safe parola chiave contestuale che attesta una dichiarazione è audio in cui il compilatore richiede di rendere esplicita la scelta.

Un extern membro chiama nel codice nativo, in modo che il compilatore non possa classificarne la sicurezza. Nel modello aggiornato contrassegnare ogni extern dichiarazione, incluso un LibraryImport metodo parziale, safe o unsafe:

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

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

getpid non accetta parametri e restituisce una primitiva, quindi l'autore attesta che la chiamata è sicura e i chiamanti lo usano senza cerimonia. strlen accetta un puntatore non elaborato che il codice nativo dereferenzia, quindi la dichiarazione è unsafe e propaga l'obbligo ai chiamanti. L'omissione di entrambi i modificatori è un errore, che forza a prendere la decisione sulla sicurezza. Un campo in uno struct con layout esplicito usa la stessa regola.

Comportamento di consenso esplicito e cross-assembly

Il modello aggiornato ha due opzioni indipendenti a livello di progetto:

  • Una nuova proprietà di consenso esplicito attiva le regole aggiornate. Quando la proprietà è disattivata, si applicano le regole originali. Quando è attivo, unsafe in un membro viene propagato ai chiamanti e il compilatore registra la scelta nell'assembly con l'attributo MemorySafetyRulesAttribute .
  • La proprietà AllowUnsafeBlocks esistente cancella ogni aspetto della unsafe parola chiave, inclusi i blocchi interni nei siti di chiamata. Per impostazione predefinita, un progetto per impostazione predefinita falsenon può chiamare alcuna API non sicura.

Le due proprietà si combinano nel modo seguente:

Proprietà di consenso esplicito AllowUnsafeBlocks Result
Attivato Disattivato (impostazione predefinita) Configurazione più sicura. Il progetto usa il modello aggiornato e non consente codice non sicuro.
Attivato Attivato Il progetto usa il modello aggiornato e consente il codice non sicuro.
Disattivato Disattivato Si applica il modello originale e il progetto non può usare tipi di puntatore.
Disattivato Attivato Si applica il modello originale e il progetto può usare tipi di puntatore.

Se un assembly applica le regole aggiornate a un altro dipende da quale lato acconsente esplicitamente:

  • Chiamante del modello aggiornato, chiamato al modello aggiornato: i marcatori del unsafe chiamato passano attraverso i metadati. Il chiamante esegue il wrapping di ogni chiamata a un membro non sicuro del chiamante in un unsafe blocco.
  • Chiamante aggiornato del modello, chiamato originale: una modalità di compatibilità considera qualsiasi membro chiamato con un tipo di puntatore nella firma come chiamante-unsafe, quindi il sito di chiamata necessita di un blocco di inclusione unsafe . Questa modalità impedisce a un'API basata su puntatore di perdere automaticamente il unsafe requisito.
  • Chiamante modello originale, chiamato modello aggiornato: le regole del puntatore originali sono ancora valide. Un membro chiamante non sicuro che non ha alcun tipo di puntatore nella firma diventa chiamabile dal codice sicuro, perché il chiamante del modello originale non riesce a leggere i nuovi marcatori.

Specifica del linguaggio C#

Per altre informazioni, vedere il capitolo codice unsafe della specifica del linguaggio C# .

Per la progettazione del modello di sicurezza della memoria aggiornato, vedere la specifica della funzionalità di sicurezza della memoria.

Vedere anche