C# 8.0 pattern Matching

in C#, Informatica

C# 8.0 – Pattern Matching

Reading Time: 3 minutes

Una delle novità di dotnet core 3.0, è l’introduzione di C# 8, che porta con sè una serie di features e migliorie degne di nota. In aggiunta ad una serie di migliorie legate al compilatore, infatti, C# 8 introduce una serie di miglioramente anche all’interno del linguaggio stesso.

Una di queste è sicuramente il miglioramento del Pattern Matching. Introdotto nella versione C# 7, con qualche accenno alla sintassi dei linguaggi funzionali come F# , ha come obiettivo principale quello di scrivere codice il più semplice e snello possibile.

In C# 7 sono stati introdotti tre tipologie di pattern:

  • const patterns
  • var patterns
  • type patterns

I pattern possono essere utilizzali all’interno di espressioni is e soprattutto all’interno di istruzioni switch/case. Di seguito alcuni esempi di utilizzo dell’istruzione is:

    // Controllo alternativo di una variabile null
    if (o is null) Console.WriteLine("o is null");
 
    // verifica di una variabile const pattern
    const double value = double.NaN;
    if (o is value) Console.WriteLine("o is value");
 
    // const pattern con una stringa
    if (o is "o") Console.WriteLine("o is \"o\"");
 
    // Type pattern
    if (o is int n) Console.WriteLine(n);

L’utilizzo del pattern const con is in generale non è l’ideale in termini di efficenza. Una considerazione importante riguarda lo scope delle variabili dichiarate:

public void ScopeAndDefiniteAssigning(object o)
{
    if (o is string s && s.Length != 0)
    {
        Console.WriteLine("o is not empty string");
    }
 
    // Non è possibile utilizzare la varibile s perchè già dichiarata 
    if (o is int n || (o is string s2 && int.TryParse(s2, out n)))
    {
        Console.WriteLine(n);
    }
}

Nell’esempio viene dichiarata la variabile s che sarà visibile all’interno di tutto il metodo: questo tipo di approccio può generare problemi con la logica della funzione, generando eventuali problemi nel riutilizzo della variabile. Inoltre, la variabile all’interno di un’espressione is viene valorizzata solo quando il pattern matching viene attivato: o meglio, quando il predicato assume il valore true. Ecco perchè nell’esempio precedente è stato introdotto il controllo int.TryParse(…).

Il var patterns è un caso particolare di type pattern: questo pattern “matcherà” qualsiasi valore, anche se il valore è null. Nell’esempio:

public void IsVar(object o)
{
    if (o is var x) Console.WriteLine($"x: {x}");
}

dove o is object è true quando la variabile o non è nulla, ma o is var x è sempre true. In questo caso il compilare rimuove l’if e lascia semplicemente la Console.Writeline(…).

Una casistica piuttosto utile è quella in combinazione con l’operatore ? (elvis). Introducendo questo operatore, il matching avviene solo quando il valore è diverso da null. Il tutto consente di scrivere codice semplice e di facile lettura, utilizzando quella che viene chiamata null propagation.

public void WithNullPropagation(IEnumerable<string> s)
{
    //null propagation
    if (s?.FirstOrDefault(str => str.Length > 10)?.Length is int length)
    {
        Console.WriteLine(length);
    }
 
    // equivalente a questa espressione
    if (s?.FirstOrDefault(str => str.Length > 10)?.Length is var length2 && length2 != null)
    {
        Console.WriteLine(length2);
    }
 
    // equivalente anche a questa espressione
    var length3 = s?.FirstOrDefault(str => str.Length > 10)?.Length;
    if (length3 != null)
    {
        Console.WriteLine(length3);
    }
}

Risulta evidente come il primo controllo if sia facilmente comprensibile e soprattutto compatto.

Novità in C# 8

All’interno di C# 8 sono state rilasciate le Switch Expression, che introducono un modo compatto per scrivere espressioni switch/case.

Partendo dalla scrittura di un blocco switch/case all’interno del codice, ci si rende conto che il codice non è proprio “compatto” anche se, per molti aspetti, mantiene la sua leggibilità. Prendendo ad esempio un semplice metodo Calculate, che esegue un operazione, passata come parametro, sui due operandi anch’essi passati come parametro:

public int Calculate(string operation, int operand1, int operand2) {
  int result;

  switch (operation) {
    case "+":
      result = operand1 + operand2;
      break;
    case "-":
      result = operand1 - operand2;
      break;
    case "*":
      result = operand1 * operand2;
      break;
    case "/":
      result = operand1 / operand2;
      break;
    default:
      throw new ArgumentException($ "The operation {operation} is invalid!");
  }

  return result;
}

Nell’ottica di semplificare la scrittura e soprattutto scrivere minor codice possibile, il pattern matching di C#8 consente di migliorare nettamente la scrittura del blocco.

public int Calculate(string operation, int operand1, int operand2) {
  var result = operation switch 
  {
    "+" => operand1 + operand2,
    "-" => operand1 - operand2,
    "*" => operand1 * operand2,
    "/" => operand1 / operand2,
    _ =>
      throw new ArgumentException($ "The operation {operation} is invalid!"),
  };

  return result;
}

La scrittura dello stesso blocco utilizzando il pattern matching di C# 8 consente di risparamiare righe di codice, utilizzando una sintassi più “lambda” style. E’ da notare l’utilizzo di _ che corrisponde alla parola chiave default della vecchia sintassi. La keyword switch esegue la “mappatura” tra la variabile che deve essere verificata e le espressioni da valutare.

L’utilizzo del pattern matching all’interno di istruzioni switch in C# 8 è modo particolarmente elegante e soprattutto efficiente per poter mappare le informazioni.