Codice non sicuro, tipi di puntatore e puntatori a funzione
La maggior parte del codice C# scritto è "codice verificabile". codice verificabile indica che gli strumenti .NET possono verificare che il codice sia sicuro. In generale, il codice sicuro non accede direttamente alla memoria usando i puntatori. Inoltre, non alloca memoria grezza. Crea invece oggetti gestiti.
C# supporta un contesto di unsafe
, in cui è possibile scrivere codice non verificabile. In un contesto di unsafe
, il codice può usare puntatori, allocare e liberare blocchi di memoria e chiamare metodi usando puntatori a funzione. Il codice non sicuro in C# non è necessariamente pericoloso; è solo codice la cui sicurezza non può essere verificata.
Il codice unsafe ha le proprietà seguenti:
- I metodi, i tipi e i blocchi di codice possono essere definiti come non sicuri.
- In alcuni casi, il codice non sicuro può aumentare le prestazioni di un'applicazione rimuovendo i controlli dei limiti di matrice.
- Il codice unsafe è necessario quando si chiamano funzioni native che richiedono puntatori.
- L'uso di codice unsafe introduce rischi per la sicurezza e la stabilità.
- Il codice che contiene blocchi non sicuri deve essere compilato con 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 del *
in un tipo puntatore viene chiamato tipo referenziale .
I tipi puntatore non ereditano da oggetto e non esistono conversioni tra tipi 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, si scrive l'asterisco (*
) insieme al solo 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 referente è un oggetto nell'heap gestito (incluse le variabili locali acquisite da espressioni lambda o delegati anonimi), l'oggetto deve essere fissato finché il puntatore viene utilizzato.
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.
L'operatore di riferimento indiretto del puntatore *
può essere usato per accedere al contenuto nella posizione a cui punta la variabile del 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
, out
o 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
. Qualsiasi tipo di puntatore può essere convertito in modo esplicito 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: {0}", number);
/* Output:
The 4 bytes of the integer: 00 04 00 00
The value of the integer: 1024
*/
}
Buffer a dimensione fissa
È possibile usare la parola chiave fixed
per creare un buffer con una matrice di dimensioni fisse in una struttura di dati. I buffer a dimensione fissa sono utili quando si scrivono metodi che interagiscono con le origini dati di altre lingue o piattaforme. Il 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
, byte
, char
, short
, int
, long
, sbyte
, ushort
, uint
, ulong
, float
o double
.
private fixed char name[30];
Nel codice sicuro, uno struct C# che contiene una matrice non contiene gli elementi della matrice. Lo struct contiene invece un riferimento agli elementi. È possibile incorporare una matrice di dimensioni fisse in uno struct quando viene usata in un blocco di codice non sicuro.
La dimensione del struct
seguente non dipende dal numero di elementi nella matrice, poiché pathName
è un riferimento:
public struct PathArray
{
public char[] pathName;
private int reserved;
}
Uno struct può contenere una matrice incorporata nel codice non sicuro. Nell'esempio seguente la matrice di fixedBuffer
ha una dimensione fissa. Si usa una dichiarazione fixed
per ottenere un puntatore al primo elemento. È possibile accedere agli elementi della matrice tramite questo puntatore. L'istruzione fixed
aggiunge il campo dell'istanza di 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 usando stackalloc abilita automaticamente 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 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:
- Può essere usato solo in un contesto di
unsafe
. - Possono essere solo campi di istanza delle strutture.
- Sono sempre vettori o matrici unidimensionali.
- La dichiarazione deve includere la lunghezza, ad esempio
fixed char id[8]
. Non è possibile usarefixed char id[]
.
Come 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 parola chiave non sicura, che consente di usare i puntatori nel metodo Copy
. L'istruzione fissa viene usata per dichiarare puntatori alle matrici di origine e di destinazione. L'istruzione fixed
i pin la posizione delle matrici di origine e di destinazione in memoria in modo che Garbage Collection non sposti le matrici. I blocchi di memoria per le matrici vengono sbloccati al termine del blocco fixed
. Poiché il metodo Copy
in questo esempio usa la parola chiave unsafe
, deve essere compilato con 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
*/
}
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 sintassi delegate*
. Il compilatore chiama la funzione usando l'istruzione calli
anziché creare un'istanza di un oggetto delegate
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 metodo UnsafeCombine
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.
- I puntatori a funzione possono essere dichiarati solo in un contesto di
unsafe
. - I metodi che accettano un
delegate*
(o restituiscono undelegate*
) possono essere chiamati solo in un contesto diunsafe
. - L'operatore
&
per ottenere l'indirizzo di una funzione è consentito solo nelle funzioni distatic
. 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*
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
, Fastcall
o 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);
Altre informazioni sui puntatori a funzione sono disponibili nella specifica della funzionalità puntatore a funzione.
Specifica del linguaggio C#
Per altre informazioni, vedere il capitolo codice unsafe della specifica del linguaggio C# .