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, un numero di parametri e 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 un set di tipi numerici predefiniti e tipi più complessi che rappresentano un'ampia gamma di costrutti logici, ad esempio file system, connessioni di rete, raccolte e matrici di oggetti e date. Un tipico programma C# usa tipi della libreria di classi e tipi definiti dall'utente che modellano i concetti specifici del dominio problematico 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 implementate.
  • I tipi di operazioni consentite.

Il compilatore usa le informazioni sul tipo per assicurarsi che tutte le operazioni eseguite nel codice siano indipendente dai tipi. Ad esempio, se si dichiara una variabile di tipo , il compilatore consente di usare la variabile in operazioni di int addizione e sottrazione. Se si tenta di eseguire le stesse operazioni su una variabile di tipo , il compilatore genera un errore, come bool 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++, si noti che in C# bool non è convertibile in int .

Il compilatore incorpora le informazioni sul tipo nel file eseguibile come metadati. Il Common Language Runtime (CLR) usa i metadati in fase di esecuzione per garantire una maggiore 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 o usare la parola chiave per consentire al var 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 del metodo e i valori restituiti sono specificati nella dichiarazione del metodo. La firma seguente illustra un metodo che richiede come int 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 dichiararla di nuovo con un nuovo tipo e non è possibile assegnare un valore non compatibile con il tipo dichiarato. Ad esempio, non è possibile dichiarare un oggetto e quindi assegnare a esso int un valore booleano . true 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. mentre una conversione che può causare la perdita di dati richiede un cast nel codice sorgente.

Per altre informazioni, vedere Cast e conversioni di tipi.

Tipi incorporati

C# fornisce un set standard di tipi predefiniti per rappresentare 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 sono disponibili per l'uso in qualsiasi programma C#. Per l'elenco completo dei tipi predefiniti, vedere Tipi predefiniti.

Tipi personalizzati

Usare i struct class costrutti , interface , , e per creare tipi enum record 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#, Altri diventano disponibili solo quando si aggiunge in modo esplicito un riferimento di progetto all'assembly in cui sono definiti. Nel momento in cui il compilatore ha un riferimento all'assembly, è possibile dichiarare variabili (e costanti) dei tipi dichiarati nell'assembly in codice sorgente. Per altre informazioni, vedere Libreria di classi .NET.

Sistema di tipi comune

È importante comprendere due punti fondamentali sul 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 (parola chiave C#: ), derivano in definitiva da un singolo tipo di base, ovvero System.Int32 int 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. Questi tipi includono tutti i tipi personalizzati nella libreria di classi .NET e anche i tipi definiti dall'utente. I tipi definiti tramite la parola chiave sono tipi valore. Tutti i struct tipi numerici predefiniti sono 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 che illustra i tipi valore e i tipi riferimento nel CTS.

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.

Le classi e gli struct sono due dei costrutti di base del sistema di tipi comune in .NET. C# 9 aggiunge record, che sono un tipo di classe. 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 membri della classe, dello struct o del record e includono i relativi metodi, proprietà, eventi e così via, come elencato 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 , è il nome del Person 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 o un record è un tipo riferimento. Quando viene creato un oggetto del tipo, la variabile a cui è assegnato l'oggetto contiene solo un riferimento a tale 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 lo struct viene assegnato a una nuova variabile, viene copiato. La nuova variabile e quella originale contengono quindi due copie separate degli stessi dati. Le modifiche apportate a una copia non influiscono sull'altra copia.

In generale, le classi vengono usate per modellare un comportamento più complesso o dati destinati a essere modificati dopo la creazione di un oggetto di classe. Gli struct sono più adatti per le strutture di dati di piccole dimensioni che contengono principalmente dati che non devono essere modificati dopo la creazione dello struct. I tipi di record sono destinati a strutture di dati più grandi che contengono principalmente dati che non devono essere modificati dopo la creazione dell'oggetto.

Tipi valore

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 dei tipi valore contengono direttamente i rispettivi valori, ovvero la memoria viene allocata inline nel contesto in cui è dichiarata la variabile. Non esiste un'allocazione heap separata o un sovraccarico di Garbage Collection per le variabili di tipo valore.

Esistono due categorie di tipi di valore: struct e enum .

I tipi numerici predefiniti sono struct e hanno 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 sealed, il che significa che non è possibile derivare un tipo da qualsiasi tipo valore, ad esempio System.Int32 . Non è possibile definire uno struct per ereditare da qualsiasi classe o struct definita 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 struct a qualsiasi tipo di interfaccia implementato; Questo cast fa sì che un'operazione di boxing eseghi il wrapping dello struct all'interno di un oggetto tipo riferimento nell'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, un tipo 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 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. Viene definito come illustrato nell'esempio seguente:

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

Il System.IO.FileMode.Create valore della costante è 2. Tuttavia, il nome è molto più significativo per gli utenti che leggono il codice sorgente e per questo motivo è meglio 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 valide per i tipi struct sono valide anche per le enumerazioni. Per altre informazioni sulle enumerazioni, vedere Tipi di enumerazione.

Tipi riferimento

Tipo definito come , class record , delegate , matrice o è interface un tipo riferimento. In fase di esecuzione, quando si dichiara una variabile di un tipo riferimento, la variabile contiene il valore fino a quando non si crea in modo esplicito un oggetto usando l'operatore o non si assegna un oggetto creato altrove tramite , come illustrato null new nell'esempio new seguente:

MyClass mc = new MyClass();
MyClass mc2 = mc;

Un'interfaccia deve essere inizializzata insieme a un oggetto classe che la implementa. Se MyClass implementa IMyInterface, si crea un'istanza di IMyInterface, come illustrato nell'esempio seguente:

IMyInterface iface = new MyClass();

Quando viene creato l'oggetto, la memoria viene allocata nell'heap gestito e la variabile mantiene solo un riferimento al percorso dell'oggetto. I tipi nell'heap gestito richiedono sovraccarico sia quando vengono allocati che quando vengono recuperati dalla funzionalità di gestione automatica della memoria di CLR, nota come Garbage Collection. Tuttavia, anche l'operazione di Garbage Collection è altamente ottimizzata e nella maggior parte degli scenari non crea 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, ma 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 sealede 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. Ad esempio, per specificare che il valore deve essere considerato come , aggiungere 4.56 float "f" o "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 è possibile specificare con suffissi di lettera, vedere Tipi numerici integrali e tipi numerici a virgola mobile.

Poiché i valori letterali sono tipi e tutti i tipi derivano in ultima analisi da , è possibile scrivere e compilare codice System.Object come il codice 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) che il codice client specifica quando si crea un'istanza del tipo. Questi tipi sono definiti tipi generici. Ad esempio, il tipo .NET ha un parametro di tipo a cui per System.Collections.Generic.List<T> convenzione viene assegnato il nome T . Quando si crea un'istanza del tipo, si specifica il tipo di oggetti che l'elenco conterrà, 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 object. Le classi di raccolte generiche vengono chiamate 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 tenta di aggiungere un integer all'oggetto nell'esempio stringList precedente. Per altre informazioni, vedere Generics.

Tipi impliciti, tipi anonimi e tipi valore nullable

È possibile digitare in modo implicito una variabile locale (ma non i membri di classe) usando la parola var chiave . 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ò essere poco pratico creare un tipo denominato per set semplici di valori correlati che non si intende archiviare o passare all'esterno dei limiti del metodo. A questo scopo è possibile creare tipi anonimi. Per altre informazioni, vedere Tipi anonimi.

I tipi valore ordinari non possono avere un null valore . È tuttavia possibile creare tipi valore nullable aggiungendo dopo ? il tipo . Ad esempio, int? è un tipo che può avere anche il valore int null . I tipi 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 database in cui i valori numerici possono essere null . Per altre informazioni, vedere Tipi valore nullable.

Tipo in fase di compilazione e tipo di run-time

Una variabile può avere tipi diversi in fase di compilazione e in fase di esecuzione. Il tipo in fase di compilazione è il tipo dichiarato o dedotto della variabile nel codice sorgente. Il tipo di run-time è il tipo dell'istanza a cui fa riferimento tale variabile. Spesso questi due tipi sono gli stessi, come nell'esempio seguente:

string message = "This is a string of characters";

In altri casi, il tipo in fase di compilazione è diverso, come illustrato nei due esempi seguenti:

object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";

In entrambi gli esempi precedenti il tipo di run-time è string . Il tipo in fase di object compilazione si trova nella prima riga e nella IEnumerable<char> seconda.

Se i due tipi sono diversi per una variabile, è importante comprendere quando vengono applicati il tipo in fase di compilazione e il tipo di run-time. Il tipo in fase di compilazione determina tutte le azioni eseguite dal compilatore. Queste azioni del compilatore includono la risoluzione delle chiamate ai metodi, la risoluzione dell'overload e i cast impliciti ed espliciti disponibili. Il tipo di run-time determina tutte le azioni risolte in fase di esecuzione. Queste azioni di run-time includono l'invio di chiamate di metodi virtuali, la valutazione e le espressioni e is altre API di test dei switch 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 ulteriori informazioni, vedere la specifica del linguaggio C#. La specifica del linguaggio costituisce il riferimento ufficiale principale per la sintassi e l'uso di C#.