Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
C# è un linguaggio fortemente tipizzato. Ogni variabile e costante ha un tipo, così come ogni espressione che restituisce un valore. C# usa principalmente un sistema di tipi normativi. Un sistema di tipi normativi usa nomi per identificare ogni tipo. In C#, struct, classe interface i tipi, inclusi record i tipi, sono tutti identificati dal nome. Ogni dichiarazione di metodo specifica un nome, un tipo e una forma (valore, riferimento o uscita) per ogni parametro e per il valore restituito. La libreria di classi .NET definisce tipi numerici predefiniti e tipi complessi che rappresentano un'ampia gamma di costrutti. Questi costrutti includono il file system, le connessioni di rete, le raccolte e le matrici di oggetti e date. Un tipico programma C# usa tipi della libreria di classi e dei tipi definiti dall'utente che modellano i concetti specifici del dominio del problema del programma.
C# supporta anche tipi strutturali, ad esempio tuple e tipi anonimi. I tipi strutturali sono definiti dai nomi e dai tipi di ogni membro e dall'ordine dei membri in un'espressione. I tipi strutturali non hanno nomi univoci.
Le informazioni archiviate in un tipo possono includere gli elementi seguenti:
- Spazio di archiviazione richiesto da una variabile del tipo.
- Valori massimi e minimi che può rappresentare.
- Membri (metodi, campi, eventi e così via) che esso contiene.
- Tipo di base da cui eredita.
- Le interfacce che implementa.
- Operazioni consentite.
Il compilatore usa informazioni sul tipo per assicurarsi che tutte le operazioni eseguite nel tuo codice siano sicure per i tipi. Ad esempio, se si dichiara una variabile di tipo int, il compilatore consente di usare la variabile in aggiunta e le operazioni di sottrazione. Se si tenta di eseguire queste stesse operazioni su una variabile di tipo bool, il compilatore genera un errore, come illustrato nell'esempio seguente:
int a = 5;
int b = a + 2; //OK
bool test = true;
// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;
Annotazioni
Gli sviluppatori C e C++ notate che in C#, bool non è convertibile in int.
Il compilatore incorpora le informazioni sul tipo nel file eseguibile come metadati. Common Language Runtime (CLR) usa i metadati in fase di esecuzione per garantire una maggiore sicurezza dei tipi quando alloca e recupera la memoria.
Specificare i tipi nelle dichiarazioni delle variabili
Quando si dichiara una variabile o una costante in un programma, è necessario specificarne il tipo o usare la var parola chiave per consentire al compilatore di dedurre il tipo. L'esempio seguente mostra alcune dichiarazioni di variabili che usano sia tipi numerici predefiniti che tipi complessi definiti dall'utente:
// Declaration only:
float temperature;
string name;
MyClass myClass;
// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = [0, 1, 2, 3, 4, 5];
var query = from item in source
where item <= limit
select item;
Specificare i tipi di parametri del metodo e i valori restituiti nella dichiarazione del metodo. La firma seguente mostra un metodo che richiede un int come argomento di input e restituisce una stringa.
public string GetName(int ID)
{
if (ID < names.Length)
return names[ID];
else
return String.Empty;
}
private string[] names = ["Spencer", "Sally", "Doug"];
Dopo aver dichiarato una variabile, non è possibile ripeterla con un nuovo tipo e non è possibile assegnare un valore non compatibile con il tipo dichiarato. Ad esempio, non è possibile dichiarare un oggetto int e quindi assegnargli un valore booleano pari truea . Tuttavia, è possibile convertire i valori in altri tipi, ad esempio quando vengono assegnati a nuove variabili o passarli come argomenti del metodo. Il compilatore esegue automaticamente una conversione dei tipi che non causa la perdita di dati. Una conversione che potrebbe causare la perdita di dati richiede un cast nel codice sorgente.
Per ulteriori informazioni, consultare Conversioni di tipi e casting.
Tipi predefiniti
C# offre un set standard di tipi predefiniti. Questi tipi rappresentano numeri interi, valori a virgola mobile, espressioni booleane, caratteri di testo, valori decimali e altri tipi di dati. Il linguaggio include anche tipi predefiniti string e object. È possibile usare questi tipi in qualsiasi programma C#. Per un elenco completo dei tipi predefiniti, vedere Tipi predefiniti.
Tipi personalizzati
Creare tipi strutturali usando le tuple per l'archiviazione di membri dati correlati. Questi tipi forniscono una struttura che contiene più membri. Le tuple hanno un comportamento limitato. Sono un contenitore per i valori. Questi sono i tipi più semplici che è possibile creare. In un secondo momento è possibile decidere che è necessario il comportamento. In tal caso, è possibile convertire una tupla in un struct o class.
Usare i costrutti struct, class, interface, enum e record per creare tipi personalizzati. La libreria di classi .NET è una raccolta di tipi personalizzati che è possibile usare nelle proprie applicazioni. Per impostazione predefinita, i tipi usati più di frequente nella libreria di classi sono disponibili in qualsiasi programma C#. È possibile rendere disponibili altri tipi aggiungendo in modo esplicito un riferimento al pacchetto che li fornisce. Dopo che il compilatore ha un riferimento al pacchetto, è possibile dichiarare variabili e costanti dei tipi dichiarati negli assembly del pacchetto nel codice sorgente.
Una delle prime decisioni prese durante la definizione di un tipo consiste nel decidere quale costrutto usare per il tipo. L'elenco seguente consente di prendere tale decisione iniziale. Alcune scelte si sovrappongono. Nella maggior parte degli scenari, più di un'opzione è una scelta ragionevole.
- Se il tipo di dati non fa parte del dominio dell'app e non include il comportamento, usa un tipo strutturale.
- Se le dimensioni di archiviazione dei dati sono ridotte, non più di 64 byte, scegliere un
structorecord struct. - Se il tipo non è modificabile o si desidera una mutazione non distruttiva, scegliere
structorecord struct. - Se il tipo deve avere una semantica di valore per l'uguaglianza, scegliere un tipo
record classorecord struct. - Se il tipo è destinato principalmente all'archiviazione dei dati, con un comportamento minimo, scegliere
record classorecord struct. - Se il tipo fa parte di una gerarchia di ereditarietà, scegli un
record classo unclass. - Se il tipo usa il polimorfismo, selezionare un elemento
class. - Se lo scopo principale è la funzionalità, scegli un
class.
È anche possibile scegliere un oggetto interface per modellare un contratto: comportamento descritto dai membri che possono essere implementati da tipi non correlati. Le interfacce sono astratte e dichiarano membri che devono essere implementati da tutti i tipi class o struct che ereditano da tale interfaccia.
Sistema comune di tipi
Il sistema di tipi comune supporta il principio di ereditarietà. I tipi possono derivare da altri tipi, denominati tipi di base. Il tipo derivato eredita (con alcune restrizioni) i metodi, le proprietà e altri membri del tipo di base. Il tipo di base può a sua volta derivare da un altro tipo, nel qual caso il tipo derivato eredita i membri di entrambi i tipi di base nella gerarchia di ereditarietà.
Tutti i tipi, inclusi i tipi numerici predefiniti, System.Int32 ad esempio (parola chiave C#: int), derivano in definitiva da un singolo tipo di base, ovvero System.Object (parola chiave C#: object). Questa gerarchia di tipi unificati è denominata Common Type System (CTS). Per altre informazioni sull'ereditarietà in C#, vedere Ereditarietà.
Ogni tipo nel CTS viene definito come tipo valore o tipo riferimento. Questi tipi includono tutti i tipi personalizzati nella libreria di classi .NET e anche i tipi definiti dall'utente:
- I tipi definiti tramite le
structparole chiave orecord structsono tipi valore. Tutti i tipi numerici predefiniti sonostructs. - I tipi definiti tramite le
classparole chiave ,record classorecordsono tipi di riferimento.
I tipi riferimento e i tipi valore hanno regole in fase di compilazione diverse e un comportamento di runtime diverso.
Annotazioni
I tipi più comunemente usati sono tutti organizzati nello spazio dei nomi System. Tuttavia, lo spazio dei nomi in cui è contenuto un tipo non ha alcuna relazione se si tratta di un tipo valore o di un tipo riferimento.
Le classi e gli struct sono due dei costrutti di base del sistema di tipi comune in .NET. Ogni costrutto è essenzialmente una struttura di dati che incapsula un set di dati e comportamenti che appartengono insieme come unità logica. I dati e i comportamenti sono i membri della classe, dello struct o del record. I membri includono i relativi metodi, proprietà, eventi e così via, come indicato più avanti in questo articolo.
Una dichiarazione di classe, struct o record è simile a un progetto usato per creare istanze o oggetti in fase di esecuzione. Se si definisce una classe, uno struct o un record denominato Person, Person è il nome del tipo . Se si dichiara e inizializza una variabile p di tipo Person, p viene detto che è un oggetto o un'istanza di Person. È possibile creare più istanze dello stesso Person tipo e ogni istanza può avere valori diversi nelle relative proprietà e campi.
Una classe è un tipo riferimento. Quando si crea un oggetto del tipo, la variabile a cui si assegna l'oggetto contiene solo un riferimento a tale memoria. Quando si assegna il riferimento all'oggetto a una nuova variabile, la nuova variabile fa riferimento all'oggetto originale. Le modifiche apportate tramite una variabile vengono riflesse nell'altra variabile perché entrambe fanno riferimento agli stessi dati.
Una struttura è un tipo di valore. Quando si crea uno struct, la variabile a cui si assegna lo struct contiene i dati effettivi dello struct. Quando si assegna lo struct a una nuova variabile, viene copiato. La nuova variabile e la variabile originale contengono quindi due copie separate degli stessi dati. Le modifiche apportate a una copia non influiscono sull'altra copia.
I tipi di record possono essere tipi riferimento (record class) o tipi valore (record struct). I tipi di record contengono metodi che supportano l'uguaglianza dei valori.
In generale, usare le classi per modellare un comportamento più complesso. Le classi archiviano in genere dati che modifichi dopo la creazione di un oggetto classe. Gli struct sono più adatti per strutture di dati di piccole dimensioni. Gli struct archiviano di solito i dati che non vengono modificati dopo la creazione degli struct. I tipi di record sono strutture di dati con membri sintetizzati del compilatore aggiuntivi. I record in genere archiviano i dati che non vengono modificati dopo la creazione dell'oggetto.
Tipi di valori
I tipi valore derivano da System.ValueType, che deriva da System.Object. I tipi che derivano da System.ValueType hanno un comportamento speciale in CLR. Le variabili di tipo valore contengono direttamente i relativi valori. La memoria per una struct viene allocata in linea nel contesto in cui la variabile è dichiarata. È possibile dichiarare record struct tipi come tipi di valore e includere i membri sintetizzati per i record.
Esistono due categorie di tipi valore: struct e enum.
I tipi numerici predefiniti sono struct e dispongono di campi e metodi a cui è possibile accedere:
// constant field on type byte.
byte b = byte.MaxValue;
Ma si dichiarano e si assegnano valori come se fossero semplici tipi non aggregati:
byte num = 0xA;
int i = 5;
char c = 'Z';
I tipi valore sono sigillati. Non è possibile derivare un tipo da qualsiasi tipo di valore, ad esempio System.Int32. Non è possibile definire uno struct da ereditare da qualsiasi classe o struct definito dall'utente perché uno struct può ereditare solo da System.ValueType. Tuttavia, uno struct può implementare una o più interfacce. È possibile eseguire il cast di un tipo di struct a qualsiasi tipo di interfaccia implementato. Questo cast determina il wrapping dello struct all'interno di un oggetto tipo riferimento nell'heap gestito. Le operazioni boxing si verificano quando si passa un tipo valore a un metodo che accetta un System.Object tipo di interfaccia o come parametro di input. Per altre informazioni, vedere Boxing e Unboxing.
Usare la parola chiave struct per creare tipi di valore personalizzati. In genere, uno struct viene usato come contenitore per un piccolo set di variabili correlate, come illustrato nell'esempio seguente:
public struct Coords(int x, int y)
{
public int X { get; init; } = x;
public int Y { get; init; } = y;
}
Per altre informazioni sugli struct, vedere Tipi di struttura. Per altre informazioni sui tipi valore, vedere Tipi valore.
L'altra categoria di tipi valore è enum. Un'enumerazione definisce un set di costanti integrali denominate. Ad esempio, l'enumerazione System.IO.FileMode nella libreria di classi .NET contiene un set di numeri interi costanti denominati che specificano la modalità di apertura di un file. È definito come illustrato nell'esempio seguente:
public enum FileMode
{
CreateNew = 1,
Create = 2,
Open = 3,
OpenOrCreate = 4,
Truncate = 5,
Append = 6,
}
La costante System.IO.FileMode.Create ha un valore pari a 2. Tuttavia, il nome è molto più significativo per gli esseri umani che leggono il codice sorgente e per questo motivo è preferibile usare enumerazioni anziché numeri letterali costanti. Per altre informazioni, vedere System.IO.FileMode.
Tutte le enumerazioni ereditano da System.Enum, che eredita da System.ValueType. Tutte le regole applicabili agli struct si applicano anche alle enumerazioni. Per altre informazioni sulle enumerazioni, vedere Tipi di enumerazione.
Tipi riferimento
Un tipo definito come class, record class, record, delegate, array o interface è reference type.
Quando si dichiara una variabile di un reference type oggetto, essa contiene il valore null finché non viene assegnata con un'istanza di tale tipo o creata usando l'operatore new. L'esempio seguente illustra la creazione e l'assegnazione di una classe:
MyClass myClass = new();
MyClass myClass2 = myClass;
Non è possibile creare direttamente un'istanza di interface usando l'operatore new. Creare e assegnare invece un'istanza di una classe che implementa l'interfaccia . Si consideri l'esempio seguente:
MyClass myClass = new();
// Declare and assign using an existing value.
IMyInterface myInterface = myClass;
// Or create and assign a value in a single statement.
IMyInterface myInterface2 = new MyClass();
Quando si crea l'oggetto, il sistema alloca memoria nell'heap gestito. La variabile contiene solo un riferimento alla posizione dell'oggetto . I tipi nell'heap gestito richiedono un overhead sia quando vengono allocati che quando vengono rilasciati. Garbage Collection è la funzionalità di gestione automatica della memoria di CLR, che esegue il recupero. Tuttavia, anche Garbage Collection è altamente ottimizzato e nella maggior parte degli scenari non crea un problema di prestazioni. Per ulteriori informazioni sulla gestione automatica della raccolta dei rifiuti, vedere Gestione automatica della memoria.
Tutte le matrici sono tipi riferimento, anche se i relativi elementi sono tipi valore. Le matrici derivano in modo implicito dalla System.Array classe . È possibile dichiararli e usarli usando la sintassi semplificata fornita da C#, come illustrato nell'esempio seguente:
// Declare and initialize an array of integers.
int[] nums = [1, 2, 3, 4, 5];
// Access an instance property of System.Array.
int len = nums.Length;
I tipi di riferimento supportano completamente l'ereditarietà. Quando si crea una classe, è possibile ereditare da qualsiasi altra interfaccia o classe non definita come sealed. Altre classi possono ereditare dalla tua classe ed eseguire l'override dei metodi virtuali. Per altre informazioni su come creare classi personalizzate, vedere Classi, struct e record. Per altre informazioni sull'ereditarietà e sui metodi virtuali, vedere Ereditarietà.
Tipi di valori letterali
In C# il compilatore assegna un tipo ai valori letterali. È possibile specificare come digitare un valore letterale numerico aggiungendo una lettera alla fine del numero. Ad esempio, per specificare che il valore 4.56 deve essere considerato come float, aggiungere un valore "f" o "F" dopo il numero: 4.56f. Se non si aggiunge una lettera, il compilatore deduce un tipo per il valore letterale. Per altre informazioni sui tipi che è possibile specificare con suffissi di lettera, vedere Tipi numerici integrali e tipi numerici a virgola mobile.
Poiché i valori letterali sono tipizzati e tutti i tipi derivano in definitiva da System.Object, è possibile scrivere e compilare codice come il seguente:
string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);
Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);
Tipi generici
Dichiarare un tipo con uno o più parametri di tipo che fungono da segnaposto per il tipo effettivo (il tipo concreto). Il codice client fornisce il tipo concreto quando crea un'istanza del tipo. Questi tipi sono denominati tipi generici. Ad esempio, il tipo .NET System.Collections.Generic.List<T> ha un parametro di tipo che per convenzione viene chiamato T. Quando si crea un'istanza del tipo, specificare il tipo degli oggetti contenuti nell'elenco, ad esempio string:
List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);
L'uso del parametro di tipo consente di riutilizzare la stessa classe per contenere qualsiasi tipo di elemento, senza dover convertire ogni elemento in oggetto. Le classi di raccolta generiche sono raccolte fortemente tipate perché il compilatore conosce il tipo specifico degli elementi della raccolta e può generare un errore in fase di compilazione se, ad esempio, si tenta di aggiungere un numero intero all'oggetto nell'esempio stringList precedente. Per altre informazioni, vedere Generics.
Tuple e tipi anonimi
La creazione di un tipo per semplici set di valori correlati può risultare scomoda se non si intende archiviare o passare questi valori usando le API pubbliche. A questo scopo, è possibile creare tuple o tipi anonimi . Per altre informazioni, vedere tuple e tipi anonimi.
Tipi valore nullable
I tipi di valore ordinari non possono avere un valore pari nulla . Tuttavia, è possibile creare tipi valore nullable aggiungendo un ? dopo il tipo. Ad esempio, int? è un int tipo che può avere anche il valore null. I tipi di valore nullable sono istanze del tipo struct generico System.Nullable<T>. I tipi valore nullable sono particolarmente utili quando si passano dati da e verso i database in cui i valori numerici possono essere null. Per altre informazioni, vedere Tipi di valore annullabile.
Dichiarazioni di tipo implicite
Digitare in modo implicito una variabile locale (ma non i membri della classe) usando la var parola chiave . La variabile riceve ancora un tipo in fase di compilazione, ma il compilatore fornisce il tipo . Per altre informazioni, vedere Variabili locali tipizzate in modo implicito.
Tipo in fase di compilazione e tipo di runtime
Una variabile può avere diversi tipi di compilazione e di runtime. Il tipo in fase di compilazione è il tipo dichiarato o dedotto della variabile nel codice sorgente. Il tipo di runtime è il tipo dell'istanza a cui fa riferimento tale variabile. Spesso questi due tipi sono uguali, come nell'esempio seguente:
string message = "This is a string of characters";
In altri casi, il tipo di compilazione è diverso, come illustrato negli esempi seguenti.
object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";
In entrambi gli esempi precedenti il tipo di runtime è .string Il tipo in fase di compilazione è object nella prima riga e IEnumerable<char> nella seconda riga.
Se i due tipi sono diversi per una variabile, è importante comprendere quando si applica il tipo in fase di compilazione e il tipo di runtime. Il tipo in fase di compilazione determina tutte le azioni eseguite dal compilatore. Queste azioni del compilatore includono la risoluzione delle chiamate al metodo, la risoluzione dell'overload e i cast impliciti ed espliciti disponibili. Il tipo di runtime determina tutte le azioni risolte in fase di esecuzione. Queste azioni in fase di esecuzione includono l'inoltro di chiamate di metodi virtuali, la valutazione delle espressioni is e switch, e altre API per il test dei tipi. Per comprendere meglio il modo in cui il codice interagisce con i tipi, riconoscere quale azione si applica a quale tipo.
Sezioni correlate
Per altre informazioni, vedere gli articoli seguenti:
Specificazione del linguaggio C#
Per altre informazioni, vedere la specifica del linguaggio C#. La specifica del linguaggio costituisce il riferimento ufficiale principale per la sintassi e l'uso di C#.