Il pattern matching confronta un valore con una forma: "è un Circle?", "è null?", "il suo Status è "paid" e il suo Total supera 100?". Quando il test riesce, il pattern può anche estrarre parti del valore in nuove variabili. È nato in piccolo in C# 7 con l'operatore is ed è cresciuto in ogni versione successiva, per questo la pagina indica la versione di ogni forma.
Il pattern di tipo con is (C# 7)
Prima di C# 7, verificare un tipo e usarlo richiedeva due passi: as più un controllo su null, oppure is più un cast. Il pattern di tipo fa entrambe le cose insieme e ti dà una variabile tipizzata:
Output:
card ending 4242: 49.90
transfer from PT50...: 1200
no payment
other payment: 5
p is CardPayment card è vero solo quando p è un CardPayment non null (o una classe derivata), e in quel caso card lo contiene con il tipo statico giusto. Un valore null non corrisponde mai a un pattern di tipo, ed è per questo che il pagamento null arriva fino al controllo su null.
Ambito e assegnazione certa
La variabile del pattern esiste nel blocco che la contiene, ma il compilatore ti permette di leggerla solo dove si sa che la corrispondenza è riuscita. Questo fa funzionare la forma a "clausola di guardia", in cui esci subito e usi la variabile dopo:
Output:
19.99
42 is over 40
price must be text
boxed is int n && n > 40 mostra l'altra forma comune: la variabile è utilizzabile a destra di &&, perché quel lato viene eseguito solo quando la corrispondenza è riuscita. A destra di || non è assegnata, e il compilatore te lo dice (CS0165).
I pattern fanno anche l'unboxing: boxed is int n riesce per un int in un box e ti dà il valore senza un cast che potrebbe lanciare un'eccezione. Per i tipi valore nullable, maybe is int n riesce esattamente quando maybe ha un valore.
Pattern di costante e null
Anche una costante è un pattern. x is null è quello più usato, e C# 7 permette anche x is 0, status is "paid" o un membro di un enum:
if (customer is null) return; // null check that ignores any == overload
if (count is 0) Console.WriteLine("empty");
if (level is LogLevel.Error) Alert();
Molti team preferiscono is null a == null perché non può essere deviato da un overload dell'operatore nel tipo. C# 9 aggiunge la forma negata, is not null.
Pattern nelle istruzioni switch (C# 7)
Gli stessi pattern di tipo possono etichettare i case, e una clausola when aggiunge una condizione. I case vengono provati dall'alto verso il basso, quindi ordinali dal più specifico al più generale:
static string Describe(object o)
{
switch (o)
{
case null: return "nothing";
case int n when n < 0: return "negative number";
case int n: return $"number {n}";
case string s when s.Length == 0: return "empty text";
case string s: return $"text of {s.Length} chars";
case IEnumerable<int> list: return "a sequence of numbers";
default: return o.GetType().Name;
}
}
Describe(-4); // negative number
Describe(""); // empty text
Describe(new List<int>()); // a sequence of numbers
Describe(2.5); // Double
Il compilatore controlla l'ordine: un case che non può mai corrispondere perché uno precedente lo copre è l'errore CS8120. default viene sempre valutato per ultimo, ovunque si trovi. La stessa logica scritta con catene di if/is, come nell'esempio dei pagamenti visto sopra, funziona in tutte le versioni di C# dalla 7 in poi.
Espressioni switch (C# 8)
Un'espressione switch è la forma compatta per "calcolare un valore a partire da diversi casi". Prima viene il valore, poi switch, poi i rami pattern => result separati da virgole, con _ come caso generico:
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
static double Area(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle { Width: var w, Height: var h } => w * h,
_ => throw new ArgumentException("Unknown shape", nameof(shape)),
};
Area(new Rectangle(3, 4)); // 12
Se in esecuzione nessun ramo corrisponde, lancia SwitchExpressionException, e il compilatore avvisa (CS8509) quando riesce a vedere che qualche input non è gestito. La pagina sull'espressione switch tratta la sintassi da sola; il resto di questa pagina riguarda i pattern che puoi mettere nei rami.
Pattern di proprietà (C# 8, estesi in C# 10)
Un pattern di proprietà confronta le proprietà di un oggetto con pattern annidati, tra graffe. L'oggetto deve anche essere non null, quindi { } da solo significa "non null":
public record Address(string City, string Country);
public record Order(decimal Total, string Status, Address ShipTo, int Items);
static string Shipping(Order order) => order switch
{
{ Status: "cancelled" } => "no shipment",
{ ShipTo.Country: "PT", Total: >= 50m } => "free, domestic", // C# 10 dotted form
{ ShipTo.Country: "PT" } => "4.90, domestic",
{ Total: > 200m } => "free, international",
_ => "12.00, international",
};
Shipping(new Order(60m, "paid", new Address("Porto", "PT"), 2)); // free, domestic
Shipping(new Order(260m, "paid", new Address("Lyon", "FR"), 2)); // free, international
In C# 8 e 9 è obbligatoria la forma annidata: { ShipTo: { Country: "PT" } }. C# 10 permette la scorciatoia con i punti usata sopra. I pattern di proprietà funzionano anche con is, e questo fa leggere i controlli con più condizioni come una descrizione: if (order is { Status: "paid", Items: > 0 }).
Pattern relazionali e logici (C# 9)
C# 9 ha aggiunto i confronti (<, <=, >, >=) e i combinatori and, or e not, che trasformano gli intervalli in pattern:
static string Grade(int score) => score switch
{
< 0 or > 100 => "invalid",
>= 90 => "A",
>= 75 and < 90 => "B",
>= 50 => "C",
_ => "F",
};
Grade(82); // B
Grade(101); // invalid
if (input is not null and not "") { ... }
if (c is >= 'a' and <= 'z' or >= 'A' and <= 'Z') { ... } // and binds tighter than or
L'equivalente in C# 7 è una catena if/else if oppure uno switch con clausole when (case int s when s >= 90:).
Pattern di tupla e posizionali (C# 8)
Per fare uno switch su più valori insieme si fa lo switch su una tupla:
static string Quadrant(int x, int y) => (x, y) switch
{
(0, 0) => "origin",
(> 0, > 0) => "I",
(< 0, > 0) => "II",
(< 0, < 0) => "III",
(> 0, < 0) => "IV",
_ => "on an axis",
};
Quadrant(-2, 5); // II
Un pattern posizionale fa lo stesso per qualsiasi tipo con un metodo Deconstruct, compresi i record: case Point(0, 0): oppure p is Rectangle(var w, var h) && w == h.
List pattern (C# 11)
I list pattern confrontano array e liste in base ai loro elementi. .. corrisponde a un numero qualsiasi di elementi (uno "slice"), e ogni posizione può contenere qualsiasi altro pattern:
static string Route(string[] args) => args switch
{
[] => "help",
["add", var item] => $"add {item}",
["remove", var item, ..] => $"remove {item}",
[var cmd, ..] => $"unknown command {cmd}",
};
Route(new[] { "add", "milk" }); // add milk
Route(new[] { "remove", "milk", "now" }); // remove milk
int[] nums = { 1, 2, 3, 4 };
if (nums is [1, .., var last]) Console.WriteLine(last); // 4
Quale versione ha aggiunto cosa
| Pattern | Esempio | Versione |
|---|---|---|
| Pattern di tipo con variabile | o is Circle c | C# 7.0 |
| Pattern di costante | x is null, n is 0 | C# 7.0 |
Pattern var | x is var v | C# 7.0 |
Pattern nelle etichette case, condizioni when | case int n when n < 0: | C# 7.0 |
Espressione switch, discard _ | x switch { ... } | C# 8 |
| Pattern di proprietà | { Status: "paid" } | C# 8 |
| Pattern di tupla | (x, y) switch { (0, 0) => ... } | C# 8 |
| Pattern posizionale | Point(0, 0) | C# 8 |
| Pattern relazionale | > 100, <= 0 | C# 9 |
| Pattern logici | not null, >= 1 and <= 5, 'a' or 'b' | C# 9 |
| Pattern di tipo semplice | case Circle:, Circle => ... | C# 9 |
| Pattern di proprietà esteso | { ShipTo.City: "Porto" } | C# 10 |
| List pattern e slice | [first, .., last] | C# 11 |
Errori comuni
- Ordinare i case dal generale allo specifico.
case Payment p:prima dicase CardPayment c:rende irraggiungibile il secondo (CS8120). Prima quelli specifici. - Aspettarsi che un pattern di tipo corrisponda a null.
null is string sè falso. Gestiscinullcon un case apposito o conis null. - Leggere la variabile del pattern dove la corrispondenza può essere fallita. Dopo
a is int n || ...,nnon è assegnata (CS0165). - Lunghi switch sui tipi invece di metodi virtuali. Se ogni nuova sottoclasse ti obbliga a modificare lo stesso switch in cinque punti, il comportamento appartiene alla gerarchia di classi. I pattern danno il meglio con dati che arrivano dall'esterno (forme JSON, messaggi, tuple di input) e con insiemi chiusi di tipi che controlli tu.
- Dimenticare il caso generico in un'espressione switch. Un input senza corrispondenza lancia un'eccezione in esecuzione; leggi l'avviso CS8509.
Domande frequenti
Cos'è il pattern matching in C#?
Il pattern matching confronta un valore con una forma e, quando corrisponde, può estrarne delle parti in variabili nello stesso passo. if (shape is Circle c) verifica il tipo e ti dà una c tipizzata; order is { Status: "paid", Total: > 100 } verifica le proprietà. I pattern compaiono nelle espressioni is, nelle istruzioni switch e nelle espressioni switch.
Come uso is con una variabile in C#?
if (obj is Customer c) { ... } verifica che obj sia un Customer (non null) e lo assegna a c, di tipo Customer, dentro l'if. Sostituisce la vecchia coppia var c = obj as Customer; if (c != null). La variabile è assegnata con certezza solo dove si sa che il test è vero, quindi if (!(obj is Customer c)) return; lascia c utilizzabile dopo l'if.
Che differenza c'è tra is null e == null in C#?
x is null controlla sempre il riferimento in sé. x == null chiama l'operatore == del tipo se ne ridefinisce uno, il che di solito dà la stessa risposta ma non è garantito. Per il contrario scrivi x != null, !(x is null) oppure x is not null da C# 9.
Come funziona il pattern matching nello switch con when in C#?
Un'etichetta case può contenere un pattern più una condizione when: case Order o when o.Total > 100:. Il case corrisponde solo se il pattern corrisponde e la condizione è vera, e i case vengono provati dall'alto verso il basso, quindi metti prima quelli più specifici. Il compilatore segnala CS8120 quando un case non può mai essere raggiunto perché uno precedente lo copre già.
Quale versione di C# ha aggiunto quali pattern?
C# 7.0: pattern di tipo, di costante e var in is e case, più when. C# 8: espressioni switch, pattern di proprietà, di tupla e posizionali. C# 9: pattern relazionali (> 5), logici (and, or, not) e di tipo semplice. C# 10: pattern di proprietà estesi ({ Address.City: "Porto" }). C# 11: list pattern ([1, .., var last]).