Pattern matching (C# 7, C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 7 (i.e. C# 7.0/7.1/7.2/7.3) et C# 8.0.

A partir de C# 7.0, quelques notions de programmation fonctionnelle sont introduites. Dans la documentation, même si l’expression “pattern matching” (i.e. “filtre avec motif”) est utilisée pour qualifier ces notions, il ne s’agit que de quelques améliorations pour simplifier certains éléments de syntaxe. Il n’y a pas, à proprement parlé, d’introduction de nouveaux concepts, il s’agit juste de raccourcis pour simplifier la syntaxe.

Ces notions de pattern matching peuvent être mises en place avec les mot-clés is et switch...case, elles permettent d’implémenter un filtre dans lequel on pourra indiquer des conditions. Suivant si une condition est vraie, une action spécifique pourra être exécutée.

Pour la suite, on considère les classes suivantes:

abstract class Vehicle  
{  
  public string Name;
  public abstract int GetWheelCount();  
}  


class MotoBike : Vehicle  
{  
  public int Power => 100;  
  
  public override int GetWheelCount()  
  {  
    return 2;  
  }
}  

class Car : Vehicle  
{  
  public int PassengerCount => 4;  
  
  public override int GetWheelCount()  
  {  
    return 4;  
  }  
}  

Avec is

C# 7.0

L’opérateur is permet de tester une expression pour savoir si elle satisfait une condition particulière. Chaque type de condition correspond à un motif (i.e. pattern). En C# 7.0, les motifs possibles sont:

  • Null pattern: test par rapport à une valeur nulle, par exemple:
    Vehicle vehicle = new Car();  
    if (vehicle is null)  
      Console.WriteLine($"{nameof(vehicle)} is null.");  
    else  
      Console.WriteLine($"{nameof(vehicle)} is not null.");  
    

    Cette fonctionnalité est disponible à partir de C# 7.0.

  • Constant pattern: test en comparant par rapport à une constante, par exemple:
    object carAsObj = new Car();  
    if (carAsObj is "45")  
      Console.WriteLine($"{nameof(carAsObj)} is 45.");  
    else  
      Console.WriteLine($"{nameof(carAsObj)} is not 45.");  
    

    Cette fonctionnalité est disponible à partir de C# 7.0.

  • Type pattern: l’expression est testée suivant un type particulier (possible avant C# 7.0):
    Vehicle vehicle = new Car();  
    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.");  
    

Dans le cas où on utilise is pour tester le type d’une expression, il est possible de combiner is et as en une seule ligne pour simplifier la syntaxe. Pour remplacer les 2 lignes:

<expression à tester> is <type voulu>  
var <variable typée> = <variable> as <type voulu>  

On peut simplifier la syntaxe en écrivant:

<expression à tester> is <type voulu> <nom variable>  

Par exemple, si on considère le code suivant:

Vehicle vehicle = new Car();  
if (vehicle is Car)  
{  
  var car = vehicle as Car;
  Console.WriteLine($"{nameof(vehicle)} is a car with {car.PassangerCount} passagers.");  
}  
else if (vehicle is Motobike)  
{  
  var motobike = vehicle as Motobike;
  Console.WriteLine($"{nameof(vehicle)} is a motobike of {motobike.Power} horsepower.");  
}  
else  
  Console.WriteLine($"{nameof(vehicle)} has not been identified.");  

La syntaxe peut être simplifier:

if (vehicle is Car car)  
  Console.WriteLine($"{nameof(vehicle)} is a car with {car.PassangerCount} 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.");  

Avec switch

Switch…case

C# 7.0

L’intérêt du pattern matching est de pouvoir simplifier la syntaxe en utilisant les apports de is dans une clause switch...case. Le type de condition applicable à is sont les mêmes pour switch...case. En une seule ligne, on peut tester les motifs suivant:

  • Si une expression est nulle (i.e. null pattern),
  • Si une expression est constante (i.e. constant pattern) et
  • Si une expression correspond à un type particulier (i.e. type pattern).

Si on prend l’exemple précédent, le code équivalent en utilisant switch...case pourrait être:

Vehicle vehicle = new Car();  
switch (vehicle)  
{  
  case Car car: // Type pattern
    Console.WriteLine($"{nameof(vehicle)} is a car with {car.PassengerCount} passagers.");  
    break;  
  case Motobike motobike:  // Type pattern
    Console.WriteLine($"{nameof(vehicle)} is a motobike of {motobike.Power} horsepower.");  
    break;  
  default:  
    Console.WriteLine($"{nameof(vehicle)} has not been identified.");  
    break;  
}  

D’autres conditions peuvent être utilisées notamment en testant la nullité ou l’égalité à une constante:

object carAsObj = new Car();  
switch (casAsObj)  
{  
  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;  
}  

when avec switch…case

C# 7.0

Au-delà des tests sur la nullité, un type ou l’égalité par rapport à une constante, il est possible de tester d’autres conditions en utilisant le mot-clé when.

Par exemple, si on souhaite ajouter des conditions quand un objet est de type Car:

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;  
}  

Dans le cas où l’ordre des conditions ne permet pas à certains cas d’être atteint, une erreur est émise à la compilation.

Par exemple:

Vehicle vehicle = new Car();  
switch (vehicle)  
{  
  case Car car: // Provoque une erreur de compilation  
    Console.WriteLine($"{nameof(vehicle)} is a car with {car.PassengerCount} passagers.");  
    break;  
  case Car car when car.PassengerCount < 1:  
    Console.WriteLine($"{nameof(vehicle)} is an empty car.");  
    break;  
  default:  
    Console.WriteLine($"{nameof(vehicle)} has not been identified.");  
    break;  
}  

Dans ce cas, la condition when car.PassengerCount < 1 n’est jamais atteinte car elle est occultée par la ligne case Car car.

Toutefois dans certains cas, du code peut ne jamais être atteint et aucune erreur de compilation ne sera générée, par exemple:

switch (vehicle)  
{  
  case Car car when car.PassengerCount > 1:  
    Console.WriteLine($"{nameof(vehicle)} with {car.PassengerCount} passenger(s).");  
    break;  
  case Car car when car.PassengerCount > 3: // ce code ne sera jamais atteinte  
    Console.WriteLine($"{nameof(vehicle)} is full.");  
    break;  
  default:  
    Console.WriteLine($"{nameof(vehicle)} has not been identified.");  
    break;  
}  

La condition when car.PassengerCount > 1 s’applique avant when car.PassengerCount > 3 donc cette partie du code ne sera jamais atteinte.

var pattern avec is ou switch…case

C# 7.0

var pattern est un filtre utilisable à partir de C# 7.0 avec is et switch...case. La syntaxe générale avec is est:

<expression> is var <nom de la variable> 

Cette syntaxe correspond, d’une part à une condition appliquée à <expression> et d’autre part, elle permet de créer une variable contenant le résultat d’une expression.

La condition est toujours vraie même si le résultat de l’expression testée est nulle. Le type de la variable correspond au type de l’expression et si l’expression est nulle alors la variable sera nulle.

L’intérêt de cette construction est de créer une variable temporaire qui pourra servir pour d’autres traitements, par exemple:

List<Vehicle> vehicles = new List<Vehicle>{ new Car() }; 
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"); 
} 

Dans cet exemple, la ligne vehicles.FirstOrDefault(...) is var bigVehicle permet de créer la variable bigVehicle qui pourra être utilisée dans la clause if.

De la même façon que pour les autres types de motifs, le motif var peut être utilisé avec switch...case, par exemple:

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.0

A partir de C# 8.0, switch peut être utilisé dans une expression avec une syntaxe équivalente à switch...case et plus concise.

Une expression est une instruction dont l’évaluation permet d’obtenir une valeur qui peut être assignable à une variable:

var <variable assignée> = <expression>;

switch sous forme d’expression est utilisé avec une variable et doit retourner une valeur assignable.

La forme générale est:

<variable assignée> = <variable> switch
{
  <condition 1> => <expression 1>,
  <condition 2> => <expression 2>,
  // ...
};

Cette forme est équivalente à:

switch (<variable>)
{
  case <condition 1>:
    <variable assignée> = <expression 1>;
    break;
  case <condition 2>:
    <variable assignée> = <expression 2>;
    break;
  // ...
}

Ainsi, les conditions sont appliquées à l’objet <variable> et le résultat des expressions est affecté à l’objet <variable assignée>.

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"), // default case
};

Dans cet exemple:

  • null => ... correspond au null pattern, il est activé quand la variable est nulle.
  • _ => ... correspond au discard pattern, il est appliqué par défaut quand toutes les autres lignes ne peuvent pas s’appliquer (discard pattern).

Cette syntaxe est équivalente à:

switch(vehicle)
{
  case Car car:
    text = $"The vehicle is a car: {car.Name}";
    break;
  case Motobike moto:
    text = $"The vehicle is a motobike: {moto.Name}";
    break;
  case null:
    text = "No vehicle";
    break;
  default:
    throw new InvalidOperationException("Vehicle is unknown");
}

Discard pattern

Le motif discard (i.e. discard pattern) correspond au cas par défaut (équivalent à default dans la syntaxe switch...case), par exemple:

_ => <expression>,

Ce pattern peut aussi s’appliquer dans le cas d’un cast de type (cf. type pattern) et si on ne veut pas utiliser la variable après le cast, par exemple:

string text = vehicle switch
{
  Car _ => "The vehicle is a car", // discard pattern
  Motobike _ => "The vehicle is a motobike", // discard pattern
  _ => throw new InvalidOperationException("Vehicle is unknown"),
};

when avec une expression switch

On peut aussi utiliser when avec une expression switch, par exemple:

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"), // default case
};

var pattern

Le motif var (i.e. var pattern) peut aussi s’appliquer à l’expression switch. Ce pattern s’applique quelque soit le type de la variable (c’est-à-dire que la condition est toujours vraie), par exemple:

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
  var unknownType => "The vehicle type is unknown",  // var pattern
  // _ => throw new InvalidOperationException("Vehicle is unknown"), Unreachable code
};

Si on place la ligne correspondant au motif var avant les autres, elle sera appliquée en priorité par rapport aux autres. Une erreur de compilation sera générée car les lignes après le motif var ne sont pas accessibles.

Tuple pattern

Dans le cas où on applique l’expression switch avec un tuple, il est possible d’appliquer des conditions aux différentes valeurs du tuple, par exemple:

(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 
};

Cette syntaxe permet de tester tous les éléments du tuple en appliquant une condition sur chaque élément.

Positional pattern

Le motif positional (i.e. positional pattern) permet d’appliquer des conditions sur les éléments d’un tuple en prenant en compte la position de chaque élément dans le tuple. Au lieu d’indiquer une condition précise pour chaque élément (comme pour le tuple pattern), on peut créer un nouveau tuple et appliquer des conditions sur un ou plusieurs éléments.

Par exemple:

(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.

Le motif positional ne s’applique pas seulement au tuple, il peut s’appliquer sur des objets quelconques si ces derniers peuvent être déconstruits en tuple (avec une méthode Deconstruct()).

Par exemple, si on considère la classe suivante:

public class Car: Vehicle
{
  public string Name;
  public int PassengerCount;

  public Car(string name, int passengerCount)
  {
    this.Name = name;
    this.PassengerCount = passsengerCount;
  }

  public void Deconstruct(out string name, out int passengerCount)
  {
    name = this.Name;
    passengerCount = this.PassengerCount;
  }
}

On peut utiliser cet objet de cette façon:

var car = new Car("Berline", 4);
var (name, passengerCount) = car;

La déconstruction peut servir pour appliquer des conditions à appliquer sur l’objet à tester en utilisant la position des éléments du tuple obtenu après déconstruction, par exemple:

Car unknownCar = ...;
bool isBigCar = unknownCar switch
{
  (_, var seatCount) when seatCount >= 4 => true, // déconstruction et utilisation de passengerCount seulement
  (var carName, _) when carName == "4WD"  => true, // déconstruction et utilisation de name seulement
  (var carName, var seatCount) => false, // Instanciation d’un tuple avec de nouveaux noms d’éléments
};

Dans le cas précédent, les conditions créent un tuple pour lequel on indique des noms d’élément particulier.

Property pattern

La motif property (i.e. property pattern) est une version plus générale du positional pattern. Il permet d’appliquer des conditions sur des propriétés d’un objet (la déconstruction n’est pas nécessaire car on applique les conditions sur un nouvel objet du même type que l’objet d’origine).

Par exemple si on considère les classes suivantes:

public class Vehicle
{
  public string Name;
  public int PassengerCount;

  public Vehicle(string name, int passengerCount)
  {
    this.Name = name;
    this.PassengerCount = passsengerCount;
  }
}

public class Car: Vehicle
{
  public int Power;

  public Vehicle(string name, int passengerCount, int power):
    base(name, passengerCount)
  {
    this.Power = power;
  }
}

public class Motobike: Vehicle
{
  public int Displacement;

  public Motobike(string name, int passengerCount, int displacement):
    base(name, passengerCount)
  {
    this.Displacement = displacement;
  }
}

On peut appliquer des conditions sur les propriétés de l’objet:

Vehicle vehicle = new Car("Berline", 4);
string result = vehicle switch
{
  Car { Name: "4WD" } => "Vehicle is a 4WD",
  Car { Name: "4WD", PassengerCount: 4 } => "Vehicle is a 4WD with 4 passengers",
  Car { Name: "Berline" } berline when berline.PassengerCount > 4 => 
      $"The car is a berline with {berline.PassengerCount} passengers.", // Utilisation d’une condition avec when
  Car { Name: "Berline" } berline => 
      $"The car is a berline with  {berline.PassengerCount} passengers.", // Utilisation d’une nouvelle variable
  _ => "No matches" // cas par défaut
};

Le détail de la syntaxe des conditions est:

  • Car { Name: "4WD" }: cette propriété permet de tester si l’objet vehicle est de type Car et si la propriété Name contient la valeur "4WD".
  • Car { Name: "4WD", PassengerCount: 4 }: cette condition permet de vérifier une condition sur la propriété Name et sur la propriété PassengerCount.
  • Car { Name: "Berline" } berline when berline.PassengerCount > 4: en plus de vérifier une condition d’égalité sur la propriété Name, une condition est vérifiée sur la propriété PassengerCount avec la syntaxe when.
  • La variable berline créée peut servir dans la condition when et dans l’expression.

Objets de type valeur

Certaines implémentations peuvent dégrader les performances si on utilise le pattern matching avec des objets de type valeur. Dans le cas où le code est exécuté fréquemment, il convient d’éviter ces constructions.

Par exemple, comparer un objet de type valeur avec une constante avec l’opérateur is occasionne du boxing:

int number = 6; 
if (number is 42) // Boxing 
{ ... } 

Cette implémentation occasionne 2 cas de boxing: pour la constante et pour la variable number.

Il n’y a pas de boxing si on utilise une construction similaire avec switch...case:

int number = 6; 
switch(number) 
{ 
  case 42: // Pas de boxing 
  ... 
  break 
} 

En revanche si on utilise variable de type object, il peut y avoir de l’unboxing:

object number = 6; // Boxing 
switch(number) 
{ 
  case 42: // Unboxing 
  ... 
  break 
} 

Si on utilise une variable de type int sans passer par une variable de type object, il n’y a pas d’unboxing:

int number = 5; 
// ... 
switch (number) 
{ 
  case 42: 
    Console.WriteLine("OK"); 
    break; 
  case int positiveInt when positiveInt > 0: 
    Console.WriteLine("Positive number"); 
    break; 
  case int negativeInt when negativeInt < 0: 
    Console.WriteLine("Negative number"); 
    break; 
  case int nullInt when nullInt == 0: 
    Console.WriteLine("Number is null"); 
    break; 
} 

Support des génériques

C# 7.1

Le pattern matching supporte les génériques à partir de C# 7.1.

Par exemple, si on considère les classes suivantes:

abstract class Vehicle 
{ 
  public int PassengerCount  { get; set; } 
} 

class Car : Vehicle 
{ } 

class MotoBike : Vehicle 
{ } 

Des conditions du pattern matching peuvent s’appliquer sur le type générique, par exemple:

public void DisplayVehicleDetail<TVehicle>(TVehicle vehicle)  
where TVehicle: Vehicle 
{ 
  switch (vehicle) 
  { 
    case Car car: 
      Console.WriteLine($"Vehicle is a car with {car.PassengerCount} passengers."); 
      break; 
    case MotoBike moto: 
      Console.WriteLine($"Vehicle is a moto."); 
      break; 
    default: 
      Console.WriteLine($"Vehicle has not been identified."); 
      break;
  } 
} 

Point de vue d’architecture

Quand on utilise la programmation orientée objet comme on le fait en C#, une pratique courante est de tenter de généraliser des traitements pour en déduire une abstraction pour utiliser cette abstraction dans une classe parente. Les traitements plus spécifiques pourront être implémentés dans des classes enfants qui dérivent de la classe parente.

L’intérêt de cette abstraction est d’identifier les comportements similaires, d’en déduire une implémentation générale dans le but d’éviter des duplications des comportements et du code. Quand on instancie un objet enfant et qu’on exécute un traitement, l’implémentation de la classe va ainsi effectuer une partie de traitement en s’appuyant sur l’implémentation générique de la classe parente, et une autre partie sur l’implémentation spécifique à la classe enfant suivant les règles d’héritage et d’encapsulation. Du point de vue externe à la classe, on peut considérer:

  • Le type correspondant à la classe enfant et accéder aux fonctions spécifiques à cette classe enfant.
  • Le type de la classe parente et ainsi accéder seulement aux fonctions génériques non spécialisées.

Par exemple, si on considère les classes suivantes:

abstract class Vehicle 
{ 
  public void MoveForward() { … } 

  public void MoveReverse() { … } 

  public abstract void AddPassenger(); 
} 

class Car : Vehicle 
{ 
  public override void AddPassenger() { ... }  

  public void PutInTrunk() { ... }  
} 

class MotoBike : Vehicle 
{ 
  public override void AddPassenger() { ... }  

  public bool IsHelmetAvailable() { ... }  
} 

Dans cet exemple, le type Vehicle permet d’appeler de l’extérieur:

  • Des fonctions génériques comme MoveForward() ou MoveReverse().
  • Des fonctions dont l’implémentation est spécifique comme AddPassenger().

Vu de l’extérieur à l’objet, suivant le type instancié, on ne peut considérer que le type parent ou le type enfant. Il devient plus difficile de considérer les 2 types à la fois:

  • Effectuer un traitement sur le type de la classe parente et
  • Appliquer des comportements spécifiques suivant le type précis de l’objet.

Ainsi, dans le cas de l’exemple:

  • Soit on considère le type Vehicle:
    Vehicle vehicle = new Car();
    

    Exposer le type Vehicle permet d’éviter d’exposer la complexité de l’implémentation toutefois on ne peut pas accéder aux fonctions spécifiques au type Car.

  • Soit on considère directement le type précis de la classe:
    Car car = new Car(); 
    

    On perd l’intérêt d’avoir créé un type générique car on expose un type trop précis.

Les solutions à ce problème pourraient être:

  • D’effectuer des “casts” pour avoir les types précis et accéder aux fonctions spécifiques:
    Car car = vehicle as Car;  
    car.PutInTrunk(); 
    

    ou

    MotoBike moto = vehicle as MotoBike; 
    moto.IsHelmetAvailable(); 
    
  • Une autre possibilité est d’utiliser le pattern Visiteur:

Le pattern matching offre une 3e solution dont l’avantage est de présenter le code de façon plus synthétique:

switch (vehicle) 
{ 
  case Car car: 
    car.AddPassenger(); 
    car.MoveForward(); 
    break; 
  case MotoBike moto when moto.IsHelmetAvailable(): 
    moto.AddPassenger(); 
    car.MoveForward(); 
    break; 
  default: 
    vehicle.MoveReverse(); 
    break; 

} 

Leave a Reply