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.Object
predefinita .
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.String
predefinita .
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 class
System.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
,short
ushort
,int
,uint
, ,long
eulong
, 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_type
E
, il valore predefinito è0
, convertito nel tipoE
.
- Per
- 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 laValue
proprietà di tale valore, viene generata un'eccezione di tipoSystem.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
i
j
ek
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 inSystem.Int32
e i membri ereditati daSystem.Object
e 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 tipoint
ed'a'
è un valore letterale di tipochar
. 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
, short
ushort
, 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
e127
, inclusi. - Il
byte
tipo rappresenta interi senza segno a 8 bit con valori compresi tra0
e255
, inclusi. - Il
short
tipo rappresenta interi con segno a 16 bit con valori compresi tra-32768
e32767
, inclusi. - Il
ushort
tipo rappresenta interi senza segno a 16 bit con valori compresi tra0
e65535
, inclusi. - Il
int
tipo rappresenta interi con segno a 32 bit con valori compresi tra-2147483648
e2147483647
, inclusi. - Il
uint
tipo rappresenta interi senza segno a 32 bit con valori compresi tra0
e4294967295
, inclusi. - Il
long
tipo rappresenta interi con segno a 64 bit con valori compresi tra-9223372036854775808
e9223372036854775807
, inclusi. - Il
ulong
tipo rappresenta interi senza segno a 64 bit con valori compresi tra0
e18446744073709551615
, inclusi. - Il
char
tipo rappresenta interi senza segno a 16 bit con valori compresi tra0
e65535
, inclusi. Il set di valori possibili per ilchar
tipo corrisponde al set di caratteri Unicode.Nota: sebbene
char
abbia la stessa rappresentazione diushort
, 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 ibyte
tipi eushort
hanno intervalli di valori completamente rappresentabili usando ilchar
tipo, le conversioni implicite da sbyte, byte oushort
perchar
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 formatox * y / z
, in cui la moltiplicazione produce un risultato esterno all'intervallodouble
, ma la divisione successiva riporta il risultato temporaneo nell'intervallodouble
, 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 Emin ≤ e ≤ 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 decimal
s con un valore assoluto minore di 1.0m
, il valore è esatto per almeno il 28° separatore decimale. Per decimal
s 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 decimal
e 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
false
booleano e un valore integrale o a virgola mobile diverso da zero oppure un puntatore non Null può essere convertito nel valoretrue
booleano . 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 connull
. 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
, , sbyte
short
, ushort
int
, 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 Item2
così 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 ilItem...
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
,pair2
epair3
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 nomiItem1
eItem2
le relative posizioni corrispondono, mentre il tipo di tupla perpair5
non è consentito, perché i nomiItem2
eItem123
non lo sono.Le dichiarazioni per
pair6
epair7
dimostrano che i tipi di tupla sono intercambiabili con i tipi costruiti del formatoValueTuple<...>
e che l'operatorenew
è 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 tipobool
- Proprietà
Value
di tipoT
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 diC
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 tipoA
sia convertibile in tipoC
da uno dei seguenti: - Se il vincolo è il vincolo di tipo riferimento (
class
), il tipoA
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
eSystem.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 tipoA
deve soddisfare uno dei seguenti:-
A
è unstruct
tipo oenum
un tipo, ma non un tipo di valore nullable.
Nota:
System.ValueType
eSystem.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 tipoA
non deve essereabstract
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 oggettoclass
che non è astratto e contiene un costruttore pubblico dichiarato in modo esplicito senza parametri. -
A
nonabstract
è 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,class
E
non è necessario specificare un vincolo, perchéList<T>
implementaIEnumerable
per qualsiasiT
oggetto .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 D
delegato , 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 inExpression<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 restituiscex + 1
e la struttura ad albero delle espressioni fa riferimento a una struttura di dati che descrive l'espressionex => 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
seobject
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
edynamic
- tra tipi costruiti uguali quando si sostituisce
dynamic
conobject
- tra i tipi di tupla uguali quando si sostituisce
dynamic
conobject
- tra
- Le conversioni implicite ed esplicite in e da
object
si applicano anche a e dadynamic
. - Le firme uguali quando si sostituisce
dynamic
conobject
vengono considerate la stessa firma. - Il tipo
dynamic
è indistinguibile dal tipoobject
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
, ,ushort
int
,uint
long
ulong
char
,float
,double
, o .decimal
bool
- 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
eR?
sono rappresentati dallo stesso tipo sottostante,R
. Una variabile di tale tipo sottostante può contenere un riferimento a un oggetto o essere il valorenull
, 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
T
riferimento , l'annotazione?
inT?
genera un messaggio e il tipoT?
è ugualeT
a . - Per qualsiasi vincolo
where T : C?
di parametro di tipo , l'annotazione?
inC?
genera un messaggio e il tipoC?
è ugualeC
a . - Per qualsiasi vincolo
where T : U?
di parametro di tipo , l'annotazione?
inU?
genera un messaggio e il tipoU?
è ugualeU
a . - 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
T
riferimento , l'annotazione?
inT?
indica cheT?
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
T
riferimento , l'annotazione?
inT?
rendeT?
nullable un tipo, mentre l'oggetto nonannotedT
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'espressionedefault(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à dip
non è null. Se fosse null, tale istruzione avrebbe generato un oggettoNullReferenceException
. È simile al comportamento se il codice era stato preceduto daif (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
ECMA C# draft specification