Pattern Matching Avanzato in C#

Il pattern matching avanzato in C# rappresenta una delle evoluzioni più significative del linguaggio negli ultimi anni. Introdotto progressivamente dalle versioni 7.0 in poi, il pattern matching consente di scrivere codice più espressivo, sicuro e conciso per l’analisi e la decostruzione di dati complessi, trasformando il modo in cui gestiamo la logica condizionale e l’analisi dei tipi.
Evoluzione del Pattern Matching
Da C# 7.0 a C# 11.0
Il pattern matching in C# è evoluto attraverso diverse versioni:
- C# 7.0: Type patterns, var patterns
- C# 8.0: Property patterns, tuple patterns, positional patterns
- C# 9.0: Relational patterns, logical patterns
- C# 10.0: Extended property patterns
- C# 11.0: List patterns, slice patterns
Type Patterns e Cast Patterns
Pattern di Tipo Base
object obj = "Hello World";
// Type pattern classico
if (obj is string str)
{
Console.WriteLine($"Stringa: {str.ToUpper()}");
}
// Switch expression con type patterns
string ProcessValue(object value) => value switch
{
int i => $"Numero intero: {i}",
double d => $"Numero decimale: {d:F2}",
string s when s.Length > 10 => $"Stringa lunga: {s.Substring(0, 10)}...",
string s => $"Stringa: {s}",
bool b => $"Booleano: {b}",
null => "Valore nullo",
_ => "Tipo non riconosciuto"
};
Patterns con Gerarchie di Tipi
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
public record Triangle(double Base, double Height) : Shape;
public static double CalculateArea(Shape shape) => shape switch
{
Circle(var radius) => Math.PI * radius * radius,
Rectangle(var width, var height) => width * height,
Triangle(var baseLength, var height) => 0.5 * baseLength * height,
_ => throw new ArgumentException("Forma non supportata")
};
public static string DescribeShape(Shape shape) => shape switch
{
Circle { Radius: > 10 } => "Cerchio grande",
Circle { Radius: <= 1 } => "Cerchio piccolo",
Circle => "Cerchio medio",
Rectangle { Width: var w, Height: var h } when w == h => "Quadrato",
Rectangle { Width: > Height } => "Rettangolo orizzontale",
Rectangle => "Rettangolo verticale",
Triangle { Base: > 5, Height: > 5 } => "Triangolo grande",
_ => "Forma generica"
};
Property Patterns
Property Patterns Semplici
public record Person(string Name, int Age, string City, bool IsStudent);
public static string ClassifyPerson(Person person) => person switch
{
{ Age: < 18 } => "Minorenne",
{ Age: >= 18, Age: < 65, IsStudent: true } => "Studente adulto",
{ Age: >= 65 } => "Anziano",
{ City: "Milano", Age: > 25 } => "Milanese adulto",
{ Name: "Mario" } => "Mario",
_ => "Persona generica"
};
// Nested property patterns
public record Address(string Street, string City, string Country);
public record Customer(string Name, Address Address, bool IsPremium);
public static decimal GetShippingCost(Customer customer) => customer switch
{
{ Address.Country: "Italy", IsPremium: true } => 0m,
{ Address.Country: "Italy" } => 5m,
{ Address.Country: "Germany" or "France" } => 15m,
{ Address: { Country: "USA", City: "New York" } } => 25m,
_ => 50m
};
Extended Property Patterns (C# 10.0+)
public record Order(Customer Customer, List<OrderItem> Items, DateTime Date);
public record OrderItem(string Product, int Quantity, decimal Price);
public static string AnalyzeOrder(Order order) => order switch
{
// Extended property pattern
{ Customer.Address.Country: "Italy", Items.Count: > 5 } => "Ordine italiano grande",
{ Items[0].Product: "iPhone", Items[0].Quantity: > 1 } => "Ordine iPhone multiplo",
{ Customer.IsPremium: true, Items: [{ Price: > 1000 }] } => "Ordine premium costoso",
_ => "Ordine standard"
};
Tuple Patterns
Decostruzione di Tuple
public static string AnalyzeCoordinate((double x, double y) point) => point switch
{
(0, 0) => "Origine",
(0, _) => "Asse Y",
(_, 0) => "Asse X",
(var x, var y) when x == y => "Diagonale principale",
(var x, var y) when x == -y => "Diagonale secondaria",
(> 0, > 0) => "Primo quadrante",
(< 0, > 0) => "Secondo quadrante",
(< 0, < 0) => "Terzo quadrante",
(> 0, < 0) => "Quarto quadrante"
};
// Multiple tuple patterns
public static string GameResult((int player1, int player2, bool overtime) game) => game switch
{
(var p1, var p2, false) when p1 > p2 => "Player 1 vince in tempo regolare",
(var p1, var p2, false) when p2 > p1 => "Player 2 vince in tempo regolare",
(var p1, var p2, false) when p1 == p2 => "Pareggio",
(var p1, var p2, true) when p1 > p2 => "Player 1 vince ai supplementari",
(var p1, var p2, true) when p2 > p1 => "Player 2 vince ai supplementari",
_ => "Risultato non valido"
};
Relational e Logical Patterns (C# 9.0+)
Relational Patterns
public static string GetTemperatureDescription(int celsius) => celsius switch
{
< -20 => "Gelido",
>= -20 and < 0 => "Molto freddo",
>= 0 and < 10 => "Freddo",
>= 10 and < 20 => "Fresco",
>= 20 and < 30 => "Mite",
>= 30 and < 40 => "Caldo",
>= 40 => "Molto caldo"
};
public static bool IsValidScore(int score) => score is >= 0 and <= 100;
public static string CategorizeAge(int age) => age switch
{
< 0 => "Età non valida",
>= 0 and < 13 => "Bambino",
>= 13 and < 20 => "Adolescente",
>= 20 and < 65 => "Adulto",
>= 65 and < 100 => "Anziano",
>= 100 => "Centenario",
_ => "Età estrema"
};
Logical Patterns
// Negation pattern
public static bool IsNotWeekend(DayOfWeek day) => day is not (DayOfWeek.Saturday or DayOfWeek.Sunday);
// Complex logical patterns
public static string ValidateInput(object input) => input switch
{
int i when i is > 0 and < 100 => "Numero valido",
string s when s is not null and not "" => "Stringa valida",
bool b => $"Booleano: {b}",
not null => "Oggetto non null ma tipo non supportato",
null => "Valore null"
};
// Parenthesized patterns
public static decimal CalculateDiscount(decimal price, bool isPremium, int quantity) =>
(price, isPremium, quantity) switch
{
(> 1000, true, _) => 0.2m,
(> 500, _, > 10) => 0.15m,
(_, true, _) => 0.1m,
(> 100, _, _) => 0.05m,
_ => 0m
};
List Patterns (C# 11.0+)
Pattern Matching su Collezioni
public static string AnalyzeNumbers(int[] numbers) => numbers switch
{
[] => "Array vuoto",
[var single] => $"Un elemento: {single}",
[var first, var second] => $"Due elementi: {first}, {second}",
[var first, .., var last] => $"Primo: {first}, Ultimo: {last}",
[1, 2, 3] => "Sequenza 1, 2, 3",
[1, .. var middle, 5] => $"Inizia con 1, finisce con 5, mezzo: [{string.Join(", ", middle)}]",
[> 0, > 0, > 0] when numbers.Length == 3 => "Tre numeri positivi",
_ => "Pattern non riconosciuto"
};
// Slice patterns con liste
public static string ProcessList(List<string> items) => items switch
{
["start", .. var middle, "end"] => $"Lista completa con {middle.Count} elementi nel mezzo",
["urgent", .. var rest] => $"Task urgente con {rest.Count} altri task",
[.. var all] when all.All(s => s.Length > 5) => "Tutti elementi lunghi",
_ => "Lista normale"
};
Positional Patterns e Decostruzione
Decostruzione Personalizzata
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public Point(double x, double y) => (X, Y) = (x, y);
// Metodo di decostruzione
public void Deconstruct(out double x, out double y) => (x, y) = (X, Y);
// Decostruzione alternativa
public void Deconstruct(out double distance, out double angle)
{
distance = Math.Sqrt(X * X + Y * Y);
angle = Math.Atan2(Y, X);
}
}
public static string AnalyzePoint(Point point) => point switch
{
(0, 0) => "Origine",
(var x, 0) => $"Sull'asse X a {x}",
(0, var y) => $"Sull'asse Y a {y}",
(var x, var y) when x == y => $"Diagonale a ({x}, {y})",
var (x, y) when Math.Abs(x) == Math.Abs(y) => "Diagonale secondaria",
(> 0, > 0) => "Primo quadrante",
_ => "Altro quadrante"
};
Applicazioni Pratiche Avanzate
State Machine con Pattern Matching
public abstract record State;
public record Idle : State;
public record Loading(string Resource) : State;
public record Ready(object Data) : State;
public record Error(string Message, Exception? Exception = null) : State;
public class StateMachine
{
public State CurrentState { get; private set; } = new Idle();
public string GetStatusMessage() => CurrentState switch
{
Idle => "Sistema inattivo",
Loading(var resource) => $"Caricamento di {resource}...",
Ready(var data) => $"Pronto con dati: {data.GetType().Name}",
Error(var message, null) => $"Errore: {message}",
Error(var message, var ex) => $"Errore: {message} ({ex.GetType().Name})",
_ => "Stato sconosciuto"
};
public void Transition(State newState)
{
var isValidTransition = (CurrentState, newState) switch
{
(Idle, Loading) => true,
(Loading, Ready or Error) => true,
(Ready or Error, Idle or Loading) => true,
_ => false
};
if (isValidTransition)
CurrentState = newState;
else
throw new InvalidOperationException($"Transizione non valida da {CurrentState} a {newState}");
}
}
Parser di Espressioni
public abstract record Expression;
public record Number(double Value) : Expression;
public record Variable(string Name) : Expression;
public record BinaryOp(Expression Left, string Operator, Expression Right) : Expression;
public record UnaryOp(string Operator, Expression Operand) : Expression;
public static double Evaluate(Expression expr, Dictionary<string, double> variables) => expr switch
{
Number(var value) => value,
Variable(var name) when variables.ContainsKey(name) => variables[name],
Variable(var name) => throw new ArgumentException($"Variabile {name} non definita"),
BinaryOp(var left, "+", var right) => Evaluate(left, variables) + Evaluate(right, variables),
BinaryOp(var left, "-", var right) => Evaluate(left, variables) - Evaluate(right, variables),
BinaryOp(var left, "*", var right) => Evaluate(left, variables) * Evaluate(right, variables),
BinaryOp(var left, "/", var right) when Evaluate(right, variables) != 0 =>
Evaluate(left, variables) / Evaluate(right, variables),
BinaryOp(_, "/", _) => throw new DivideByZeroException(),
UnaryOp("-", var operand) => -Evaluate(operand, variables),
UnaryOp("+", var operand) => Evaluate(operand, variables),
_ => throw new ArgumentException($"Operazione non supportata: {expr}")
};
Performance e Ottimizzazioni
Il pattern matching in C# è ottimizzato dal compilatore per generare codice efficiente:
- Jump tables per switch expressions con molti casi
- Inline checks per pattern semplici
- Guard optimization per condizioni when
// Ottimizzato come jump table
public static string GetMonth(int month) => month switch
{
1 => "Gennaio", 2 => "Febbraio", 3 => "Marzo",
4 => "Aprile", 5 => "Maggio", 6 => "Giugno",
7 => "Luglio", 8 => "Agosto", 9 => "Settembre",
10 => "Ottobre", 11 => "Novembre", 12 => "Dicembre",
_ => "Mese non valido"
};
Best Practices
Ordinamento dei Pattern
// ✅ Dal più specifico al più generale
public static string ClassifyAnimal(Animal animal) => animal switch
{
Dog { Breed: "Labrador", Age: > 5 } => "Labrador adulto",
Dog { Breed: "Labrador" } => "Labrador giovane",
Dog { Age: > 10 } => "Cane anziano",
Dog => "Cane generico",
Cat { IsIndoor: true } => "Gatto domestico",
Cat => "Gatto",
_ => "Animale sconosciuto"
};
Exhaustive Matching
// Assicurarsi che tutti i casi siano coperti
public static decimal GetTax(CustomerType type) => type switch
{
CustomerType.Individual => 0.22m,
CustomerType.Business => 0.24m,
CustomerType.NonProfit => 0.0m,
_ => throw new ArgumentException($"Tipo cliente non gestito: {type}")
};
Conclusione
Il pattern matching avanzato in C# rappresenta un paradigma potente per l’analisi e la decostruzione di dati complessi. Con l’evoluzione continua del linguaggio, dalla decostruzione di tuple ai list patterns, C# offre strumenti sempre più espressivi per scrivere codice conciso, leggibile e type-safe. La padronanza di queste tecniche moderne consente di sviluppare soluzioni eleganti per problemi complessi di parsing, state management e analisi di dati, migliorando significativamente la qualità e la manutenibilità del codice.