C# è a tipizzazione statica: ogni variabile, campo ed espressione ha un tipo noto in fase di compilazione. Il linguaggio ha un insieme di tipi predefiniti con parole chiave proprie (int, double, string...). Ogni parola chiave è un alias di un tipo del namespace System, quindi int e System.Int32 sono lo stesso tipo, così come string e System.String.
I tipi predefiniti
| Tipo | Tipo .NET | Dimensione | Intervallo o precisione | Letterale |
|---|---|---|---|---|
bool | Boolean | 1 byte | true o false | true |
byte | Byte | 1 byte | da 0 a 255 | 200 |
sbyte | SByte | 1 byte | da -128 a 127 | -5 |
short | Int16 | 2 byte | da -32,768 a 32,767 | 1200 |
ushort | UInt16 | 2 byte | da 0 a 65,535 | 60000 |
int | Int32 | 4 byte | da -2,147,483,648 a 2,147,483,647 | 42 |
uint | UInt32 | 4 byte | da 0 a 4,294,967,295 | 42u |
long | Int64 | 8 byte | circa ±9.2 × 10^18 | 42L |
ulong | UInt64 | 8 byte | da 0 a circa 1.8 × 10^19 | 42ul |
float | Single | 4 byte | circa ±3.4 × 10^38, ~6-9 cifre | 2.5f |
double | Double | 8 byte | circa ±1.8 × 10^308, ~15-17 cifre | 2.5 o 2.5d |
decimal | Decimal | 16 byte | circa ±7.9 × 10^28, 28-29 cifre | 2.5m |
char | Char | 2 byte | una unità di codice UTF-16, da U+0000 a U+FFFF | 'A' |
string | String | riferimento | testo di qualsiasi lunghezza, immutabile | "hello" |
object | Object | riferimento | la base di ogni tipo | new object() |
Un letterale intero senza suffisso è un int, oppure il primo tra uint, long e ulong in grado di contenerlo se è troppo grande per un int, e un letterale con il punto decimale è un double. Ecco perché float f = 2.5; e decimal d = 2.5; non compilano: il letterale è un double, e non esiste una conversione implicita da double a float o decimal. Aggiungi il suffisso: 2.5f, 2.5m.
Ogni tipo numerico espone i propri limiti come costanti:
Output:
byte: 0 to 255
short: -32768 to 32767
int: -2147483648 to 2147483647
long: -9223372036854775808 to 9223372036854775807
decimal max: 79228162514264337593543950335
double max: 1.798E+308
sizes: int 4, long 8, double 8, char 2
255 161 3000000000
In pratica la maggior parte del codice ne usa quattro: int per conteggi e indici, long per ID e totali grandi, double per misure e calcoli matematici, e decimal per il denaro.
decimal vs double
double (e float) memorizzano i numeri in binario. La maggior parte delle frazioni decimali, 0.1 compreso, non ha una forma binaria esatta, quindi viene memorizzata come il valore rappresentabile più vicino e piccoli errori si accumulano. decimal memorizza un intero in base 10 più una scala, quindi 0.1 è esattamente 0.1.
Output:
False
True
0.30000000000000004
0.3
double sum == 1.0: False
decimal sum == 1.0: True
Total: 59.97
Split three ways: 19.99
Usa decimal per denaro, prezzi, tasse e qualsiasi cosa una persona sommerà a mano e confronterà. Conserva gli zeri finali dei suoi input (1.50m viene stampato come 1.50) e ha da 28 a 29 cifre significative.
Usa double per misure fisiche, statistica, geometria e giochi. È varie volte più veloce, usa metà della memoria e il suo intervallo è enorme. Non confrontare mai due double calcolati con ==; confronta la differenza con una tolleranza: Math.Abs(x - y) < 1e-9.
decimal non fa miracoli: 1m / 3m resta comunque 0.3333333333333333333333333333, e l'aritmetica con double non lancia eccezioni nella divisione per zero (produce infinito, o NaN per 0.0 / 0.0), mentre la divisione per zero con decimal e con gli interi lancia una DivideByZeroException.
Overflow degli interi e checked
L'aritmetica intera che supera l'intervallo del tipo per impostazione predefinita ricomincia dall'altro estremo, in silenzio. Il compilatore rileva l'overflow nelle espressioni costanti, ma non quando i valori arrivano da variabili:
Output:
-2147483648
OverflowException
-1294967296
2147483648
checked(...) o un blocco checked { ... } trasforma l'overflow in una OverflowException. Puoi anche attivarlo per l'intero progetto con <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>. Scrivere int x = int.MaxValue + 1; con delle costanti è un errore di compilazione (CS0220), perché il compilatore vede l'overflow.
L'ultima riga mostra la soluzione per la maggior parte dei bug di overflow: convertire in un tipo più ampio prima dell'operazione. (long)(max + 1) sarebbe sbagliato, perché quando il cast viene eseguito l'addizione è già andata in overflow.
Tipi valore e tipi riferimento
Ogni tipo C# è un tipo valore o un tipo riferimento, e la differenza decide cosa fa l'assegnazione.
- Tipi valore: tutti i tipi numerici,
bool,char,DateTime, gli enum e ognistruct. La variabile contiene il dato; l'assegnazione lo copia. - Tipi riferimento:
string, gli array,objecte ogniclass. La variabile contiene un riferimento a un oggetto; l'assegnazione copia il riferimento, quindi entrambe le variabili vedono lo stesso oggetto.
Output:
a=10 b=99
first[0]=99
s1=cat s2=cats
string è un tipo riferimento che in pratica si comporta come un valore: è immutabile, e == confronta il testo, non i riferimenti. Trovi di più sui tipi valore definiti dall'utente nelle struct.
char e bool
Un char è una unità di codice UTF-16, scritta tra apici singoli. Sotto sotto è un tipo numerico, quindi si converte in int e supporta l'aritmetica:
Output:
66
C
True
True
7
b
True
bool ha esattamente due valori e non si converte da o verso i numeri: if (count) è un errore di compilazione, non un controllo sul diverso da zero. Scrivi if (count != 0). I caratteri fuori dal Basic Multilingual Plane, come la maggior parte delle emoji, occupano due valori char (una coppia surrogata), quindi la Length di una string conta le unità UTF-16, non i caratteri visibili.
Valori predefiniti
Campi, elementi di array e default(T) ricevono il valore predefinito del tipo: zero per i numeri, false per bool, '\0' per char e null per i tipi riferimento. Le variabili locali non ricevono alcun valore predefinito e vanno assegnate prima dell'uso.
Output:
0
0
False
True
0001-01-01
0,0,0
Da C# 7.1 puoi scrivere il letterale default senza il tipo quando il compilatore riesce a dedurlo (int x = default;). Un tipo valore non può essere null, ed è a questo che serve int? (un tipo valore nullable); vedi i tipi nullable.
Interi a dimensione nativa e interi grandi
Esistono altri due tipi numerici per casi speciali. nint e nuint (C# 9) sono interi grandi quanto un puntatore, 32 o 64 bit a seconda della piattaforma, usati nell'interoperabilità e nel codice a basso livello. System.Numerics.BigInteger contiene interi di qualsiasi dimensione, a scapito della velocità:
using System.Numerics;
BigInteger factorial = 1;
for (int i = 2; i <= 30; i++) factorial *= i;
// 265252859812191058636308480000000
Domande frequenti
Quali sono i tipi di dati in C#?
I tipi predefiniti sono gli interi (sbyte, byte, short, ushort, int, uint, long, ulong), i tipi a virgola mobile float e double, decimal per l'aritmetica decimale esatta, char per un carattere UTF-16, bool e i tipi riferimento string e object. Ogni parola chiave è un alias di un tipo .NET: int è System.Int32, string è System.String.
Per il denaro in C# meglio decimal o double?
Usa decimal. Un double memorizza frazioni binarie, quindi valori come 0.1 non si possono rappresentare esattamente e le somme si discostano (0.1 + 0.2 == 0.3 è false). Un decimal memorizza cifre in base 10, mantiene da 28 a 29 cifre significative e dà i risultati che un contabile si aspetta. double è più veloce e ha un intervallo molto più ampio, quindi usalo per scienza, grafica e misurazioni.
Qual è il valore massimo di int in C#?
int.MaxValue è 2,147,483,647 e int.MinValue è -2,147,483,648 (un intero con segno a 32 bit). Sommare 1 a int.MaxValue torna silenziosamente a int.MinValue, a meno che il codice non giri in un contesto checked, dove lancia una OverflowException. Per valori più grandi usa long (fino a circa 9,2 trilioni di milioni).
Che differenza c'è tra float e double in C#?
float è a 32 bit con circa 6-9 cifre significative; double è a 64 bit con circa 15-17. I letterali a virgola mobile sono double per impostazione predefinita, quindi un float richiede il suffisso f: float speed = 2.5f;. Usa double a meno che la memoria o un'API (come Vector3 di Unity) non richieda float.
Che differenza c'è tra tipi valore e tipi riferimento in C#?
Una variabile di tipo valore (int, double, bool, DateTime, qualsiasi struct) contiene il dato stesso, e l'assegnazione lo copia. Una variabile di tipo riferimento (string, array, qualsiasi class) contiene un riferimento a un oggetto nell'heap, e l'assegnazione copia il riferimento, quindi due variabili possono puntare allo stesso oggetto.