Passa al contenuto principale

Switch Expression e Pattern Matching

C# 8+ introduce le switch expression come alternativa compatta alle switch statement. Combinato con il pattern matching, produce codice leggibile, esaustivo e senza stati impossibili.

Switch expression base

// ❌ Switch statement classico
string descrizione;
switch (stato)
{
case OrdineStato.Bozza:
descrizione = "In lavorazione";
break;
case OrdineStato.Confermato:
descrizione = "Confermato";
break;
case OrdineStato.Spedito:
descrizione = "In consegna";
break;
default:
throw new ArgumentOutOfRangeException(nameof(stato));
}

// ✅ Switch expression
string descrizione = stato switch
{
OrdineStato.Bozza => "In lavorazione",
OrdineStato.Confermato => "Confermato",
OrdineStato.Spedito => "In consegna",
_ => throw new ArgumentOutOfRangeException(nameof(stato))
};

Assegnazione da switch expression

La switch expression è un'espressione: produce un valore e si assegna direttamente.

decimal sconto = categoriaCliente switch
{
CategoriaCliente.Standard => 0m,
CategoriaCliente.Premium => 0.10m,
CategoriaCliente.Corporate => 0.20m,
_ => throw new ArgumentOutOfRangeException(nameof(categoriaCliente))
};

var endpoint = ambiente switch
{
Ambiente.Local => "http://localhost:5000",
Ambiente.Staging => "https://staging.example.com",
Ambiente.Prod => "https://example.com",
_ => throw new ArgumentOutOfRangeException(nameof(ambiente))
};

Pattern matching su tipo

decimal CalcolaIva(Prodotto prodotto) => prodotto switch
{
ProdottoAlimentare => prodotto.Prezzo * 0.04m,
ProdottoFarmaceutico => prodotto.Prezzo * 0.10m,
ProdottoElettronico => prodotto.Prezzo * 0.22m,
_ => prodotto.Prezzo * 0.22m
};

Pattern matching con condizioni (when)

string Classifica(int punteggio) => punteggio switch
{
>= 90 => "Eccellente",
>= 70 => "Buono",
>= 50 => "Sufficiente",
_ => "Insufficiente"
};

decimal CalcolaSpedizione(Ordine ordine) => ordine switch
{
{ Totale: >= 50 } => 0m,
{ Destinazione: Destinazione.Italia } => 4.90m,
{ Destinazione: Destinazione.Europa } => 9.90m,
_ => 19.90m
};

Pattern matching su tuple

Utile per combinare più valori in una sola espressione:

string DescriviTransizione(OrdineStato da, OrdineStato a) => (da, a) switch
{
(OrdineStato.Bozza, OrdineStato.Confermato) => "Ordine confermato",
(OrdineStato.Confermato, OrdineStato.Spedito) => "Ordine spedito",
(OrdineStato.Spedito, OrdineStato.Consegnato) => "Ordine consegnato",
(OrdineStato.Confermato, OrdineStato.Annullato) => "Ordine annullato",
_ => throw new InvalidOperationException($"Transizione {da}{a} non ammessa")
};

Uso nel Result pattern

IActionResult ToActionResult(Result result) => result switch
{
{ IsSuccess: true } => Ok(),
{ Error: NotFoundError e } => NotFound(e.Message),
{ Error: ValidationError e } => BadRequest(e.Message),
{ Error: UnauthorizedError } => Unauthorized(),
_ => StatusCode(500)
};

IActionResult ToActionResult<T>(Result<T> result) => result switch
{
{ IsSuccess: true, Value: var v } => Ok(v),
{ Error: NotFoundError e } => NotFound(e.Message),
{ Error: ValidationError e } => BadRequest(e.Message),
_ => StatusCode(500)
};

Esaustività

Il compilatore avvisa se un enum non è coperto completamente. Il pattern _ come fallback su un enum è un segnale che si vuole gestire esplicitamente i casi futuri — usare throw come default, non un valore silenzioso:

// ❌ Silenzioso: nasconde il caso non gestito
string label = stato switch
{
OrdineStato.Bozza => "Bozza",
_ => "" // nuovo stato aggiunto? nessun errore, stringa vuota
};

// ✅ Esplicito: il compilatore o il runtime segnalano il caso mancante
string label = stato switch
{
OrdineStato.Bozza => "Bozza",
OrdineStato.Confermato => "Confermato",
_ => throw new ArgumentOutOfRangeException(nameof(stato), stato, null)
};