Enumerazioni, struct e tuple in C#
Nella prima serie abbiamo distinto tra tipi valore e tipi riferimento e abbiamo lavorato a lungo con le classi, che sono tipi riferimento. Esistono però altri costrutti, appartenenti in gran parte alla famiglia dei tipi valore, che permettono di modellare i dati in modo più preciso ed efficiente. In questo articolo esploriamo le enumerazioni per rappresentare insiemi finiti di valori, le struct per aggregare dati leggeri e le tuple per raggruppare valori senza definire un tipo dedicato.
Le enumerazioni
Un'enumerazione, dichiarata con la parola chiave enum, definisce un tipo i cui valori possibili sono un insieme finito di costanti con un nome. Le enumerazioni rendono il codice più leggibile e sicuro rispetto all'uso di numeri o stringhe "magiche", perché limitano i valori ammessi a quelli previsti.
// Un insieme finito di valori con un nome
enum DayOfWeek
{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
DayOfWeek today = DayOfWeek.Wednesday;
if (today == DayOfWeek.Saturday || today == DayOfWeek.Sunday)
{
Console.WriteLine("E il weekend");
}
else
{
Console.WriteLine("E un giorno feriale");
}
Internamente, ogni valore di un'enumerazione è associato a un numero intero: per impostazione predefinita si parte da zero e si incrementa di uno. È possibile assegnare esplicitamente i valori quando servono corrispondenze specifiche, ad esempio per allinearsi a codici esterni.
enum HttpStatus
{
Ok = 200,
NotFound = 404,
ServerError = 500
}
int code = (int)HttpStatus.NotFound; // vale 404
Enumerazioni come flag
A volte si desidera combinare più valori di un'enumerazione, ad esempio per rappresentare un insieme di permessi. L'attributo Flags e valori assegnati come potenze di due permettono di comporre e verificare combinazioni tramite gli operatori bit a bit.
[Flags]
enum Permissions
{
None = 0,
Read = 1,
Write = 2,
Execute = 4
}
// Combinazione di piu permessi con l'operatore OR
Permissions granted = Permissions.Read | Permissions.Write;
// Verifica della presenza di un permesso
bool canWrite = granted.HasFlag(Permissions.Write); // true
Le struct
Una struct è simile a una classe, ma è un tipo valore. Questo significa che viene copiata per valore quando la si assegna o la si passa a un metodo, e non memorizzata tramite riferimento. Le struct sono adatte a rappresentare piccoli aggregati di dati strettamente correlati, come un punto nello spazio o un colore, dove la semantica di copia è desiderabile e le dimensioni ridotte.
// Una struct che rappresenta un punto nel piano
struct Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y)
{
X = x;
Y = y;
}
public double DistanceFromOrigin() => Math.Sqrt(X * X + Y * Y);
}
var origin = new Point(0, 0);
var target = new Point(3, 4);
Console.WriteLine(target.DistanceFromOrigin()); // Stampa 5
Poiché le struct vengono copiate, occorre prestare attenzione: modificare una copia non altera l'originale. Questa è la stessa semantica dei tipi valore vista per gli interi.
var first = new Point(1, 1);
var second = first; // copia indipendente
second.X = 99;
Console.WriteLine(first.X); // Stampa ancora 1
Struct di sola lettura
Per garantire l'immutabilità e comunicare chiaramente l'intento, una struct può essere dichiarata readonly. In questo modo il compilatore impedisce qualsiasi modifica ai suoi campi dopo la costruzione, rendendo il tipo più sicuro e prevedibile.
// Struct immutabile: i valori non cambiano dopo la creazione
readonly struct Temperature
{
public double Celsius { get; }
public Temperature(double celsius)
{
Celsius = celsius;
}
public double Fahrenheit => Celsius * 9 / 5 + 32;
}
Quando usare struct o class
La scelta tra struct e class non è arbitraria. In generale, si preferisce una struct quando il tipo è piccolo, rappresenta un singolo valore logico, è immutabile e viene creato in gran quantità. Negli altri casi, in particolare quando il tipo ha uno stato mutabile o dimensioni significative, la classe resta la scelta predefinita.
Le tuple
Le tuple permettono di raggruppare al volo più valori in un'unica entità, senza dover definire un tipo apposito. Sono comode quando si vuole restituire più di un valore da un metodo o raggruppare temporaneamente dati correlati. Ogni elemento può essere nominato per migliorare la leggibilità.
// Una tupla con elementi nominati
(string Name, int Age) person = ("Gabriele", 40);
Console.WriteLine($"{person.Name} ha {person.Age} anni");
// Restituire piu valori da un metodo tramite una tupla
static (int Min, int Max) FindRange(int[] numbers)
{
return (numbers.Min(), numbers.Max());
}
var range = FindRange(new[] { 5, 2, 8, 1 });
Console.WriteLine($"Minimo {range.Min}, massimo {range.Max}");
Le tuple supportano anche la destrutturazione, che permette di assegnare i loro elementi a variabili separate in un'unica istruzione, rendendo il codice più conciso.
// Destrutturazione della tupla in due variabili
var (minimum, maximum) = FindRange(new[] { 10, 3, 7 });
Console.WriteLine(minimum); // 3
Console.WriteLine(maximum); // 10
Conclusione
Abbiamo ampliato la nostra conoscenza del sistema dei tipi con tre costrutti utili. Le enumerazioni rappresentano insiemi finiti di valori con un nome, anche combinabili come flag; le struct modellano piccoli aggregati di dati con semantica di copia, con la variante immutabile readonly; le tuple raggruppano al volo più valori e permettono di restituirli comodamente da un metodo, con il supporto alla destrutturazione. Nel prossimo e ultimo articolo di questa serie intermedia impareremo a leggere e scrivere file, interagendo con il mondo esterno tramite l'input e l'output.