Sistema di tipi C#

C# è un linguaggio fortemente tipizzato. Ogni variabile e costante ha un tipo, così come ogni espressione che restituisce un valore. Ogni dichiarazione di metodo specifica un nome, il tipo e il tipo (valore, riferimento o output) per ogni parametro di input e per il valore restituito. La libreria di classi .NET definisce tipi numerici predefiniti e tipi complessi che rappresentano un'ampia gamma di costrutti. Sono inclusi il file system, le connessioni di rete, le raccolte e le matrici di oggetti e date. Un tipico programma C# usa tipi dalla libreria di classi e tipi definiti dall'utente che modellano i concetti specifici del dominio relativo al problema del programma.

Le informazioni archiviate in un tipo possono includere gli elementi seguenti:

  • Lo spazio di archiviazione richiesto da una variabile del tipo.
  • I valori minimi e massimi che può rappresentare.
  • I membri (metodi, campi, eventi e così via) in esso contenuti.
  • Il tipo di base da cui eredita.
  • Interfacce che implementa.
  • I tipi di operazioni consentite.

Il compilatore usa le informazioni sul tipo per assicurarsi che tutte le operazioni eseguite nel codice siano indipendenti dai tipi. Se ad esempio si dichiara una variabile di tipo int, il compilatore consente di usare la variabile anche in operazioni di addizione e sottrazione. Se si prova a eseguire le 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;

Nota

Gli sviluppatori C e C++ devono tenere presente 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 ulteriormente l'indipendenza dai tipi quando alloca e recupera la memoria.

Specifica dei tipi nelle dichiarazioni di variabile

Quando si dichiara una variabile o una costante in un programma, è necessario specificarne il tipo oppure usare la parola chiave var per consentire al compilatore di dedurre il tipo. L'esempio seguente illustra alcune dichiarazioni di variabili che usano sia tipi numerici incorporati sia 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;

I tipi di parametri e valori restituiti del metodo sono specificati nella dichiarazione del metodo. La firma seguente illustra 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. Non è possibile, ad esempio, dichiarare una variabile int e assegnare ad essa il valore booleano true. I valori possono tuttavia essere convertiti in altri tipi, ad esempio quando vengono assegnati a nuove variabili o passati come argomenti di metodo. Una conversione del tipo che non causa la perdita di dati viene eseguita automaticamente dal compilatore. Una conversione che potrebbe causare la perdita di dati richiede un cast nel codice sorgente.

Per altre informazioni, vedere Cast e conversioni di tipi.

Tipi incorporati

C# offre un set standard di tipi predefiniti. Rappresentano numeri interi, valori a virgola mobile, espressioni booleane, caratteri di testo, valori decimali e altri tipi di dati. Sono anche disponibili tipi string e object incorporati, Questi tipi possono essere usati in qualsiasi programma C#. Per l'elenco completo dei tipi incorporati, vedere Tipi incorporati.

Tipi personalizzati

Usare i costrutti struct, class, interface, enume record per creare tipi personalizzati. La libreria di classi .NET stessa è una raccolta di tipi personalizzati che è possibile usare nelle proprie applicazioni. Per impostazione predefinita, i tipi più comunemente usati nella libreria di classi sono disponibili in qualsiasi programma C#, Mentre altri diventano disponibili solo quando si aggiunge in modo esplicito un riferimento di progetto all'assembly che li definisce. Dopo che il compilatore avrà un riferimento all'assembly, è possibile dichiarare variabili (e costanti) dei tipi dichiarati in tale assembly in codice sorgente. Per altre informazioni, vedere Libreria di classi .NET.

Common Type System

È importante tenere presente due aspetti fondamentali del sistema dei tipi in .NET:

  • Supporta il principio di ereditarietà. I tipi possono derivare da altri tipi, denominati tipi di base. Il tipo derivato eredita (con alcune limitazioni) metodi, proprietà e altri membri del tipo di base, che a sua volta può derivare da un altro tipo. In questo 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, ad esempio System.Int32 (parola chiave C#: int), derivano in definitiva da un unico tipo di base, ovvero System.Object (parola chiave C#: object). Questa gerarchia di tipi unificata prende il nome di Common Type System (CTS). Per altre informazioni sull'ereditarietà in C#, vedere Ereditarietà.
  • Nel CTS ogni tipo è definito come tipo valore o tipo riferimento. In questi tipi sono inclusi anche tutti i tipi personalizzati nella libreria di classi .NET e i tipi definiti dall'utente. I tipi definiti tramite la parola chiave struct sono tipi valore e tutti i tipi numerici incorporati sono tipi structs. I tipi definiti tramite la parola chiave class o record sono tipi riferimento. I tipi di riferimento e i tipi di valore hanno regole diverse e un comportamento diverso in fase di esecuzione.

La figura seguente illustra la relazione tra tipi valore e tipi riferimento nel CTS.

Screenshot that shows CTS value types and reference types.

Nota

È possibile osservare come i tipi usati con maggiore frequenza siano tutti organizzati nello spazio dei nomi System. L'inserimento di un tipo in uno spazio dei nomi, tuttavia, è indipendente dalla categoria a cui appartiene il tipo.

Classi e struct sono due dei costrutti di base del Common Type System in .NET. Ognuno di essi è costituito essenzialmente da una struttura di dati che incapsula un set di dati e comportamenti che formano insieme un'unità logica. I dati e i comportamenti sono i membri della classe, dello struct o del record. I membri includono i relativi metodi, le relative proprietà, i relativi eventi e così via, come indicato più avanti in questo articolo.

Una dichiarazione di classe, struct o record è come un progetto iniziale usato per creare istanze o oggetti in fase di esecuzione. Se si definisce una classe, uno struct o un record chiamato Person, Person è il nome del tipo. Se si dichiara e inizializza una variabile p di tipo Person, p è definito oggetto o istanza di Person. È possibile creare più istanze dello stesso tipo Person e nelle proprietà e nei campi di ogni istanza possono essere specificati valori diversi.

Una classe è un tipo riferimento. Quando viene creato un oggetto del tipo, la variabile a cui è assegnato l'oggetto contiene solo un riferimento alla memoria. Se il riferimento all'oggetto viene assegnato a una nuova variabile, questa fa riferimento all'oggetto originale. Le modifiche apportate tramite una variabile vengono riflesse nell'altra variabile perché entrambe fanno riferimento agli stessi dati.

Un tipo struct è un tipo valore. Quando viene creato un tipo struct, la variabile a cui è assegnato questo tipo ne contiene i dati effettivi. Quando viene assegnato a una nuova variabile, il tipo struct viene copiato. La nuova variabile e quella originale contengono quindi due copie separate degli stessi dati. Eventuali 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, le classi vengono usate per modellare un comportamento più complesso. Le classi archiviano in genere i dati che devono essere modificati dopo la creazione di un oggetto classe. Gli struct sono più adatti per strutture di dati di piccole dimensioni. Gli struct archiviano in genere i dati che non devono essere modificati dopo la creazione dello struct. I tipi di record sono strutture di dati con membri sintetizzati del compilatore aggiuntivi. I record archiviano in genere i dati che non devono essere 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 uno struct viene allocata inline in qualsiasi contesto dichiarato dalla variabile. Non esiste un'allocazione heap o un overhead di Garbage Collection separato per le variabili dei tipi valore. È possibile dichiarare record struct tipi che sono tipi di valore e includere i membri sintetizzati per record.

Esistono due categorie di tipi valore: struct e enum.

I tipi numerici incorporati sono struct i cui metodi e i cui campi sono accessibili dall'utente:

// constant field on type byte.
byte b = byte.MaxValue;

Ad essi, tuttavia, si dichiarano e si assegnano valori come se fossero tipi non aggregati semplici:

byte num = 0xA;
int i = 5;
char c = 'Z';

I tipi valore sono sealed. 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. Un tipo struct può tuttavia implementare una o più interfacce. È possibile eseguire il cast di un tipo di struct a qualsiasi tipo di interfaccia implementato. Questo cast causa una conversione boxing con cui si esegue il wrapping dello struct in un oggetto tipo riferimento sull'heap gestito. Le operazioni di conversione boxing si verificano quando si passa un tipo valore a un metodo che accetta System.Object o qualsiasi tipo di interfaccia come parametro di input. Per altre informazioni, vedere Boxing e unboxing.

Usare la parola chiave struct per creare tipi 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
{
    public int x, y;

    public Coords(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

Per altre informazioni sui tipi struct, vedere Tipi di struttura. Per altre informazioni sui tipi valore in, vedere Tipi di valore.

L'altra categoria di tipi valore è enum. Un tipo enum definisce un set di costanti integrali denominate. L'enumerazione System.IO.FileMode nella libreria di classi .NET, ad esempio, contiene un set di valori interi costanti e denominati che specificano come deve essere aperto un file. L'enumerazione deve essere definita come illustrato nell'esempio seguente:

public enum FileMode
{
    CreateNew = 1,
    Create = 2,
    Open = 3,
    OpenOrCreate = 4,
    Truncate = 5,
    Append = 6,
}

Il valore della costante System.IO.FileMode.Create è 2. I nomi, tuttavia, sono molto più significativi per gli utenti che leggono il codice sorgente e, quindi, è preferibile usare enumerazioni anziché valori letterali numerici costanti. Per ulteriori informazioni, vedere System.IO.FileMode.

Tutte le enumerazioni ereditano da System.Enum, che eredita da System.ValueType. Tutte le regole valide per i tipi struct sono valide anche per le enumerazioni. Per altre informazioni sulle enumerazioni, vedere Tipi di enumerazione.

Tipi di riferimento

Un tipo definito come class, record, delegate, matrice o interface è un reference type.

Quando si dichiara una variabile di un reference type, contiene il valore null fino a quando non viene assegnato con un'istanza di tale tipo o se ne crea una usando l'operatore new. La creazione e l'assegnazione di una classe sono illustrate nell'esempio seguente:

MyClass myClass = new MyClass();
MyClass myClass2 = myClass;

Non è possibile creare direttamente un'istanza di un 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 MyClass();

// 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 l'oggetto viene creato, la memoria viene allocata nell'heap gestito. La variabile contiene solo un riferimento alla posizione dell'oggetto. I tipi nell'heap gestito richiedono un sovraccarico sia quando vengono allocati sia quando vengono recuperati. Garbage Collection è la funzionalità di gestione automatica della memoria di CLR, che esegue il recupero. La Garbage Collection, tuttavia, è anche altamente ottimizzata e, nella maggior parte degli scenari, non genera un problema di prestazioni. Per altre informazioni sulla Garbage Collection, 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 classe System.Array. Vengono dichiarate e usate con 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 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 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# i valori letterali ricevono un tipo dal compilatore. È possibile specificare come deve essere tipizzato un valore letterale numerico aggiungendo una lettera alla fine del numero. Per specificare, ad esempio, che il valore 4.56 deve essere considerato come un tipo floatfloat, aggiungere una "f" o una "F" dopo il numero: 4.56f. Se non viene aggiunta alcuna lettera, il compilatore dedurrà un tipo per il valore letterale. Per altre informazioni sui tipi che possono essere specificati con suffissi letterali, vedere Tipi numerici integrali e Tipi numerici a virgola mobile.

Poiché i valori letterali sono tipizzati e tutti i tipi derivano in ultima istanza 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

Un tipo può essere dichiarato con uno o più parametri di tipo che agiscono da segnaposto per il tipo effettivo (tipo concreto). Il codice client fornisce il tipo concreto quando crea un'istanza del tipo. Questi tipi sono definiti tipi generici. Ad esempio, il tipo System.Collections.Generic.List<T> di .NET ha un solo parametro a cui, per convenzione, viene assegnato il nome T. Quando si crea un'istanza del tipo, si specifica il tipo degli oggetti che saranno 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 rende possibile il riuso della stessa classe per contenere qualsiasi tipo di elemento, senza la necessità di convertire ogni elemento in object. Le classi di raccolte generiche sono definite raccolte fortemente tipizzate perché il compilatore conosce il tipo specifico degli elementi della raccolta e può generare un errore in fase di compilazione se, ad esempio, si prova ad aggiungere un numero intero all'oggetto stringList nell'esempio precedente. Per altre informazioni, vedere Generics.

Tipi impliciti, tipi anonimi e tipi valore nullable

È possibile tipizzare una variabile locale (ma non membri di classe) in modo implicito usando la parola chiave var. Alla variabile viene comunque assegnato un tipo in fase di compilazione, specificato dal compilatore. Per altre informazioni, vedere Variabili locali tipizzate in modo implicito.

Può non essere consigliabile creare un tipo denominato per set semplici di valori correlati che non si intende archiviare o passare fuori dai limiti del metodo. A questo scopo è possibile creare tipi anonimi. Per altre informazioni, vedere Tipi anonimi.

I tipi valore comuni non possono avere un valore null. È tuttavia possibile creare tipi valore nullable aggiungendo un ? dopo il tipo. Ad esempio, int? è un tipo int che può avere anche il valore null. I tipi valore nullable sono istanze del tipo di struct generico System.Nullable<T>. I tipi di valore nullable sono particolarmente utili quando si passano dati da e verso database in cui possono essere presenti valori numerici null. Per altre informazioni, vedere Tipi valore nullable.

Tipo in fase di compilazione e tipo di runtime

Una variabile può avere diversi tipi di runtime e in fase di compilazione. 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 in fase 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 è un string Il tipo in fase di compilazione si trova object nella prima riga e IEnumerable<char> nel secondo.

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'invio di chiamate di metodi virtuali, la valutazione di is e switch espressioni e altre API di test dei tipi. Per comprendere meglio il modo in cui il codice interagisce con i tipi, riconoscere quale azione si applica a quale tipo.

Per altre informazioni, vedere gli articoli seguenti:

Specifiche 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#.