Il pattern matching avanzato in C#
Abbiamo incontrato il pattern matching nella prima serie, usando l'operatore is e le switch expression per verificare tipi e valori. Da allora il linguaggio ha ampliato notevolmente questa funzionalità, trasformandola in uno strumento potente per esprimere logica complessa in modo dichiarativo e conciso. In questo articolo approfondiamo i vari tipi di pattern, dai pattern di proprietà a quelli relazionali, logici e di lista, mostrando come combinarli per scrivere codice più leggibile.
Ripasso dei pattern di base
I pattern più semplici verificano il tipo o un valore costante. Il pattern di tipo controlla la classe di un oggetto e ne assegna il valore convertito a una variabile, mentre il pattern costante confronta con un valore fisso, incluso null.
object value = 42;
// Pattern di tipo con assegnazione
if (value is int number)
{
Console.WriteLine($"Intero: {number}");
}
// Pattern costante
if (value is null)
{
Console.WriteLine("Valore nullo");
}
I pattern relazionali
I pattern relazionali usano gli operatori di confronto per verificare intervalli di valori. Sono particolarmente eleganti all'interno delle switch expression, dove sostituiscono catene di condizioni con una sintassi compatta.
static string Classify(int temperature) => temperature switch
{
< 0 => "gelo",
>= 0 and < 15 => "freddo",
>= 15 and < 25 => "mite",
_ => "caldo"
};
I pattern logici
Come si intravede nell'esempio precedente, i pattern possono essere combinati con gli operatori logici and, or e not. Questo permette di esprimere condizioni articolate in modo naturale e leggibile, spesso più chiaro dei corrispondenti operatori booleani tradizionali.
static bool IsVowel(char c) => c is 'a' or 'e' or 'i' or 'o' or 'u';
static bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
// Il pattern not e utile anche per la leggibilita
object value = GetValue();
if (value is not null)
{
Console.WriteLine("Valore presente");
}
I pattern di proprietà
I pattern di proprietà esaminano i valori delle proprietà di un oggetto. Consentono di verificare più caratteristiche in un'unica espressione, senza estrarre manualmente ogni proprietà. All'interno si possono annidare altri pattern, inclusi quelli relazionali.
class Order
{
public decimal Amount { get; set; }
public string Country { get; set; } = "";
public bool IsPriority { get; set; }
}
static decimal CalculateShipping(Order order) => order switch
{
// Verifica di piu proprieta contemporaneamente
{ Amount: > 100, Country: "IT" } => 0m,
{ IsPriority: true } => 15m,
{ Country: "IT" } => 5m,
_ => 10m
};
I pattern posizionali
Quando un tipo supporta la destrutturazione, tramite un metodo Deconstruct o perché è una tupla o un record posizionale, si possono usare i pattern posizionali. Questi scompongono l'oggetto nei suoi componenti e li confrontano posizionalmente, in modo simile a quanto avviene con le tuple viste nella serie intermedia.
record Point(int X, int Y);
static string Describe(Point point) => point switch
{
(0, 0) => "origine",
(var x, 0) => $"sull'asse X in {x}",
(0, var y) => $"sull'asse Y in {y}",
_ => "in un punto qualsiasi"
};
Nell'esempio, il pattern var cattura un valore in una variabile senza imporre condizioni su di esso, rendendolo disponibile nel ramo corrispondente.
I pattern di lista
Tra le aggiunte più recenti ci sono i pattern di lista, che verificano la struttura di array e collezioni. Permettono di controllare il numero di elementi e i loro valori, con il pattern di scarto _ per un singolo elemento qualsiasi e il pattern di intervallo .. per zero o più elementi.
static string Analyze(int[] numbers) => numbers switch
{
[] => "vuoto",
[var single] => $"un solo elemento: {single}",
[var first, .., var last] => $"da {first} a {last}",
_ => "altro"
};
Console.WriteLine(Analyze(new[] { 1, 2, 3, 4 })); // "da 1 a 4"
Le clausole di guardia
Quando un pattern da solo non basta, si può aggiungere una condizione con la clausola when, già incontrata con le eccezioni. Il ramo viene selezionato solo se il pattern corrisponde e la condizione è vera, combinando la struttura dei pattern con la flessibilità delle espressioni booleane.
static string Evaluate(Order order) => order switch
{
{ Amount: var a } when a > 1000 => "ordine importante",
{ Country: "IT" } when DateTime.Now.Hour < 12 => "spedizione mattutina",
_ => "ordine standard"
};
Combinare i pattern
La vera potenza emerge dalla combinazione dei diversi tipi di pattern in un'unica espressione. Si possono annidare pattern di proprietà, posizionali e relazionali per esprimere in poche righe una logica che altrimenti richiederebbe molti if annidati e difficili da seguire.
record Employee(string Name, string Department, decimal Salary);
static string Categorize(Employee employee) => employee switch
{
{ Department: "Engineering", Salary: > 80000 } => "ingegnere senior",
{ Department: "Sales", Salary: > 60000 } => "venditore esperto",
{ Salary: < 30000 } => "posizione entry level",
_ => "ruolo standard"
};
Conclusione
Abbiamo esplorato il pattern matching moderno di C# in tutta la sua ampiezza: i pattern relazionali per gli intervalli, quelli logici con and, or e not, i pattern di proprietà, posizionali e di lista, le clausole di guardia when e la loro combinazione in espressioni ricche. Usato con misura, il pattern matching rende il codice più dichiarativo e riduce la complessità visiva. Nel prossimo articolo affronteremo la concorrenza e il parallelismo, andando oltre l'asincronia per sfruttare appieno i processori multi-core.