Condividi tramite


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 la caratteristica (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 della libreria di classi e dei tipi definiti dall'utente che modellano i concetti specifici del dominio del problema del programma.

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

Si noti che gli sviluppatori C e C++ devono essere consapevoli 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;

I tipi di parametri del metodo e i valori restituiti vengono specificati 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, i valori possono essere convertiti in altri tipi, ad esempio quando vengono assegnati a nuove variabili o passati come argomenti del 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 ulteriori informazioni, consultare Conversioni di tipi e casting.

Tipi predefiniti

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 predefiniti string e object. Questi tipi sono disponibili per l'uso in qualsiasi programma C#. Per l'elenco completo dei tipi predefiniti, vedere Tipi predefiniti.

Tipi personalizzati

Usare i costrutti struct, class, interface, enume 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#. Altri diventano disponibili solo quando si aggiunge in modo esplicito un riferimento al progetto all'assembly che li definisce. Dopo che il compilatore ha un riferimento all'assembly, è possibile dichiarare variabili (e costanti) dei tipi dichiarati in tale assembly nel codice sorgente. Per altre informazioni, vedere Libreria di classi .NET.

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. C'è sovrapposizione nelle scelte. Nella maggior parte degli scenari, più di un'opzione è una scelta ragionevole.

  • Se le dimensioni di archiviazione dei dati sono ridotte, non più di 64 byte, scegliere un struct o record struct.
  • Se il tipo non è modificabile o si desidera una mutazione non distruttiva, scegliere struct o record struct.
  • Se il tipo deve avere una semantica di valore per l'uguaglianza, scegliere un tipo record class o record struct.
  • Se il tipo viene utilizzato principalmente per l'archiviazione dei dati, non per il comportamento, scegliere record class o record struct.
  • Se il tipo fa parte di una gerarchia di ereditarietà, scegli un record class o un class.
  • Se il tipo usa il polimorfismo, selezionare un elemento class.
  • Se lo scopo principale è la funzionalità, scegli un class.

Sistema comune di tipi

È importante comprendere due punti fondamentali sul sistema di tipi in .NET:

  • Supporta il principio dell'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, ad System.Int32 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 usando la struct parola chiave sono tipi valore. Tutti i tipi numerici predefiniti sono structs. I tipi definiti tramite la class parola chiave o record sono tipi di riferimento. I tipi riferimento e i tipi valore hanno regole in fase di compilazione diverse e un comportamento di runtime diverso.

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

Screenshot che mostra i tipi di valore CTS e i tipi di riferimento.

Annotazioni

È possibile notare che i tipi usati più di frequente sono tutti organizzati nel namespace 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 è 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 tipo di Person e ogni istanza può avere valori diversi nelle relative proprietà e campi.

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

Una struttura è un tipo di valore. Quando viene creato uno struct, la variabile a cui viene assegnato lo struct contiene i dati effettivi dello struct. Quando lo struct viene assegnato 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, le classi vengono usate per modellare un comportamento più complesso. Le classi archivia 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 generalmente archiviano dati che non devono essere modificati dopo la loro creazione. I tipi di record sono strutture di dati con membri sintetizzati del compilatore aggiuntivi. I record in genere archiviano 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 una struct viene allocata in linea nel contesto in cui la variabile è dichiarata. Non esiste un sovraccarico di allocazione dell'heap o garbage collection separato per le variabili di tipo valore. È possibile dichiarare record struct tipi come tipi di valore e includere i membri sintetizzati per i record.

Esistono due categorie di tipi di 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
{
    public int x, y;

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

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, delegate, matrice o interface è un oggetto reference type.

Quando si dichiara una variabile di tipo reference type, essa contiene il valore null fino a quando non viene assegnata un'istanza di quel tipo o finché non ne viene creata una utilizzando 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 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 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 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 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# i valori letterali ricevono un tipo dal compilatore. È 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 viene aggiunta alcuna 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 letterali sono tipizzati e tutti i tipi derivano infine 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 fungono da segnaposto per il tipo effettivo (il tipo concreto). Il codice client fornisce il tipo concreto quando crea un'istanza del tipo. Tali tipi sono denominati tipi generici. Ad esempio, il tipo .NET ha un parametro di System.Collections.Generic.List<T> tipo a cui per convenzione viene assegnato il nome T. Quando si crea un'istanza del tipo, si specifica il tipo degli oggetti contenuti nell'elenco, stringad esempio :

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 vengono chiamate 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.

Tipi impliciti, tipi anonimi e tipi di valore nullable

È possibile 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 tipo viene fornito dal compilatore. Per altre informazioni, vedere Variabili locali tipizzate in modo implicito.

Può essere scomodo creare un tipo denominato per semplici set di valori correlati che non si intende archiviare o superare i limiti dei metodi esterni. A questo scopo, è possibile creare tipi anonimi . Per altre informazioni, vedere Tipi anonimi.

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.

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 durante la 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.

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