Aide-mémoire pattern matching C#

Cet article est un aide-mémoire des motifs les plus courants de pattern matching suivant les versions de C# pour aider à se rappeler de la syntaxe:

Motif Version C# Remarques et exemples
Null pattern C# 7.0 Test pour vérifier si une variable est nulle

Vehicle vehicle = new Car();  
if (vehicle is null)  
  Console.WriteLine($"{nameof(vehicle)} is null.");  
else  
  Console.WriteLine($"{nameof(vehicle)} is not null.");
Constant pattern C# 7.0 Comparaison entre une variable et une constante

object carAsObj = new Car();  
if (carAsObj is "45")  
  Console.WriteLine($"{nameof(carAsObj)} is 45.");  
else  
  Console.WriteLine($"{nameof(carAsObj)} is not 45.");
Type pattern < C# 7.0 Test par rapport à un type:
Si on considère les types:

class Vehicle
{
    public string Name;
}

class MotoBike : Vehicle
{
    public int Power => 100;
}

class Car : Vehicle
{
    public int PassengerCount { get; set; }
}

Avant C# 7.0, on pouvait effectuer les tests suivants:

if (vehicle is Car)  
  Console.WriteLine($"{nameof(vehicle)} is a car.");  
else if (vehicle is Motobike)  
  Console.WriteLine($"{nameof(vehicle)} is a motobike.");  
else  
  Console.WriteLine($"{nameof(vehicle)} has not been identified."); 
C# 7.0 Test par rapport à un type et cast (utilisation implicite de as).

Avec le motif type, à partir de C# 7.0, on peut écrire:

if (vehicle is Car car)  
  Console.WriteLine($"{nameof(vehicle)} is a car with {car.PassengerCount} passagers.");  
else if (vehicle is Motobike motoBike)  
  Console.WriteLine($"{nameof(vehicle)} is a motobike of {motobike.Power} horsepower.");  
else  
  Console.WriteLine($"{nameof(vehicle)} has not been identified.");
Construction switch...case C# 7.0
object carAsObj = new Car();  
switch (carAsObj)  
{  
  case null:  // Null pattern
    Console.WriteLine("Is null");  
    break;  
  case "45":  // Constant pattern 
    Console.WriteLine("Is a constant, not a vehicle.");  
    break;  
  case Car car:  // Type pattern
    Console.WriteLine($"{nameof(carAsObj)} is a car with {car.PassengerCount} passagers.");  
    break;  
  case Motobike motobike:  // Type pattern
    Console.WriteLine($"{nameof(carAsObj)} is a motobike of {motobike.Power} horsepower.");  
    break;  
  default:  
    Console.WriteLine($"{nameof(carAsObj)} has not been identified.");  
    break;  
} 
Ajouter des conditions avec when et switch...case C# 7.0
Vehicle vehicle = new Car();  
switch (vehicle)  
{  
  case Car car when car.PassengerCount < 1:  
    Console.WriteLine($"{nameof(vehicle)} is an empty car.");  
    break;  
  case Car car when car.PassengerCount > 3 && car.PassengerCount <= 5:  
    Console.WriteLine($"{nameof(vehicle)} is a fully loaded car.");  
    break;  
  case Car car when car.PassengerCount > 8:  
    Console.WriteLine($"{nameof(vehicle)} is a heavy loaded car.");  
    break;  
  default:  
    Console.WriteLine($"{nameof(vehicle)} has not been identified.");  
    break;  
}
var pattern C# 7.0 Ce motif donne l’impression d’appliquer une condition avec l’opérateur is toutefois ce n’est pas le cas, la clause est toujours vraie. L’intérêt est d’effectuer une affectation du résultat de l’expression dans une variable qui pourra être utilisée pour d’autres conditions:
<expression> is var <nom de la variable> est toujours vraie, le résultat de <expression> est affecté dans la variable <nom de la variable>.

Par exemple:

List<Vehicle> vehicles = new List<Vehicle>{ new Car() }; 
// Toujours vrai, le résultat est dans bigVehicle
if (vehicles.FirstOrDefault(v => v.GetWheelCount() > 3) is var bigVehicle)
{ 
  if (bigVehicle.GetWheelCount() == 4) 
    Console.WriteLine("The vehicle is a car"); 
  else if (bigVehicle.GetWheelCount() == 6) 
    Console.WriteLine("The vehicle is a little truck"); 
  else if (bigVehicle.GetWheelCount() > 6) 
    Console.WriteLine("The vehicle is a big truck"); 
}

Utilisation avec switch...case:

switch(vehicles.FirstOrDefault(v => v.GetWheelCount() > 3)) 
{ 
  case null: 
    Console.WriteLine("No big vehicle round"); 
    break; 
  case var car when car.GetWheelCount() == 4: 
    Console.WriteLine("The vehicle is a little truck"); 
    break; 
  case var truck when truck.GetWheelCount() == 6: 
    Console.WriteLine("The vehicle is a little truck"); 
    break; 
  case var bigTruck when bigTruck.GetWheelCount() > 6: 
    Console.WriteLine("The vehicle is a big truck"); 
    break; 
}
Expression switch C# 8 Syntaxe permettant de faciliter l’affectation d’une variable suivant plusieurs conditions:

<nouvelle variable à assigner> = <variable existante> switch
{
  <condition 1> => <expression 1>,
  <condition 2> => <expression 2>,
  // ...
};

Par exemple:

Vehicle vehicle = new Car{ Name = "Car1"  };
string text = vehicle switch
{
  Car car => $"The vehicle is a car: {car.Name}",
  MotoBike moto => $"The vehicle is a motobike: {moto.Name}",
  null => "No vehicle", // null pattern
  _ => throw new InvalidOperationException("Vehicle is unknown"), // discard pattern (cas par défaut)
};
Type pattern dans une expression switch C# 8 Pour tester par rapport à un type dans une expression switch.

Vehicle vehicle = new Car{ Name = "Car1"  };
string text = vehicle switch
{
  Car car => $"The vehicle is a car: {car.Name}",
  MotoBike moto => $"The vehicle is a motobike: {moto.Name}",
  // [...]
};
C# 9 On peut supprimer la variable si on ne l’utilise pas par la suite.

Vehicle vehicle = new Car{ Name = "Car1"  };
string text = vehicle switch
{
  Car => "The vehicle is a car",
  MotoBike => "The vehicle is a motobike",
  // [...]
};
null pattern C# 9 A utiliser avec une expression switch pour tester si une variable est nulle.

Vehicle vehicle = new Car{ Name = "Car1"  };
string text = vehicle switch
{
  // [...]
  null => "No vehicle", // null pattern
  // [...]
};

La valeur "No vehicle" est affectée à text si vehicle est null.

Discard pattern C# 9 C’est le cas par défaut dans une expression switch.

Ce cas s’applique si aucune autre condition n’est satisfaite (cas par défaut).

string text = vehicle switch
{
  // [...]
  _ => throw new InvalidOperationException("Vehicle is unknown"),
};
Ajouter des conditions avec when dans une expression switch C# 9
string text = vehicle switch
{
  Car car when string.IsNullOrEmpty(car.Name) => $"The vehicle is a car",
  Car car when car.Name.Equals("Car1") => $"The vehicle is the first car",
  Car car => $"The vehicle is a car: {car.Name}",
  _ => throw new InvalidOperationException("Vehicle is unknown"), // cas par défaut
};
var pattern avec une expression switch C# 9 Ce motif s’applique quelque soit le type de variable (la condition est toujours vraie).

L’intérêt est d’effectuer une affectation de l’expression dans une variable qui pourra éventuellement être utilisée dans le reste de l’expression.

string text = vehicle switch
{
  // [...]
  var unknownType => $"The vehicle type {unknownType} is not handled", // var pattern
  // _ => throw new InvalidOperationException("Vehicle is unknown"), // Ce code est inatteignable 
};

Le motif var est toujours vrai donc il est inutile de l’utiliser avec le motif discard.

Tuple pattern C# 9 Pour tester des conditions dans le cas de tuple et d’une expression switch.

(int valueAsInt, string valueAsString, float valueAsFloat) tuple = (5, "5", 5f);
string result = tuple switch
{
  (5, "5", 5f) => "All values are 5",
  (6, "5", 5f) => "Int is 6",
  (7, "7", 7f) => "All values are 7",
  (_, _, _) => "No matches", // Cas par défaut 
};
Positional pattern avec un tuple C# 9 On utilise le caractère _ si on ne veut pas que la condition s’applique à un élément d’un tuple.

(int valueAsInt, string valueAsString, float valueAsFloat) tuple = (5, "5", 5.0f);
string result = tuple switch
{
  (5, "5", 5.0f) => "All values are equal", // la condition porte sur tous les éléments
  (5, _, _) => "Ints are equal",            // la condition porte seulement sur le 1er élément
  (_, "5", _) => "Strings are equal",       // la condition porte seulement sur le 2e élément
  (_, _, 5.0f) => "Floats are equal",       // la condition porte seulement sur le 3e élément
  (_, _, _) => "No matches",                // cas par défaut
};

On peut créer un nouveau tuple pour l’utiliser dans l’expression et appliquer une condition avec when:

var tuple = (5, "6", 6f);
string result = tuple switch
{
  (5, "5", 5f) => "All values are equal",
  (5, _, _) tupleWithSameInt => 
    $"Ints are equal (string values are {tupleWithSameInt.Item2})", // Utilisation du nouveau tuple dans l'expression
  (_, _, _) matchingTuple when matchingTuple.Item1 == 5 && matchingTuple.Item2 == "6" => 
    "Ints and strings are equal", // Utilisation du nouveau tuple avec une condition when
  (_, _, _) => "No matches",
};

Quelques détails sur les conditions utilisées:

  • (5, _, _) tupleWithSameInt: la condition porte seulement sur le 1er élément qui doit être égal à 5. Le tuple tupleWithSameInt est instancié et utilisable dans le reste de la condition si on utilise when ou dans l’expression.
  • On peut créer un nouveau tuple contenant des éléments dont les noms sont différents du tuple d’origine.
    Par exemple, si on utilise la condition (var x, var y, var z) => $"{x} {y} {z}", on crée un tuple dont les éléments sont nommés x, y, z qui sont utilisable dans une condition when et dans l’expression.
  • On peut utiliser le caractère discard (i.e. _) si on crée un nouveau type de tuple.
    Par exemple, avec la condition (var x, _, _) => $"{x}". Le caractère _ permet d’ignorer les autres éléments.
  • Il n’est pas possible d’utiliser une condition avec un tuple dont le nombre d’éléments n’est pas égal à celui du tuple d’origine.
    Par exemple, la condition (var x, var y) => ... provoque une erreur de compilation.
Relational pattern C# 9 On peut utiliser directement les opérateurs >, <, >= et <= pour tester une condition.

string text = vehicle.GetWheelCount() switch
{
  >= 4 => "The vehicle is a car",
  <= 2 => "The vehicle is a motobike",
  _ => throw new InvalidOperationException("Vehicle is unknown"),
};

Ce motif peut être utilisé en dehors d’une expression switch.

if (vehicle is Car { PassengerCount: <= 4 } car)
{
  // ...
}

L’opérateur == n’est pas utilisable, pour appliquer une condition d’égalité, il suffit d’omettre l’opérateur:

string text = vehicle.GetWheelCount() switch
{
  4 => "The vehicle is a car",
  2 => "The vehicle is a motobike",
  _ => throw new InvalidOperationException("Vehicle is unknown"),
};
Logical pattern C# 9 Permet d’utiliser les opérateurs and, or et not pour tester des conditions.

string text = vehicle.GetWheelCount() switch
{
  >= 4 and <= 6 => "The vehicle is a car",
  <= 2 and > 0 => "The vehicle is a motobike",
  _ => throw new InvalidOperationException("Vehicle is unknown"),
};

string text = vehicle switch
{
  null => "not instanciated",
  not null => "instanciated", // Opérateur not
};

Ce motif peut être utilisé en dehors d’une expression switch.

if (vehicle is Car { PassengerCount: >= 4 and <= 6 and not 5 } car)
{
  // ...
}
Property pattern C# 10 Permet d’appliquer des conditions sur les propriétés des objets sans avoir une syntaxe trop lourde.

Si on considère:

public class Car
{
  public int PassengerCount;
  public Engine Engine;
}

public class Engine
{
  public string EngineType; 
  public int Horsepower; 
}

On peut écrire:

string engineSize = vehicle switch
{
  Car { Engine.EngineType: "V8" } => "Big engine",
  Car { Engine.EngineType: "Straight-four" } => "Little engine",
  _ => "No matches"
};

Avec une clause if:

if (vehicle is Car { Engine.EngineType: "four stroke" } car)
{
  // ...
}
List pattern C# 11 Permet d’énoncer des conditions applicables sur les éléments d’une structure:

  • Enumérable (i.e. countable) et indexable ou
  • Enumérable (i.e. countable) et dont on peut extraire un sous-groupe (i.e. sliceable).

La syntaxe générale de ce motif utilise l’opérateur is:

<structure> is <conditions>

Il est possible de combiner plusieurs conditions avec les opérateurs and, or ou not:

<structure> is <condition 1> and <condition 2>

Une condition peut être énoncée en utilisant les syntaxes:

  • [2, 4, 6, 8, 10] pour indiquer que la structure doit contenir 5 entiers précis dans cet ordre.
  • Discard pattern: en utilisant le caractère _ pour indiquer n’importe quel élément.
    Par exemple:
    [2, 4, _, 8, _] permet d’indiquer que la structure doit contenir 3 entiers précis à la 1ère, 2e et 4e place. Il n’y a pas de condition sur les éléments à la 3e et 5e place.
  • Range pattern: permet d’indiquer un interval en utilisant les caractères ...
    Par exemple:

    • [.. , 10] permet d’indiquer que la structure doit se terminer par 10.
    • [.. , 8, 10] permet d’indiquer que la structure doit se terminer par un sous-groupe contenant 8 et 10.
    • [2, .. ] permet d’indiquer que la structure doit commencer par 2.
    • [2, 4, .. ] permet d’indiquer que la structure doit commencer par un sous-groupe contenant 2 et 4.
    • [2, .. , 10] permet d’indiquer que la structure doit commencer par 2 et se terminer par 10.
  • Relational pattern: permet d’indiquer des conditions en utilisant des opérateurs de comparaison <, <=, => ou > (== ne peut pas être utilisé, pour ajouter une condition d’égalité il suffit d’omettre l’opérateur).
    Par exemple:
    [2, 4, >= 6, <= 8, 10] permet d’indiquer que le 3e élément doit être supérieur ou égale à 6 et que le 4e élément doit inférieur ou égal à 8.
  • var pattern: ce motif n’applique pas de conditions mais permet d’effectuer des assignations en une seule ligne.
    Par exemple:

    var integers = {2, 4, 6, 8, 10};
    bool result = integers is [var item1, var item2, .., var itemN];
    

    Cette ligne permet d’assigner le 1er élément à item1, le 2e élément à item2 et le dernier élément à itemN. Les autres éléments sont ignorés.

Il est possible de combiner plusieurs motifs, par exemple le discard pattern et le range pattern:

bool result = integers is [_, 4, .., 10];

Pas de conditions sur le 1er élément et des conditions d’égalité sur le 2e et dernier élément.

On ne peut pas utiliser 2 fois le range pattern:

bool result = integers is [2, .., 4, .., 10]; // NE COMPILE PAS

On peut combiner le var pattern avec d’autres motifs:

bool result = integers is [var item1, .., var itemN];

Permet d’assigner le 1er et le dernier élément de la structure.

Leave a Reply