Condividi tramite


8 tipi

8.1 Generale

I tipi del linguaggio C# sono suddivisi in due categorie principali: tipi di riferimento e tipi valore. Entrambi i tipi valore e i tipi riferimento possono essere tipi generici, che accettano uno o più parametri di tipo. I parametri di tipo possono designare sia tipi valore che tipi riferimento.

type
    : reference_type
    | value_type
    | type_parameter
    | pointer_type     // unsafe code support
    ;

pointer_type (§23.3) è disponibile solo nel codice non sicuro (§23).

I tipi valore differiscono dai tipi riferimento in quanto le variabili dei tipi valore contengono direttamente i dati, mentre le variabili dei tipi di riferimento archiviano i riferimenti ai relativi dati, quest'ultimo noto come oggetti. Con i tipi riferimento, è possibile che due variabili facciano riferimento allo stesso oggetto e quindi sia possibile che le operazioni su una variabile influiscano sull'oggetto a cui fa riferimento l'altra variabile. Con i tipi valore, le variabili hanno una propria copia dei dati e non è possibile che le operazioni su uno influiscano sull'altro.

Nota: quando una variabile è un riferimento o un parametro di output, non ha una propria risorsa di archiviazione, ma fa riferimento all'archiviazione di un'altra variabile. In questo caso, la variabile ref o out è in effetti un alias per un'altra variabile e non una variabile distinta. nota finale

Il sistema di tipi di C#è unificato in modo che un valore di qualsiasi tipo possa essere considerato come un oggetto . In C# ogni tipo deriva direttamente o indirettamente dal tipo classe object e object è la classe di base principale di tutti i tipi. I valori dei tipi riferimento vengono trattati come oggetti semplicemente visualizzando tali valori come tipi object. I valori dei tipi valore vengono trattati come oggetti eseguendo operazioni di conversione boxing e unboxing (§8.3.13).

Per praticità, in questa specifica, alcuni nomi dei tipi di libreria vengono scritti senza usare la qualifica del nome completo. Per altre informazioni, fare riferimento a §C.5 .

8.2 Tipi di riferimento

8.2.1 Generale

Un tipo riferimento è un tipo di classe, un tipo di interfaccia, un tipo di matrice, un tipo delegato o il dynamic tipo . Per ogni tipo riferimento non nullable, è presente un tipo riferimento nullable corrispondente annotato aggiungendo l'oggetto ? al nome del tipo.

reference_type
    : non_nullable_reference_type
    | nullable_reference_type
    ;

non_nullable_reference_type
    : class_type
    | interface_type
    | array_type
    | delegate_type
    | 'dynamic'
    ;

class_type
    : type_name
    | 'object'
    | 'string'
    ;

interface_type
    : type_name
    ;

array_type
    : non_array_type rank_specifier+
    ;

non_array_type
    : value_type
    | class_type
    | interface_type
    | delegate_type
    | 'dynamic'
    | type_parameter
    | pointer_type      // unsafe code support
    ;

rank_specifier
    : '[' ','* ']'
    ;

delegate_type
    : type_name
    ;

nullable_reference_type
    : non_nullable_reference_type nullable_type_annotation
    ;

nullable_type_annotation
    : '?'
    ;

pointer_type è disponibile solo nel codice unsafe (§23.3). nullable_reference_type è discusso ulteriormente in §8.9.

Un valore di tipo riferimento è un riferimento a un'istanza del tipo, quest'ultimo noto come oggetto . Il valore null speciale è compatibile con tutti i tipi di riferimento e indica l'assenza di un'istanza di .

8.2.2 Tipi di classe

Un tipo di classe definisce una struttura di dati che contiene membri dati (costanti e campi), membri di funzione (metodi, proprietà, eventi, indicizzatori, operatori, costruttori di istanza, finalizzatori e costruttori statici) e tipi annidati. I tipi di classe supportano l'ereditarietà, un meccanismo in cui le classi derivate possono estendere ed specializzare le classi di base. Le istanze dei tipi di classe vengono create utilizzando object_creation_expression(§12.8.17.2).

I tipi di classe sono descritti in §15.

Alcuni tipi di classe predefiniti hanno un significato speciale nel linguaggio C#, come descritto nella tabella seguente.

Tipo di classe Descrizione
System.Object Classe di base finale di tutti gli altri tipi. Vedere §8.2.3.
System.String Tipo stringa del linguaggio C#. Vedere §8.2.5.
System.ValueType Classe base di tutti i tipi valore. Vedere §8.3.2.
System.Enum Classe base di tutti i enum tipi. Vedere §19.5.
System.Array Classe base di tutti i tipi di matrice. Vedere §17.2.2.
System.Delegate Classe base di tutti i delegate tipi. Vedere §20.1.
System.Exception Classe base di tutti i tipi di eccezione. Vedere §21.3.

8.2.3 Tipo di oggetto

Il object tipo di classe è la classe base finale di tutti gli altri tipi. Ogni tipo in C# deriva direttamente o indirettamente dal object tipo di classe .

La parola chiave object è semplicemente un alias per la classe System.Objectpredefinita .

8.2.4 Tipo dinamico

Il dynamic tipo, ad esempio object, può fare riferimento a qualsiasi oggetto . Quando le operazioni vengono applicate alle espressioni di tipo dynamic, la risoluzione viene posticipata fino all'esecuzione del programma. Pertanto, se l'operazione non può essere applicata legittimamente all'oggetto a cui si fa riferimento, non viene fornito alcun errore durante la compilazione. Al contrario, verrà generata un'eccezione quando la risoluzione dell'operazione non riesce in fase di esecuzione.

Il dynamic tipo è descritto ulteriormente in §8.7 e l'associazione dinamica in §12.3.1.

8.2.5 Tipo stringa

Il string tipo è un tipo di classe sealed che eredita direttamente da object. Le istanze della string classe rappresentano stringhe di caratteri Unicode.

I valori del string tipo possono essere scritti come valori letterali stringa (§6.4.5.6).

La parola chiave string è semplicemente un alias per la classe System.Stringpredefinita .

8.2.6 Tipi di interfaccia

Un'interfaccia definisce un contratto. Una classe o uno struct che implementa un'interfaccia deve rispettare il contratto. Un'interfaccia può ereditare da più interfacce di base e una classe o uno struct può implementare più interfacce.

I tipi di interfaccia sono descritti in §18.

8.2.7 Tipi di matrice

Una matrice è una struttura di dati che contiene zero o più variabili a cui si accede tramite indici calcolati. Le variabili contenute in una matrice, chiamate anche elementi della matrice, sono tutte dello stesso tipo, definito tipo di elemento della matrice.

I tipi di matrice sono descritti in §17.

8.2.8 Tipi delegati

Un delegato è una struttura di dati che fa riferimento a uno o più metodi. Ad esempio, i metodi fanno riferimento anche alle istanze dell'oggetto corrispondenti.

Nota: l'equivalente più vicino di un delegato in C o C++ è un puntatore a funzione, ma mentre un puntatore a funzione può fare riferimento solo a funzioni statiche, un delegato può fare riferimento a metodi statici e di istanza. Nel secondo caso, il delegato archivia non solo un riferimento al punto di ingresso del metodo, ma anche un riferimento all'istanza dell'oggetto su cui richiamare il metodo. nota finale

I tipi delegati sono descritti in §20.

8.3 Tipi valore

8.3.1 Generale

Un tipo valore è un tipo di struct o un tipo di enumerazione. C# fornisce un set di tipi di struct predefiniti denominati tipi semplici. I tipi semplici vengono identificati tramite parole chiave.

value_type
    : non_nullable_value_type
    | nullable_value_type
    ;

non_nullable_value_type
    : struct_type
    | enum_type
    ;

struct_type
    : type_name
    | simple_type
    | tuple_type
    ;

simple_type
    : numeric_type
    | 'bool'
    ;

numeric_type
    : integral_type
    | floating_point_type
    | 'decimal'
    ;

integral_type
    : 'sbyte'
    | 'byte'
    | 'short'
    | 'ushort'
    | 'int'
    | 'uint'
    | 'long'
    | 'ulong'
    | 'char'
    ;

floating_point_type
    : 'float'
    | 'double'
    ;

tuple_type
    : '(' tuple_type_element (',' tuple_type_element)+ ')'
    ;
    
tuple_type_element
    : type identifier?
    ;
    
enum_type
    : type_name
    ;

nullable_value_type
    : non_nullable_value_type nullable_type_annotation
    ;

A differenza di una variabile di un tipo riferimento, una variabile di un tipo valore può contenere il valore null solo se il tipo di valore è un tipo di valore nullable (§8.3.12). Per ogni tipo di valore non nullable è presente un tipo di valore nullable corrispondente che indica lo stesso set di valori più il valore null.

L'assegnazione a una variabile di un tipo valore crea una copia del valore assegnato. Ciò differisce dall'assegnazione a una variabile di un tipo riferimento, che copia il riferimento ma non l'oggetto identificato dal riferimento.

8.3.2 Tipo System.ValueType

Tutti i tipi valore ereditano in modo implicito da classSystem.ValueType, che a sua volta eredita dalla classe object. Non è possibile che alcun tipo derivi da un tipo valore e i tipi valore siano quindi bloccati in modo implicito (§15.2.2.3).

Si noti che System.ValueType non è un value_type. Invece, si tratta di un class_type da cui tutti i value_typevengono derivati automaticamente.

8.3.3 Costruttori predefiniti

Tutti i tipi valore dichiarano in modo implicito un costruttore di istanza senza parametri pubblico denominato costruttore predefinito. Il costruttore predefinito restituisce un'istanza inizializzata zero nota come valore predefinito per il tipo di valore:

  • Per tutti i simple_types, il valore predefinito è il valore prodotto da un criterio di bit di tutti gli zeri:
    • Per sbyte, byte, shortushort, int, uint, , longe ulong, il valore predefinito è 0.
    • Per char, il valore predefinito è '\x0000'.
    • Per float, il valore predefinito è 0.0f.
    • Per double, il valore predefinito è 0.0d.
    • Per decimal, il valore predefinito è 0m (ovvero, valore zero con scala 0).
    • Per bool, il valore predefinito è false.
    • Per un enum_typeE, il valore predefinito è 0, convertito nel tipo E.
  • Per un struct_type, il valore predefinito è il valore prodotto impostando tutti i campi tipo valore sul valore predefinito e tutti i campi del tipo di riferimento su null.
  • Per un nullable_value_type il valore predefinito è un'istanza per la quale la HasValue proprietà è false. Il valore predefinito è noto anche come valore Null del tipo valore nullable. Se si tenta di leggere la Value proprietà di tale valore, viene generata un'eccezione di tipo System.InvalidOperationException (§8.3.12).

Come qualsiasi altro costruttore di istanza, il costruttore predefinito di un tipo valore viene richiamato usando l'operatore new .

Nota: per motivi di efficienza, questo requisito non deve effettivamente avere l'implementazione di generare una chiamata al costruttore. Per i tipi valore, l'espressione valore predefinita (§12.8.21) produce lo stesso risultato dell'uso del costruttore predefinito. nota finale

Esempio: nel codice seguente, le variabili ij e k vengono tutte inizializzate su zero.

class A
{
    void F()
    {
        int i = 0;
        int j = new int();
        int k = default(int);
    }
}

esempio finale

Poiché ogni tipo di valore ha in modo implicito un costruttore di istanza senza parametri pubblico, non è possibile che un tipo di struct contenga una dichiarazione esplicita di un costruttore senza parametri. Un tipo struct è tuttavia autorizzato a dichiarare costruttori di istanza con parametri (§16.4.9).

Tipi di struct 8.3.4

Un tipo di struct è un tipo valore che può dichiarare costanti, campi, metodi, proprietà, eventi, indicizzatori, operatori, costruttori di istanza, costruttori statici e tipi annidati. La dichiarazione dei tipi di struct è descritta in §16.

8.3.5 Tipi semplici

C# fornisce un set di tipi predefiniti denominati struct tipi semplici. I tipi semplici vengono identificati tramite parole chiave, ma queste parole chiave sono semplicemente alias per i tipi predefiniti struct nello spazio dei System nomi, come descritto nella tabella seguente.

Parola chiave Tipo con alias
sbyte System.SByte
byte System.Byte
short System.Int16
ushort System.UInt16
int System.Int32
uint System.UInt32
long System.Int64
ulong System.UInt64
char System.Char
float System.Single
double System.Double
bool System.Boolean
decimal System.Decimal

Poiché un tipo semplice esegue l'alias di un tipo struct, ogni tipo semplice ha membri.

Esempio: int i membri dichiarati in System.Int32 e i membri ereditati da System.Objecte sono consentite le istruzioni seguenti:

int i = int.MaxValue;      // System.Int32.MaxValue constant
string s = i.ToString();   // System.Int32.ToString() instance method
string t = 123.ToString(); // System.Int32.ToString() instance method

esempio finale

Nota: i tipi semplici differiscono da altri tipi di struct in quanto consentono determinate operazioni aggiuntive:

  • La maggior parte dei tipi semplici consente la creazione di valori letterali (§6.4.5), anche se C# non esegue il provisioning per valori letterali di tipi struct in generale. Esempio: 123 è un valore letterale di tipo int ed 'a' è un valore letterale di tipo char. esempio finale
  • Quando gli operandi di un'espressione sono tutte costanti di tipo semplice, è possibile che un compilatore valuti l'espressione in fase di compilazione. Tale espressione è nota come constant_expression (§12.23). Le espressioni che coinvolgono operatori definiti da altri tipi di struct non sono considerate espressioni costanti
  • Tramite const dichiarazioni, è possibile dichiarare costanti dei tipi semplici (§15.4). Non è possibile avere costanti di altri tipi di struct, ma un effetto simile viene fornito da campi statici di sola lettura.
  • Le conversioni che coinvolgono tipi semplici possono partecipare alla valutazione degli operatori di conversione definiti da altri tipi di struct, ma un operatore di conversione definito dall'utente non può mai partecipare alla valutazione di un altro operatore di conversione definito dall'utente (§10.5.3).

nota finale.

8.3.6 Tipi integrali

C# supporta nove tipi integrali: sbyte, byte, shortushort, int, uint, long, ulong, e char. I tipi integrali hanno le dimensioni e gli intervalli di valori seguenti:

  • Il sbyte tipo rappresenta interi con segno a 8 bit con valori compresi tra -128 e 127, inclusi.
  • Il byte tipo rappresenta interi senza segno a 8 bit con valori compresi tra 0 e 255, inclusi.
  • Il short tipo rappresenta interi con segno a 16 bit con valori compresi tra -32768 e 32767, inclusi.
  • Il ushort tipo rappresenta interi senza segno a 16 bit con valori compresi tra 0 e 65535, inclusi.
  • Il int tipo rappresenta interi con segno a 32 bit con valori compresi tra -2147483648 e 2147483647, inclusi.
  • Il uint tipo rappresenta interi senza segno a 32 bit con valori compresi tra 0 e 4294967295, inclusi.
  • Il long tipo rappresenta interi con segno a 64 bit con valori compresi tra -9223372036854775808 e 9223372036854775807, inclusi.
  • Il ulong tipo rappresenta interi senza segno a 64 bit con valori compresi tra 0 e 18446744073709551615, inclusi.
  • Il char tipo rappresenta interi senza segno a 16 bit con valori compresi tra 0 e 65535, inclusi. Il set di valori possibili per il char tipo corrisponde al set di caratteri Unicode.

    Nota: sebbene char abbia la stessa rappresentazione di ushort, non tutte le operazioni consentite in un tipo sono consentite nell'altra. nota finale

Tutti i tipi integrali con segno sono rappresentati usando il formato di complemento di due.

Gli operatori unari e binari integral_type operano sempre con precisione a 32 bit con segno, precisione senza segno a 32 bit, precisione a 64 bit con segno o precisione senza segno a 64 bit, come descritto in §12.4.7.

Il char tipo è classificato come tipo integrale, ma differisce dagli altri tipi integrali in due modi:

  • Non esistono conversioni implicite predefinite da altri tipi al char tipo . In particolare, anche se i byte tipi e ushort hanno intervalli di valori completamente rappresentabili usando il char tipo, le conversioni implicite da sbyte, byte o ushort per char non esistere.
  • Le costanti del char tipo devono essere scritte come character_literalo come integer_literals in combinazione con un cast al tipo char.

Esempio: (char)10 è uguale '\x000A'a . esempio finale

Gli checked operatori e le istruzioni e unchecked vengono usati per controllare il controllo dell'overflow per le operazioni aritmetiche e le conversioni di tipo integrale (§12.8.20). In un checked contesto, un overflow genera un errore in fase di compilazione o genera un'eccezione System.OverflowException . In un unchecked contesto, gli overflow vengono ignorati e tutti i bit di ordine elevato che non rientrano nel tipo di destinazione vengono eliminati.

8.3.7 Tipi a virgola mobile

C# supporta due tipi a virgola mobile: float e double. I float tipi e double vengono rappresentati usando i formati IEC a precisione doppia a 32 bit e a precisione doppia a 64 bit IEC 60559, che forniscono i set di valori seguenti:

  • Zero positivo e zero negativo. Nella maggior parte dei casi, zero positivo e zero negativo si comportano in modo identico al valore zero semplice, ma alcune operazioni distinguono tra i due (§12.10.3).
  • Infinito positivo e infinito negativo. Gli infiniti sono prodotti da operazioni quali la divisione di un numero diverso da zero per zero.

    Esempio: 1.0 / 0.0 restituisce infinito positivo e –1.0 / 0.0 restituisce infinito negativo. esempio finale

  • Il valore Not-a-Number , spesso abbreviato NaN. I valori NaN sono prodotti da operazioni a virgola mobile non valide, come la divisione di zero per zero.
  • Set finito di valori diversi da zero della maschera × m × 2e dove s è 1 o −1 e m esono determinati dal particolare tipo a virgola mobile: per , 0 < 2²⁴ e −149 ≤ e ≤ 104 e per <, 0 double< 2⁵² e −1075 ≤ e ≤ 970.< I numeri a virgola mobile denormalizzati sono considerati valori non zero validi. C# non richiede né impedisce che un'implementazione conforme supporti numeri a virgola mobile denormalizzati.

Il float tipo può rappresentare valori compresi tra circa 1,5 × 10⁻⁴⁵ a 3,4 × 10⁸ con precisione di 7 cifre.

Il double tipo può rappresentare valori compresi tra circa 5,0 × 10⁻²⁴ a 1,7 × 10⁸ con precisione di 15-16 cifre.

Se uno degli operandi di un operatore binario è un tipo a virgola mobile, vengono applicate promozioni numeriche standard, come descritto in dettaglio in §12.4.7 e l'operazione viene eseguita con float precisione o double .

Gli operatori a virgola mobile, inclusi gli operatori di assegnazione, non producono mai eccezioni. In situazioni eccezionali, invece, le operazioni a virgola mobile producono zero, infinito o NaN, come descritto di seguito:

  • Il risultato di un'operazione a virgola mobile viene arrotondato al valore rappresentabile più vicino nel formato di destinazione.
  • Se la grandezza del risultato di un'operazione a virgola mobile è troppo piccola per il formato di destinazione, il risultato dell'operazione diventa zero positivo o zero negativo.
  • Se la grandezza del risultato di un'operazione a virgola mobile è troppo grande per il formato di destinazione, il risultato dell'operazione diventa infinito positivo o infinito negativo.
  • Se un'operazione a virgola mobile non è valida, il risultato dell'operazione diventa NaN.
  • Se uno o entrambi gli operandi di un'operazione a virgola mobile è NaN, il risultato dell'operazione diventa NaN.

Le operazioni a virgola mobile possono essere eseguite con maggiore precisione rispetto al tipo di risultato dell'operazione. Per forzare la precisione esatta di un tipo a virgola mobile, è possibile utilizzare un cast esplicito (§12.9.7).

Esempio: alcune architetture hardware supportano un tipo a virgola mobile "extended" o "long double" con un intervallo e una precisione maggiori rispetto al double tipo ed eseguono in modo implicito tutte le operazioni a virgola mobile usando questo tipo di precisione superiore. Solo a un costo eccessivo di prestazioni possono essere effettuate architetture hardware per eseguire operazioni a virgola mobile con minore precisione e invece di richiedere un'implementazione per l'esecuzione di prestazioni e precisione, C# consente l'uso di un tipo di precisione superiore per tutte le operazioni a virgola mobile. Oltre a fornire risultati più precisi, questo raramente ha effetti misurabili. Tuttavia, nelle espressioni del formato x * y / z, in cui la moltiplicazione produce un risultato esterno all'intervallo double , ma la divisione successiva riporta il risultato temporaneo nell'intervallo double , il fatto che l'espressione viene valutata in un formato di intervallo superiore può causare la produzione di un risultato finito anziché un infinito. esempio finale

8.3.8 Tipo decimale

Il tipo decimal è un tipo dati a 128 bit adatto per i calcoli finanziari e monetari. Il decimal tipo può rappresentare valori inclusi quelli inclusi nell'intervallo almeno -7,9 × 10⁻²⁸ a 7,9 × 10²⁸, con precisione di almeno 28 cifre.

Il set finito di valori di tipo decimal è del formato (–1)v × c × 10⁻e, dove il segno v è 0 o 1, il coefficiente c è dato da 0 ≤ < e la scala e è tale che Emine ≤ Emax, dove Cmax è almeno 1 × 10²⁸, Emin ≤ 0, e Emax ≥ 28. Il decimal tipo non supporta necessariamente zeri firmati, infiniti o NaN.

Un decimal oggetto è rappresentato come un numero intero ridimensionato da una potenza di dieci. Per decimals con un valore assoluto minore di 1.0m, il valore è esatto per almeno il 28° separatore decimale. Per decimals con un valore assoluto maggiore o uguale a 1.0m, il valore è esatto per almeno 28 cifre. Contrariamente ai float tipi di dati e double , i numeri frazionari decimali, ad 0.1 esempio, possono essere rappresentati esattamente nella rappresentazione decimale. float Nelle rappresentazioni e double tali numeri hanno spesso espansioni binarie non irreversibili, rendendo tali rappresentazioni più soggette a errori di arrotondamento.

Se uno degli operandi di un operatore binario è di decimal tipo , vengono applicate promozioni numeriche standard, come descritto in dettaglio in §12.4.7 e l'operazione viene eseguita con double precisione.

Il risultato di un'operazione sui valori di tipo decimal è che il risultato sarebbe il calcolo di un risultato esatto (mantenendo la scala, come definito per ogni operatore) e quindi l'arrotondamento per adattarsi alla rappresentazione. I risultati vengono arrotondati al valore rappresentabile più vicino e, quando un risultato è ugualmente vicino a due valori rappresentabili, al valore con un numero pari nella posizione meno significativa della cifra (noto come "arrotondamento del banchiere"). Ovvero, i risultati sono esatti per almeno il 28° separatore decimale. Si noti che l'arrotondamento può produrre un valore zero da un valore diverso da zero.

Se un'operazione decimal aritmetica produce un risultato la cui grandezza è troppo grande per il decimal formato, viene generata un'eccezione System.OverflowException .

Il decimal tipo ha una maggiore precisione, ma può avere un intervallo inferiore rispetto ai tipi a virgola mobile. Di conseguenza, le conversioni dai tipi a virgola mobile a decimal potrebbero produrre eccezioni di overflow e conversioni da decimal a tipi a virgola mobile potrebbero causare la perdita di eccezioni di precisione o overflow. Per questi motivi, non esistono conversioni implicite tra i tipi a virgola mobile e decimale senza cast espliciti, si verifica un errore in fase di compilazione quando gli operandi e decimal a virgola mobile vengono misti direttamente nella stessa espressione.

8.3.9 Tipo Bool

Il bool tipo rappresenta le quantità logiche booleane. I valori possibili di tipo bool sono true e false. La rappresentazione di false è descritta in §8.3.3. Anche se la rappresentazione di true non è specificata, è diversa da quella di false.

Non esistono conversioni standard tra bool e altri tipi di valore. In particolare, il bool tipo è distinto e separato dai tipi integrali, non è possibile usare un bool valore al posto di un valore integrale e viceversa.

Nota: nei linguaggi C e C++, un valore integrale zero o a virgola mobile o un puntatore Null può essere convertito nel valore falsebooleano e un valore integrale o a virgola mobile diverso da zero oppure un puntatore non Null può essere convertito nel valore truebooleano . In C# tali conversioni vengono eseguite confrontando in modo esplicito un valore integrale o a virgola mobile con zero oppure confrontando in modo esplicito un riferimento a un oggetto con null. nota finale

8.3.10 Tipi di enumerazione

Un tipo di enumerazione è un tipo distinto con costanti denominate. Ogni tipo di enumerazione ha un tipo sottostante, che deve essere byte, , sbyteshort, ushortint, uint, long o ulong. Il set di valori del tipo di enumerazione corrisponde al set di valori del tipo sottostante. I valori del tipo di enumerazione non sono limitati ai valori delle costanti denominate. I tipi di enumerazione vengono definiti tramite dichiarazioni di enumerazione (§19.2).

Tipi di tupla 8.3.11

Un tipo di tupla rappresenta una sequenza ordinata e a lunghezza fissa di valori con nomi facoltativi e singoli tipi. Il numero di elementi in un tipo di tupla viene definito arità. Un tipo di tupla viene scritto (T1 I1, ..., Tn In) con n ≥ 2, dove gli identificatori sono nomi di elementi di tupla facoltativi I1...In.

Questa sintassi è abbreviata per un tipo costruito con i tipi T1...Tn di System.ValueTuple<...>, che deve essere un set di tipi struct generici in grado di esprimere direttamente tipi di tupla di qualsiasi arità tra due e sette inclusi. Non è necessario che esista una System.ValueTuple<...> dichiarazione che corrisponda direttamente all'arità di qualsiasi tipo di tupla con un numero corrispondente di parametri di tipo. Le tuple con un livello di arità maggiore di sette sono invece rappresentate con un tipo System.ValueTuple<T1, ..., T7, TRest> di struct generico che oltre agli elementi della tupla ha un campo contenente un Rest valore annidato degli elementi rimanenti, usando un altro System.ValueTuple<...> tipo. Tale annidamento può essere osservabile in vari modi, ad esempio tramite la presenza di un Rest campo. Se è necessario un solo campo aggiuntivo, viene usato il tipo System.ValueTuple<T1> di struct generico. Questo tipo non è considerato un tipo di tupla in se stesso. Se sono necessari più di sette campi aggiuntivi, System.ValueTuple<T1, ..., T7, TRest> viene usato in modo ricorsivo.

I nomi degli elementi all'interno di un tipo di tupla devono essere distinti. Un nome di elemento tupla del formato ItemX, dove X è una sequenza di0 cifre decimali non avviate che potrebbero rappresentare la posizione di un elemento di tupla, è consentita solo nella posizione indicata da X.

I nomi degli elementi facoltativi non sono rappresentati nei ValueTuple<...> tipi e non vengono archiviati nella rappresentazione in fase di esecuzione di un valore di tupla. Le conversioni di identità (§10.2.2) esistono tra tuple con sequenze di elementi convertibili tramite identità.

L'operatore new§12.8.17.2 non può essere applicato con la sintassi new (T1, ..., Tn)del tipo di tupla . I valori delle tuple possono essere creati da espressioni di tupla (§12.8.6) o applicando l'operatore new direttamente a un tipo costruito da ValueTuple<...>.

Gli elementi della tupla sono campi pubblici con i nomi Item1, e Item2così via e possono essere accessibili tramite l'accesso a un membro su un valore di tupla (§12.8.7. Inoltre, se il tipo di tupla ha un nome per un determinato elemento, tale nome può essere usato per accedere all'elemento in questione.

Nota: anche quando le tuple di grandi dimensioni sono rappresentate con valori annidati System.ValueTuple<...> , è comunque possibile accedere direttamente a ogni elemento tupla con il Item... nome corrispondente alla relativa posizione. nota finale

Esempio: in base agli esempi seguenti:

(int, string) pair1 = (1, "One");
(int, string word) pair2 = (2, "Two");
(int number, string word) pair3 = (3, "Three");
(int Item1, string Item2) pair4 = (4, "Four");
// Error: "Item" names do not match their position
(int Item2, string Item123) pair5 = (5, "Five");
(int, string) pair6 = new ValueTuple<int, string>(6, "Six");
ValueTuple<int, string> pair7 = (7, "Seven");
Console.WriteLine($"{pair2.Item1}, {pair2.Item2}, {pair2.word}");

I tipi di tupla per pair1, pair2e pair3 sono tutti validi, con nomi per no, alcuni o tutti gli elementi del tipo di tupla.

Il tipo di tupla per pair4 è valido perché i nomi Item1 e Item2 le relative posizioni corrispondono, mentre il tipo di tupla per pair5 non è consentito, perché i nomi Item2 e Item123 non lo sono.

Le dichiarazioni per pair6 e pair7 dimostrano che i tipi di tupla sono intercambiabili con i tipi costruiti del formato ValueTuple<...>e che l'operatore new è consentito con la sintassi di quest'ultima.

L'ultima riga mostra che è possibile accedere agli elementi della tupla dal Item nome corrispondente alla relativa posizione, nonché dal nome dell'elemento tupla corrispondente, se presente nel tipo. esempio finale

8.3.12 Tipi valore nullable

Un tipo valore nullable può rappresentare tutti i valori del tipo sottostante più un valore Null aggiuntivo. Viene scritto T?un tipo valore nullable , dove T è il tipo sottostante. Questa sintassi è abbreviata per System.Nullable<T>e le due forme possono essere usate in modo intercambiabile.

Viceversa, un tipo valore non nullable è qualsiasi tipo di valore diverso da System.Nullable<T> e la relativa sintassi abbreviata T? (per qualsiasi T), più qualsiasi parametro di tipo vincolato a essere un tipo valore non nullable (ovvero qualsiasi parametro di tipo con un vincolo di tipo valore (§15.2.5)). Il System.Nullable<T> tipo specifica il vincolo di tipo valore per T, il che significa che il tipo sottostante di un tipo valore nullable può essere qualsiasi tipo di valore non nullable. Il tipo sottostante di un tipo valore nullable non può essere un tipo valore nullable o un tipo riferimento. Ad esempio, int?? è un tipo non valido. I tipi riferimento nullable sono trattati in §8.9.

Un'istanza di un tipo valore T? nullable ha due proprietà pubbliche di sola lettura:

  • Proprietà HasValue di tipo bool
  • Proprietà Value di tipo T

Un'istanza per la quale HasValue viene true detto non null. Un'istanza non Null contiene un valore noto e Value restituisce tale valore.

Un'istanza per la quale HasValue viene false detto null. Un'istanza Null ha un valore non definito. Se si tenta di leggere l'oggetto di un'istanza Value Null, viene generata un'eccezione System.InvalidOperationException . Il processo di accesso alla proprietà Value di un'istanza nullable viene definito annullamento del wrapping.

Oltre al costruttore predefinito, ogni tipo di T? valore nullable ha un costruttore pubblico con un singolo parametro di tipo T. Dato un valore x di tipo T, una chiamata al costruttore del form

new T?(x)

crea un'istanza non Null di T? per cui la Value proprietà è x. Il processo di creazione di un'istanza non Null di un tipo valore nullable per un determinato valore viene definito wrapping.

Le conversioni implicite sono disponibili dal null valore letterale a T? (§10.2.7) e da T a T? (§10.2.6).

Il tipo T? valore nullable non implementa interfacce (§18). In particolare, ciò significa che non implementa alcuna interfaccia eseguita dal tipo T sottostante.

8.3.13 Boxing e unboxing

Il concetto di boxing e unboxing fornisce un ponte tra value_types e reference_types consentendo a qualsiasi valore di un value_type di essere convertito in e dal tipo object. La conversione boxing e unboxing consente una visualizzazione unificata del sistema di tipi in cui un valore di qualsiasi tipo può essere considerato come .object

Il boxing è descritto in modo più dettagliato in §10.2.9 e unboxing è descritto in §10.3.7.

8.4 Tipi costruiti

8.4.1 Generale

Una dichiarazione di tipo generico, da sola, indica un tipo generico non associato usato come "progetto" per formare molti tipi diversi, tramite l'applicazione di argomenti di tipo. Gli argomenti di tipo vengono scritti tra parentesi angolari (< e >) immediatamente dopo il nome del tipo generico. Un tipo che include almeno un argomento di tipo è denominato tipo costruito. Un tipo costruito può essere usato nella maggior parte delle posizioni nella lingua in cui può essere visualizzato un nome di tipo. Un tipo generico non associato può essere usato solo all'interno di un typeof_expression (§12.8.18).

I tipi costruiti possono essere usati anche nelle espressioni come nomi semplici (§12.8.4) o quando accedono a un membro (§12.8.7).

Quando viene valutata una namespace_or_type_name , vengono considerati solo i tipi generici con il numero corretto di parametri di tipo. Pertanto, è possibile usare lo stesso identificatore per identificare tipi diversi, purché i tipi abbiano numeri diversi di parametri di tipo. Ciò è utile quando si combinano classi generiche e non generiche nello stesso programma.

Esempio:

namespace Widgets
{
    class Queue {...}
    class Queue<TElement> {...}
}

namespace MyApplication
{
    using Widgets;

    class X
    {
        Queue q1;      // Non-generic Widgets.Queue
        Queue<int> q2; // Generic Widgets.Queue
    }
}

esempio finale

Le regole dettagliate per la ricerca dei nomi nelle produzioni namespace_or_type_name sono descritte in §7.8. La risoluzione delle ambiguità in queste produzioni è descritta in §6.2.5. Un type_name potrebbe identificare un tipo costruito anche se non specifica direttamente i parametri di tipo. Ciò può verificarsi quando un tipo è annidato all'interno di una dichiarazione generica class e il tipo di istanza della dichiarazione contenitore viene usato in modo implicito per la ricerca del nome (§15.3.9.7).

Esempio:

class Outer<T>
{
    public class Inner {...}

    public Inner i; // Type of i is Outer<T>.Inner
}

esempio finale

Un tipo non enum costruito non deve essere utilizzato come unmanaged_type (§8.8).

8.4.2 Argomenti di tipo

Ogni argomento in un elenco di argomenti di tipo è semplicemente un tipo.

type_argument_list
    : '<' type_arguments '>'
    ;

type_arguments
    : type_argument (',' type_argument)*
    ;   

type_argument
    : type
    | type_parameter nullable_type_annotation?
    ;

Ogni argomento di tipo deve soddisfare tutti i vincoli sul parametro di tipo corrispondente (§15.2.5). Argomento di tipo riferimento il cui valore Nullbility non corrisponde al valore Nullbility del parametro di tipo soddisfa il vincolo; tuttavia può essere generato un avviso.

8.4.3 Tipi aperti e chiusi

Tutti i tipi possono essere classificati come tipi aperti o tipi chiusi. Un tipo aperto è un tipo che include parametri di tipo. In particolare:

  • Un parametro di tipo definisce un tipo aperto.
  • Un tipo di matrice è un tipo aperto se e solo se il tipo di elemento è un tipo aperto.
  • Un tipo costruito è un tipo aperto se e solo se uno o più dei relativi argomenti di tipo è un tipo aperto. Un tipo annidato costruito è un tipo aperto se e solo se uno o più dei relativi argomenti di tipo o gli argomenti di tipo del relativo tipo contenitore sono un tipo aperto.

Un tipo chiuso è un tipo che non è un tipo aperto.

In fase di esecuzione, tutto il codice all'interno di una dichiarazione di tipo generico viene eseguito nel contesto di un tipo costruito chiuso creato applicando argomenti di tipo alla dichiarazione generica. Ogni parametro di tipo all'interno del tipo generico è associato a un particolare tipo di runtime. L'elaborazione in fase di esecuzione di tutte le istruzioni ed espressioni si verifica sempre con tipi chiusi e i tipi aperti si verificano solo durante l'elaborazione in fase di compilazione.

Due tipi costruiti chiusi sono identity convertibile (§10.2.2) se vengono costruiti dallo stesso tipo generico non associato e esiste una conversione di identità tra ognuno dei rispettivi argomenti di tipo. Gli argomenti di tipo corrispondenti possono essere chiusi tipi o tuple costruiti che sono convertibili in identità. I tipi costruiti chiusi che sono identity convertibile condividono un singolo set di variabili statiche. In caso contrario, ogni tipo costruito chiuso ha un proprio set di variabili statiche. Poiché un tipo aperto non esiste in fase di esecuzione, non esistono variabili statiche associate a un tipo aperto.

8.4.4 Tipi associati e non associati

Il termine tipo non associato fa riferimento a un tipo non generico o a un tipo generico non associato. Il termine tipo associato fa riferimento a un tipo non generico o a un tipo costruito.

Un tipo non associato fa riferimento all'entità dichiarata da una dichiarazione di tipo. Un tipo generico non associato non è un tipo e non può essere usato come tipo di variabile, argomento o valore restituito o come tipo di base. L'unico costrutto in cui è possibile fare riferimento a un tipo generico non associato è l'espressione typeof (§12.8.18).

8.4.5 Vincoli soddisfacenti

Ogni volta che viene fatto riferimento a un tipo costruito o a un metodo generico, gli argomenti di tipo forniti vengono controllati in base ai vincoli dei parametri di tipo dichiarati nel tipo o nel metodo generico (§15.2.5). Per ogni where clausola, l'argomento A di tipo che corrisponde al parametro di tipo denominato viene controllato in base a ogni vincolo come indicato di seguito:

  • Se il vincolo è un class tipo, un tipo di interfaccia o un parametro di tipo, consentire di C rappresentare tale vincolo con gli argomenti di tipo forniti sostituiti per tutti i parametri di tipo visualizzati nel vincolo. Per soddisfare il vincolo, è necessario che il tipo A sia convertibile in tipo C da uno dei seguenti:
    • Conversione di identità (§10.2.2)
    • Conversione implicita dei riferimenti (§10.2.8)
    • Conversione boxing (§10.2.9), a condizione che il tipo sia un tipo A di valore non nullable.
    • Riferimento implicito, conversione boxing o parametro di tipo da un parametro A di tipo a C.
  • Se il vincolo è il vincolo di tipo riferimento (class), il tipo A deve soddisfare uno dei seguenti elementi:
    • A è un tipo di interfaccia, un tipo di classe, un tipo delegato, un tipo di matrice o il tipo dinamico.

    Nota: System.ValueType e System.Enum sono tipi di riferimento che soddisfano questo vincolo. nota finale

    • A è un parametro di tipo noto come tipo riferimento (§8.2).
  • Se il vincolo è il vincolo di tipo valore (struct), il tipo A deve soddisfare uno dei seguenti:
    • A è un struct tipo o enum un tipo, ma non un tipo di valore nullable.

    Nota: System.ValueType e System.Enum sono tipi di riferimento che non soddisfano questo vincolo. nota finale

    • A è un parametro di tipo con il vincolo di tipo valore (§15.2.5).
  • Se il vincolo è il vincolo new()del costruttore , il tipo A non deve essere abstract e deve avere un costruttore pubblico senza parametri. Questa condizione viene soddisfatta se una delle condizioni seguenti è vera:
    • A è un tipo valore, poiché tutti i tipi valore hanno un costruttore predefinito pubblico (§8.3.3).
    • A è un parametro di tipo con il vincolo del costruttore (§15.2.5).
    • A è un parametro di tipo con il vincolo di tipo valore (§15.2.5).
    • A è un oggetto class che non è astratto e contiene un costruttore pubblico dichiarato in modo esplicito senza parametri.
    • A non abstract è e ha un costruttore predefinito (§15.11.5).

Si verifica un errore in fase di compilazione se uno o più vincoli di un parametro di tipo non sono soddisfatti dagli argomenti di tipo specificati.

Poiché i parametri di tipo non vengono ereditati, i vincoli non vengono mai ereditati.

B<T> Al contrario, classE non è necessario specificare un vincolo, perché List<T> implementa IEnumerable per qualsiasi Toggetto .

class B<T> where T: IEnumerable {...}
class D<T> : B<T> where T: IEnumerable {...}
class E<T> : B<List<T>> {...}

esempio finale

8.5 Parametri di tipo

Un parametro di tipo è un identificatore che designa un tipo di valore o un tipo riferimento a cui è associato il parametro in fase di esecuzione.

type_parameter
    : identifier
    ;

Poiché è possibile creare un'istanza di un parametro di tipo con molti argomenti di tipo diversi, i parametri di tipo hanno operazioni e restrizioni leggermente diverse rispetto ad altri tipi.

Nota: questi includono:

  • Un parametro di tipo non può essere utilizzato direttamente per dichiarare una classe base (§15.2.4.2) o un'interfaccia (§18.2.4).
  • Le regole per la ricerca dei membri sui parametri di tipo dipendono dai vincoli, se presenti, applicati al parametro di tipo. Sono descritti in dettaglio in §12.5.
  • Le conversioni disponibili per un parametro di tipo dipendono dai vincoli, se presenti, applicati al parametro di tipo. Sono descritti in dettaglio in §10.2.12 e §10.3.8.
  • Il valore letterale null non può essere convertito in un tipo specificato da un parametro di tipo, tranne se il parametro di tipo è noto come tipo riferimento (§10.2.12). Tuttavia, è possibile usare un'espressione predefinita (§12.8.21). Inoltre, un valore con un tipo specificato da un parametro di tipo può essere confrontato con null utilizzando == e != (§12.12.7) a meno che il parametro di tipo non abbia il vincolo di tipo valore.
  • Un'espressione new (§12.8.17.2) può essere usata solo con un parametro di tipo se il parametro di tipo è vincolato da un constructor_constraint o dal vincolo di tipo valore (§15.2.5).
  • Non è possibile usare un parametro di tipo in qualsiasi punto all'interno di un attributo.
  • Non è possibile utilizzare un parametro di tipo in un accesso membro (§12.8.7) o un nome di tipo (§7.8) per identificare un membro statico o un tipo annidato.
  • Un parametro di tipo non può essere utilizzato come unmanaged_type (§8.8).

nota finale

Come tipo, i parametri di tipo sono puramente un costrutto in fase di compilazione. In fase di esecuzione, ogni parametro di tipo è associato a un tipo di runtime specificato fornendo un argomento di tipo alla dichiarazione di tipo generico. Di conseguenza, il tipo di una variabile dichiarata con un parametro di tipo sarà, in fase di esecuzione, un tipo costruito chiuso §8.4.3. L'esecuzione in fase di esecuzione di tutte le istruzioni ed espressioni che coinvolgono parametri di tipo usa il tipo fornito come argomento di tipo per tale parametro.

8.6 Tipi di albero delle espressioni

Gli alberi delle espressioni consentono di rappresentare le espressioni lambda come strutture di dati anziché codice eseguibile. Gli alberi delle espressioni sono valori dei tipi di albero delle espressioni del formato System.Linq.Expressions.Expression<TDelegate>, dove TDelegate è qualsiasi tipo delegato. Per la parte restante di questa specifica, questi tipi verranno indicati usando la sintassi abbreviata Expression<TDelegate>.

Se esiste una conversione da un'espressione lambda a un tipo Ddelegato , esiste anche una conversione nel tipo di Expression<TDelegate>albero delle espressioni . Mentre la conversione di un'espressione lambda in un tipo delegato genera un delegato che fa riferimento a codice eseguibile per l'espressione lambda, la conversione in un tipo di albero delle espressioni crea una rappresentazione dell'albero delle espressioni dell'espressione lambda. Altri dettagli di questa conversione sono disponibili in §10.7.3.

Esempio: il programma seguente rappresenta un'espressione lambda sia come codice eseguibile che come albero delle espressioni. Poiché esiste una conversione in Func<int,int>, esiste anche una conversione in Expression<Func<int,int>>:

Func<int,int> del = x => x + 1;             // Code
Expression<Func<int,int>> exp = x => x + 1; // Data

Dopo queste assegnazioni, il delegato del fa riferimento a un metodo che restituisce x + 1e la struttura ad albero delle espressioni fa riferimento a una struttura di dati che descrive l'espressione x => x + 1.

esempio finale

Expression<TDelegate> fornisce un metodo Compile di istanza che produce un delegato di tipo TDelegate:

Func<int,int> del2 = exp.Compile();

La chiamata di questo delegato fa sì che il codice rappresentato dall'albero delle espressioni venga eseguito. Di conseguenza, date le definizioni precedenti del e del2 sono equivalenti, e le due istruzioni seguenti avranno lo stesso effetto:

int i1 = del(1);
int i2 = del2(1);

Dopo l'esecuzione di questo codice i1 e i2 avrà entrambi il valore 2.

La superficie API fornita da Expression<TDelegate> è definita dall'implementazione oltre il requisito per un Compile metodo descritto in precedenza.

Nota: mentre i dettagli dell'API fornita per gli alberi delle espressioni sono definiti dall'implementazione, è previsto che un'implementazione:

  • Abilitare il codice per esaminare e rispondere alla struttura di un albero delle espressioni creato come risultato di una conversione da un'espressione lambda
  • Abilitare la creazione di alberi delle espressioni a livello di codice utente

nota finale

8.7 Tipo dinamico

Il tipo dynamic usa l'associazione dinamica, come descritto in dettaglio in §12.3.2, anziché l'associazione statica usata da tutti gli altri tipi.

Il tipo dynamic è considerato identico a object tranne nei seguenti aspetti:

  • Le operazioni sulle espressioni di tipo dynamic possono essere associate dinamicamente (§12.3.3).
  • L'inferenza del tipo (§12.6.3) preferisce dynamic se object entrambi sono candidati.
  • dynamic non può essere usato come
    • tipo in un object_creation_expression (§12.8.17.2)
    • un class_base (§15.2.4)
    • un predefined_type in un member_access (§12.8.7.1)
    • operando dell'operatore typeof
    • un argomento attributo
    • un vincolo
    • un tipo di metodo di estensione
    • qualsiasi parte di un argomento di tipo all'interno di struct_interfaces (§16.2.5) o interface_type_list (§15.2.4.1).

A causa di questa equivalenza, sono contenuti i seguenti elementi:

  • Esiste una conversione implicita di identità
    • tra object e dynamic
    • tra tipi costruiti uguali quando si sostituisce dynamic con object
    • tra i tipi di tupla uguali quando si sostituisce dynamic con object
  • Le conversioni implicite ed esplicite in e da object si applicano anche a e da dynamic.
  • Le firme uguali quando si sostituisce dynamic con object vengono considerate la stessa firma.
  • Il tipo dynamic è indistinguibile dal tipo object in fase di esecuzione.
  • Un'espressione del tipo dynamic viene definita espressione dinamica.

8.8 Tipi non gestiti

unmanaged_type
    : value_type
    | pointer_type     // unsafe code support
    ;

Un unmanaged_type è qualsiasi tipo che non è né un reference_type né un type_parameter non vincolato a essere non gestito e non contiene campi di istanza il cui tipo non è un unmanaged_type. In altre parole, un unmanaged_type è uno dei seguenti:

  • sbyte, byte, short, , ushortint, uintlongulongchar, float, double, o .decimalbool
  • Qualsiasi enum_type.
  • Qualsiasi struct_type definito dall'utente che contiene solo campi di istanza di unmanaged_type.
  • Qualsiasi parametro di tipo vincolato per essere non gestito.
  • Qualsiasi pointer_type (§23.3).

8.9 Tipi di riferimento e nullbility

8.9.1 Generale

Un tipo riferimento nullable viene indicato aggiungendo un nullable_type_annotation (?) a un tipo riferimento non nullable. Non esiste alcuna differenza semantica tra un tipo riferimento non nullable e il tipo nullable corrispondente, entrambi possono essere un riferimento a un oggetto o null. La presenza o l'assenza del nullable_type_annotation dichiara se un'espressione deve consentire o meno valori Null. Un compilatore può fornire la diagnostica quando un'espressione non viene usata in base a tale finalità. Lo stato Null di un'espressione è definito in §8.9.5. Esiste una conversione di identità tra un tipo riferimento nullable e il tipo di riferimento non nullable corrispondente (§10.2.2).

Esistono due forme di nullità per i tipi di riferimento:

  • nullable: è possibile assegnare un null . Lo stato null predefinito è forse null.
  • non nullable: a un riferimento non nullable non deve essere assegnato un null valore. Lo stato Null predefinito non è Null.

Nota: i tipi R e R? sono rappresentati dallo stesso tipo sottostante, R. Una variabile di tale tipo sottostante può contenere un riferimento a un oggetto o essere il valore null, che indica "nessun riferimento". nota finale

La distinzione sintattica tra un tipo riferimento nullable e il tipo riferimento non nullable corrispondente consente a un compilatore di generare la diagnostica. Un compilatore deve consentire il nullable_type_annotation come definito in §8.2.1. La diagnostica deve essere limitata agli avvisi. Né la presenza o l'assenza di annotazioni nullable, né lo stato del contesto nullable può modificare il comportamento in fase di compilazione o di runtime di un programma, ad eccezione delle modifiche apportate ai messaggi di diagnostica generati in fase di compilazione.

8.9.2 Tipi di riferimento non nullable

Un tipo riferimento non nullable è un tipo riferimento del formato T, dove T è il nome del tipo. Lo stato null predefinito di una variabile non nullable non è Null. Gli avvisi possono essere generati quando viene usata un'espressione che è forse Null in cui è necessario un valore non Null .

8.9.3 Tipi riferimento nullable

Un tipo riferimento del form T? , ad esempio string?, è un tipo riferimento nullable. Lo stato null predefinito di una variabile nullable è forse Null. L'annotazione ? indica la finalità che le variabili di questo tipo sono nullable. Un compilatore può riconoscere queste finalità per generare avvisi. Quando il contesto di annotazione nullable è disabilitato, l'uso di questa annotazione può generare un avviso.

8.9.4 Contesto nullable

8.9.4.1 Generale

Ogni riga di codice sorgente ha un contesto nullable. Annotazioni e flag di avviso per il controllo del contesto nullable rispettivamente (§8.9.4.3) e avvisi nullable (§8.9.4.4). Ogni flag può essere abilitato o disabilitato. Un compilatore può usare l'analisi statica del flusso per determinare lo stato Null di qualsiasi variabile di riferimento. Lo stato Null di una variabile di riferimento (§8.9.5) non è null, forse null o forse predefinito.

Il contesto nullable può essere specificato all'interno del codice sorgente tramite direttive nullable (§6.5.9) e/o tramite un meccanismo specifico dell'implementazione esterno al codice sorgente. Se vengono usati entrambi gli approcci, le direttive nullable sostituisce le impostazioni effettuate tramite un meccanismo esterno.

Lo stato predefinito del contesto nullable è definito dall'implementazione.

In questa specifica, tutto il codice C# che non contiene direttive nullable o su cui non viene eseguita alcuna istruzione relativa allo stato del contesto nullable corrente, si presuppone che sia stata compilata usando un contesto nullable in cui sono abilitate sia le annotazioni che gli avvisi.

Nota: un contesto nullable in cui entrambi i flag sono disabilitati corrisponde al comportamento standard precedente per i tipi di riferimento. nota finale

8.9.4.2 Disabilitazione nullable

Quando entrambi i flag di avviso e annotazioni sono disabilitati, il contesto nullable è disabilitato.

Quando il contesto nullable è disabilitato:

  • Non verrà generato alcun avviso quando una variabile di un tipo riferimento non annotato viene inizializzata con o viene assegnato un valore di . null
  • Non verrà generato alcun avviso quando una variabile di un tipo riferimento che può avere il valore Null.
  • Per qualsiasi tipo di Triferimento , l'annotazione ? in T? genera un messaggio e il tipo T? è uguale Ta .
  • Per qualsiasi vincolo where T : C?di parametro di tipo , l'annotazione ? in C? genera un messaggio e il tipo C? è uguale Ca .
  • Per qualsiasi vincolo where T : U?di parametro di tipo , l'annotazione ? in U? genera un messaggio e il tipo U? è uguale Ua .
  • Il vincolo class? generico genera un messaggio di avviso. Il parametro di tipo deve essere un tipo riferimento.

    Nota: questo messaggio è caratterizzato come "informativo" anziché come "avviso", in modo da non confonderlo con lo stato dell'impostazione di avviso nullable, che non è correlato. nota finale

  • L'operatore ! null-forgiving (§12.8.9) non ha alcun effetto.

Esempio:

#nullable disable annotations
string? s1 = null;    // Informational message; ? is ignored
string s2 = null;     // OK; null initialization of a reference
s2 = null;            // OK; null assignment to a reference
char c1 = s2[1];      // OK; no warning on dereference of a possible null;
                      //     throws NullReferenceException
c1 = s2![1];          // OK; ! is ignored

esempio finale

8.9.4.3 Annotazioni nullable

Quando il flag di avviso è disabilitato e il flag di annotazioni è abilitato, il contesto nullable è annotazioni.

Quando il contesto nullable è un'annotazione:

  • Per qualsiasi tipo di Triferimento , l'annotazione ? in T? indica che T? un tipo nullable, mentre l'oggetto non è T nullable.
  • Non vengono generati avvisi di diagnostica correlati a valori Null.
  • L'operatore ! null-forgiving (§12.8.9) può modificare lo stato Null analizzato del relativo operando e quali avvisi diagnostici in fase di compilazione vengono generati.

Esempio:

#nullable disable warnings
#nullable enable annotations
string? s1 = null;    // OK; ? makes s2 nullable
string s2 = null;     // OK; warnings are disabled
s2 = null;            // OK; warnings are disabled
char c1 = s2[1];      // OK; warnings are disabled; throws NullReferenceException
c1 = s2![1];          // No warnings

esempio finale

8.9.4.4 Avvisi nullable

Quando il flag di avviso è abilitato e il flag di annotazioni è disabilitato, il contesto nullable viene visualizzato come avviso.

Quando il contesto nullable è un avviso, un compilatore può generare la diagnostica nei casi seguenti:

  • Una variabile di riferimento che è stata determinata come forse null, viene dereferenziata.
  • Una variabile di riferimento di un tipo non nullable viene assegnata a un'espressione che è forse Null.
  • Viene ? utilizzato per prendere nota di un tipo riferimento nullable.
  • L'operatore null-forgiving (!) viene utilizzato per impostare lo stato Null del relativo operando su non Null.

Esempio:

#nullable disable annotations
#nullable enable warnings
string? s1 = null;    // OK; ? makes s2 nullable
string s2 = null;     // OK; null-state of s2 is "maybe null"
s2 = null;            // OK; null-state of s2 is "maybe null"
char c1 = s2[1];      // Warning; dereference of a possible null;
                      //          throws NullReferenceException
c1 = s2![1];          // The warning is suppressed

esempio finale

8.9.4.5 Abilitazione nullable

Quando sono abilitati sia il flag di avviso che il flag di annotazioni, il contesto nullable è abilitato.

Quando il contesto nullable è abilitato:

  • Per qualsiasi tipo di Triferimento , l'annotazione ? in T? rende T? nullable un tipo, mentre l'oggetto nonannoted T non è nullable.
  • Un compilatore può usare l'analisi statica del flusso per determinare lo stato Null di qualsiasi variabile di riferimento. Quando gli avvisi nullable sono abilitati, lo stato Null di una variabile di riferimento (§8.9.5) non è null, forse null o forse predefinito e
  • L'operatore null-forgiving (!) imposta lo stato Null del relativo operando su non null.
  • Un compilatore può emettere un avviso se la nullabilità di un parametro di tipo non corrisponde alla nullabilità dell'argomento di tipo corrispondente.

8.9.5 Nullabilities e stati Null

Non è necessario che un compilatore esegua alcuna analisi statica né sia necessario generare avvisi di diagnostica correlati a valori Null.

Il resto di questa sottoclausa è normativo in modo condizionale.

Un compilatore che genera avvisi di diagnostica è conforme a queste regole.

Ogni espressione ha uno dei tre statiNull:

  • forse null: il valore dell'espressione può restituire null.
  • forse predefinito: il valore dell'espressione può restituire il valore predefinito per quel tipo.
  • not null: il valore dell'espressione non è Null.

Lo stato Null predefinito di un'espressione è determinato dal tipo e dallo stato del flag di annotazioni quando viene dichiarato:

  • Lo stato Null predefinito di un tipo riferimento nullable è:
    • Può essere Null quando la dichiarazione è in testo in cui è abilitato il flag di annotazioni.
    • Non null quando la dichiarazione è in testo in cui il flag di annotazioni è disabilitato.
  • Lo stato Null predefinito di un tipo riferimento non nullable non è Null.

Nota: lo stato forse predefinito viene usato con parametri di tipo non vincolato quando il tipo è un tipo non nullable, ad esempio string e l'espressione default(T) è il valore Null. Poiché null non è incluso nel dominio per il tipo non nullable, lo stato potrebbe essere predefinito. nota finale

Una diagnostica può essere generata quando una variabile (§9.2.1) di un tipo riferimento non nullable viene inizializzata o assegnata a un'espressione che è forse Null quando tale variabile viene dichiarata in testo in cui è abilitato il flag di annotazione.

Esempio: considerare il metodo seguente in cui un parametro è nullable e tale valore viene assegnato a un tipo non nullable:

#nullable enable
public class C
{
    public void M(string? p)
    {
        // Warning: Assignment of maybe null value to non-nullable variable
        string s = p;
    }
}

Un compilatore può generare un avviso in cui il parametro che potrebbe essere Null viene assegnato a una variabile che non deve essere Null. Se il parametro viene controllato per valori nulli prima dell'assegnazione, il compilatore può utilizzare questo controllo nell'analisi dello stato di nullabilità e non emettere un avviso.

#nullable enable
public class C
{
    public void M(string? p)
    {
        if (p != null)
        {
            string s = p; // No warning
            // Use s
        }
    }
}

esempio finale

Un compilatore può aggiornare lo stato Null di una variabile come parte dell'analisi.

esempio: un compilatore può scegliere di aggiornare lo stato in base a qualsiasi istruzione nel programma:

#nullable enable
public void M(string? p)
{
    int length = p.Length; // Warning: p is maybe null

    string s = p; // No warning. p is not null

    if (s != null)
    {
        int l2 = s.Length; // No warning. s is not null 
    }
    int l3 = s.Length; // Warning. s is maybe null
}

Nell'esempio precedente un compilatore può decidere che, dopo l'istruzione int length = p.Length;, lo stato di nullità di p non è null. Se fosse null, tale istruzione avrebbe generato un oggetto NullReferenceException. È simile al comportamento se il codice era stato preceduto da if (p == null) throw NullReferenceException(); , ad eccezione del fatto che il codice scritto può generare un avviso, lo scopo del quale è avvisare che un'eccezione può essere generata in modo implicito. esempio finale

Più avanti nel metodo, il codice verifica che s non sia un riferimento Null. Lo stato Null di s può essere impostato su null dopo la chiusura del blocco con controllo Null. Un compilatore può dedurre che s è forse null perché il codice è stato scritto per presupporre che fosse null. In genere, quando il codice contiene un controllo Null, un compilatore può dedurre che il valore potrebbe essere null:

esempio: ognuna delle espressioni seguenti include una forma di controllo nullo. Lo stato Null di o può passare da non Null a forse null dopo ognuna di queste istruzioni:

#nullable enable
public void M(string s)
{
    int length = s.Length; // No warning. s is not null

    _ = s == null; // Null check by testing equality. The null state of s is maybe null
    length = s.Length; // Warning, and changes the null state of s to not null

    _ = s?.Length; // The ?. is a null check and changes the null state of s to maybe null
    if (s.Length > 4) // Warning. Changes null state of s to not null
    {
        _ = s?[4]; // ?[] is a null check and changes the null state of s to maybe null
        _ = s.Length; // Warning. s is maybe null
    }
}

Entrambe le dichiarazioni di evento auto-property e field-like usano un campo sottostante generato dal compilatore. L'analisi dello stato Null può dedurre che l'assegnazione all'evento o alla proprietà è un'assegnazione a un campo sottostante generato dal compilatore.

esempio: un compilatore può determinare che la scrittura di una proprietà automatica o di un evento simile a un campo scrive il campo sottostante generato dal compilatore corrispondente. Lo stato null della proprietà corrisponde a quello del campo di supporto.

class Test
{
    public string P
    {
        get;
        set;
    }

    public Test() {} // Warning. "P" not set to a non-null value.

    static void Main()
    {
        var t = new Test();
        int len = t.P.Length; // No warning. Null state is not null.
    }
}

Nell'esempio precedente, il costruttore non imposta P su un valore non null e un compilatore potrebbe generare un avviso. Non viene visualizzato alcun avviso quando si accede alla proprietà P, perché il tipo della proprietà è un tipo di riferimento non annullabile. esempio finale

Un compilatore può considerare una proprietà (§15.7) come variabile con stato o come funzioni di accesso get e set indipendenti (§15.7.3).

Esempio: un compilatore può scegliere se la scrittura in una proprietà modifica lo stato null della lettura della stessa, oppure se la lettura di una proprietà modifica lo stato null di tale proprietà.

class Test
{
    private string? _field;
    public string? DisappearingProperty
    {
        get
        {
               string tmp = _field;
               _field = null;
               return tmp;
        }
        set
        {
             _field = value;
        }
    }

    static void Main()
    {
        var t = new Test();
        if (t.DisappearingProperty != null)
        {
            int len = t.DisappearingProperty.Length; // No warning. A compiler can assume property is stateful
        }
    }
}

Nell'esempio precedente il campo sottostante per è DisappearingProperty impostato su Null quando viene letto. Tuttavia, un compilatore può presupporre che la lettura di una proprietà non modifichi lo stato Null di tale espressione. esempio finale

Un compilatore può usare qualsiasi espressione che dereferenzia una variabile, una proprietà o un evento per impostare lo stato Null su non Null. Se fosse Null, l'espressione di dereferenziazione avrebbe generato un NullReferenceException:

Esempio:


public class C
{
    private C? child;
   
    public void M()
    {
        _ = child.child.child; // Warning. Dereference possible null value
        var greatGrandChild = child.child.child; // No warning. 
    }
}

esempio finale

Fine del testo normativo condizionale