Esaminare il sistema di tipi .NET
C# è un linguaggio fortemente tipizzato. Ogni variabile e costante ha un tipo, così come ogni espressione che restituisce un valore. 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.
Tipi predefiniti
C# offre un set standard di tipi predefiniti. Questi tipi standard rappresentano numeri interi, valori a virgola mobile, espressioni booleane, caratteri di testo, valori decimali e altri tipi di dati. Sono disponibili anche tipi stringa e oggetto predefiniti. Questi tipi sono disponibili per l'uso in qualsiasi programma C#.
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.
Sistema di tipi comune
È 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 esempio System.Int32 (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).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 parola chiave struct sono tipi valore; tutti i tipi numerici predefiniti sono struct. I tipi definiti tramite la classe o la parola chiave 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.
Le classi e le strutture 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 del class, structo record. I membri di una classe includono proprietà (dati), metodi (comportamenti), campi (variabili dichiarate all'interno della classe) e molte altre.
Una dichiarazione class, structo record è simile a un progetto usato per creare istanze o oggetti in fase di esecuzione. Se si definisce una classe denominata 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.
Un struct è un tipo di valore. Quando viene creato un struct, la variabile a cui viene assegnato il struct contiene i dati effettivi del struct. Quando il struct viene assegnato a una nuova variabile, il valore dei dati 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. Le strutture sono più adatte per strutture di dati di piccole dimensioni. Le strutture in genere archiviano i dati che non devono essere modificati dopo la creazione del struct. I tipi di record sono strutture di dati con altri membri sintetizzati dal compilatore. I record in genere archiviano i 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 Common Language Runtime. Le variabili di tipo valore contengono direttamente i relativi valori. La memoria per un struct viene allocata inline in qualsiasi contesto dichiarato dalla variabile. Non esiste un sovraccarico di allocazione dell'heap o garbage collection separato per le variabili di tipo valore. È possibile dichiarare i tipi di struct di record che sono tipi valore e includere i membri sintetizzati per i record.
Esistono due categorie di tipi valore: struct ed enumerazione.
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 un struct da ereditare da qualsiasi class o struct definito dall'utente perché un struct può ereditare solo da System.ValueType.
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;
}
}
L'altra categoria di tipi valore è enum. Un enum 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. La sintassi per un enum è illustrata 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.
Tutti i enums ereditano da System.Enum, che eredita da System.ValueType. Tutte le regole applicabili a structs si applicano anche a enums.
Tipi di riferimento
I tipi class, record, delegate, arraye interface sono tipi di riferimento .
Quando si dichiara una variabile di un tipo riferimento, contiene il valore null fino a quando non viene assegnato con un'istanza di tale tipo o crearne una usando l'operatore new.
Nell'esempio seguente viene illustrato come dichiarare variabili di tipo riferimento usando matrici:
// Declaring an array variable
int[] numbers;
// Initializing the array with a size of 5
numbers = new int[5];
// Alternatively, declaring and initializing an array in one line
int[] numbers2 = new int[] { 1, 2, 3, 4, 5 };
// Assigning a reference to another variable
int[] numbers3 = numbers2;
L'operatore new crea un'istanza del tipo e restituisce un riferimento a tale istanza. Il riferimento è l'indirizzo di memoria dell'oggetto e tale riferimento viene archiviato nella variabile . Quando si assegna una variabile di tipo riferimento a un'altra variabile, si copia il riferimento, non l'oggetto stesso. Entrambe le variabili fanno riferimento allo stesso oggetto in memoria.
Nota
Oltre a essere tipi di riferimento, le matrici sono raccolte. Le raccolte possono essere inizializzate usando espressioni di raccolta, eliminando così il requisito di includere la parola chiave new durante la dichiarazione e l'inizializzazione di una matrice in una riga. Ad esempio: int[] numbers = [ 1, 2, 3, 4, 5 ];.
È possibile creare un'istanza di una classe usando la stessa sintassi usata per creare un'istanza dei tipi predefiniti. Nell'esempio seguente viene illustrato come creare un'istanza di una classe :
MyClass myClass = new MyClass();
MyClass myClass2 = myClass;
L'operatore new crea un'istanza della classe e restituisce un riferimento a tale istanza. Il riferimento è l'indirizzo di memoria dell'oggetto e tale riferimento viene archiviato nella variabile . Quando si assegna una variabile di tipo riferimento a un'altra variabile, si copia il riferimento, non l'oggetto stesso. Entrambe le variabili fanno riferimento allo stesso oggetto in memoria.