22 mai 2025
Le C# moderne a connu une révolution silencieuse dans la manière dont nous gérons la logique conditionnelle. Fini le temps où les vérifications de type et les comparaisons de valeurs nécessitaient de longues cascades if-else
ou des instructions switch
maladroites. L'introduction de capacités sophistiquées de correspondance de modèles (pattern matching), en particulier depuis C# 8.0, a fondamentalement changé la façon dont les développeurs écrivent le flux de contrôle, rendant le code simultanément plus expressif, plus concis et plus sûr.
La correspondance de modèles en C# offre une syntaxe concise pour tester une expression et effectuer des actions lorsque cette expression correspond à un modèle spécifique. Elle permet aux développeurs de tester des valeurs par rapport à divers critères, y compris leur type, leur valeur, leurs propriétés ou même la structure d'objets complexes. Cette capacité est principalement exposée via l'expression is
et l'expression switch
(ou l'instruction switch
). Bien que la vérification de type de base existait auparavant, C# 8.0 a introduit un vocabulaire de modèles plus riche, étendant considérablement son utilité et son impact sur les pratiques de codage quotidiennes. Cette amélioration est cruciale pour une correspondance de modèles switch
et if
en C# efficace, transformant une logique conditionnelle verbeuse en constructions compactes et compréhensibles.
Explorons quelques-uns des principaux types de modèles disponibles et comment ils simplifient les tâches de programmation courantes, en fournissant des exemples clairs de correspondance de modèles en C# qui illustrent leurs avantages.
La véritable puissance de la correspondance de modèles devient évidente lorsqu'on la compare aux approches traditionnelles. Les développeurs obtiennent des améliorations significatives en matière de lisibilité et de concision du code.
Le modèle de déclaration vérifie le type d'exécution d'une expression et, si elle correspond, affecte le résultat à une nouvelle variable. Cela élimine le besoin de transtypage explicite et de vérifications de null
dans de nombreux scénarios.
Avant :
public void ProcessAssetOld(object asset)
{
if (asset is SoftwareLicense)
{
SoftwareLicense license = (SoftwareLicense)asset;
Console.WriteLine($"Software License: {license.ProductName}, Expiration: {license.ExpirationDate.ToShortDateString()}");
}
else if (asset is HardwareComponent)
{
HardwareComponent hardware = (HardwareComponent)asset;
Console.WriteLine($"Hardware Component: {hardware.ComponentName}, Serial: {hardware.SerialNumber}");
}
else
{
Console.WriteLine("Unknown asset type.");
}
}
public class SoftwareLicense { public string ProductName { get; set; } public DateTime ExpirationDate { get; set; } }
public class HardwareComponent { public string ComponentName { get; set; } public string SerialNumber { get; set; } }
Après (avec le Modèle de Déclaration) :
public void ProcessAssetNew(object asset)
{
if (asset is SoftwareLicense license)
{
Console.WriteLine($"Software License: {license.ProductName}, Expiration: {license.ExpirationDate.ToShortDateString()}");
}
else if (asset is HardwareComponent hardware)
{
Console.WriteLine($"Hardware Component: {hardware.ComponentName}, Serial: {hardware.SerialNumber}");
}
else
{
Console.WriteLine("Unknown asset type.");
}
}
Les variables license
et hardware
ne sont dans la portée et affectées que si le modèle correspond, améliorant la sécurité des types et réduisant les erreurs d'exécution potentielles.
Similaire au modèle de déclaration, le modèle de type vérifie le type d'exécution d'une expression. Il est souvent utilisé dans les expressions switch
où une nouvelle déclaration de variable n'est pas strictement nécessaire pour le modèle lui-même.
Avant :
public decimal CalculateShippingCostOld(Shipment shipment)
{
if (shipment == null)
throw new ArgumentNullException(nameof(shipment));
if (shipment.GetType() == typeof(StandardShipment))
return 5.00m;
if (shipment.GetType() == typeof(ExpressShipment))
return 15.00m;
if (shipment.GetType() == typeof(InternationalShipment))
return 25.00m;
return 0.00m; // Default for unknown types
}
public abstract class Shipment { }
public class StandardShipment : Shipment { }
public class ExpressShipment : Shipment { }
public class InternationalShipment : Shipment { }
Après (avec le Modèle de Type dans l'expression switch
) :
public decimal CalculateShippingCostNew(Shipment shipment) => shipment switch
{
StandardShipment => 5.00m,
ExpressShipment => 15.00m,
InternationalShipment => 25.00m,
null => throw new ArgumentNullException(nameof(shipment)),
_ => 0.00m // Handle unknown shipment types
};
Cela démontre une correspondance de modèles switch case
C# concise pour différents types.
Un modèle de constante teste si le résultat d'une expression est égal à une valeur constante spécifiée. Cela simplifie les comparaisons de valeurs discrètes.
Avant :
public string GetOrderStateOld(OrderStatus status)
{
if (status == OrderStatus.Pending)
return "Order is awaiting processing.";
else if (status == OrderStatus.Shipped)
return "Order has been dispatched.";
else if (status == OrderStatus.Delivered)
return "Order has been delivered.";
else if (status == OrderStatus.Cancelled)
return "Order has been cancelled.";
else
return "Unknown order state.";
}
public enum OrderStatus { Pending, Shipped, Delivered, Cancelled, Returned }
Après (avec le Modèle de Constante) :
public string GetOrderStateNew(OrderStatus status) => status switch
{
OrderStatus.Pending => "Order is awaiting processing.",
OrderStatus.Shipped => "Order has been dispatched.",
OrderStatus.Delivered => "Order has been delivered.",
OrderStatus.Cancelled => "Order has been cancelled.",
_ => "Unknown order state."
};
C'est une manière élégante de gérer la correspondance de modèles de chaînes ou de valeurs d'énumération spécifiques en C#.
Les modèles relationnels comparent le résultat d'une expression à une constante en utilisant les opérateurs de comparaison (<
, >
, <=
, >=
). Cela rend les vérifications de plage beaucoup plus lisibles.
Avant :
public string GetEmployeePerformanceLevelOld(int score)
{
if (score < 50)
return "Needs Improvement";
else if (score >= 50 && score < 70)
return "Meets Expectations";
else if (score >= 70 && score < 90)
return "Exceeds Expectations";
else if (score >= 90)
return "Outstanding";
else
return "Invalid Score"; // Should not happen with int, but for completeness
}
Après (avec les Modèles Relationnels) :
public string GetEmployeePerformanceLevelNew(int score) => score switch
{
< 50 => "Needs Improvement",
>= 50 and < 70 => "Meets Expectations",
>= 70 and < 90 => "Exceeds Expectations",
>= 90 => "Outstanding",
_ => "Invalid Score" // Catches any unexpected integer values
};
Cela montre la puissance de la combinaison des modèles relationnels avec des modèles logiques pour la correspondance de modèles C#.
Le modèle de propriété permet de faire correspondre les propriétés ou les champs d'une expression à des modèles imbriqués. C'est incroyablement utile pour inspecter l'état des objets.
Avant :
public decimal CalculateOrderDiscountOld(CustomerOrder order)
{
if (order == null) return 0m;
if (order.TotalAmount >= 500 && order.Customer.IsPremium)
{
return 0.15m; // 15% for premium customers with large orders
}
else if (order.TotalAmount >= 100)
{
return 0.05m; // 5% for general large orders
}
return 0m;
}
public class Customer { public bool IsPremium { get; set; } }
public class CustomerOrder { public decimal TotalAmount { get; set; } public Customer Customer { get; set; } }
Après (avec le Modèle de Propriété) :
public decimal CalculateOrderDiscountNew(CustomerOrder order) => order switch
{
{ TotalAmount: >= 500, Customer.IsPremium: true } => 0.15m,
{ TotalAmount: >= 100 } => 0.05m,
null => 0m,
_ => 0m
};
Cela démontre une puissante correspondance de modèles de propriétés en C#, simplifiant les vérifications de l'état des objets.
Les modèles positionnels déconstruisent le résultat d'une expression (comme les tuples ou les enregistrements avec des méthodes Deconstruct
) et font correspondre les valeurs résultantes à des modèles imbriqués.
Avant :
public string DescribeTransactionOld(Transaction transaction)
{
if (transaction.Type == TransactionType.Deposit && transaction.Amount > 1000)
return "Large Deposit";
else if (transaction.Type == TransactionType.Withdrawal && transaction.Amount > 500)
return "Significant Withdrawal";
else if (transaction.Type == TransactionType.Fee && transaction.Amount > 0)
return "Applied Fee";
else
return "Standard Transaction";
}
public record Transaction(TransactionType Type, decimal Amount);
public enum TransactionType { Deposit, Withdrawal, Fee, Transfer }
Après (avec le Modèle Positionnel) :
public string DescribeTransactionNew(Transaction transaction) => transaction switch
{
(TransactionType.Deposit, > 1000m) => "Large Deposit",
(TransactionType.Withdrawal, > 500m) => "Significant Withdrawal",
(TransactionType.Fee, > 0m) => "Applied Fee",
_ => "Standard Transaction"
};
Cette forme concise améliore la lisibilité de la correspondance de modèles C# pour les types déconstructibles.
and
, or
, not
)Les modèles logiques combinent d'autres modèles en utilisant les mots-clés and
, or
et not
, permettant des vérifications conditionnelles complexes au sein d'un seul modèle.
Avant :
public bool CanGrantAccessOld(UserRole role, int securityLevel, bool hasTwoFactorAuth)
{
if ((role == UserRole.Administrator || role == UserRole.Manager) && securityLevel > 7 && hasTwoFactorAuth)
return true;
else if (role == UserRole.Developer && securityLevel > 5)
return true;
else if (role != UserRole.Guest && securityLevel >= 3 && securityLevel <= 10)
return true;
else
return false;
}
public enum UserRole { Guest, User, Developer, Manager, Administrator }
Après (avec les Modèles Logiques) :
public bool CanGrantAccessNew(UserRole role, int securityLevel, bool hasTwoFactorAuth) => (role, securityLevel, hasTwoFactorAuth) switch
{
(UserRole.Administrator or UserRole.Manager, > 7, true) => true,
(UserRole.Developer, > 5, _) => true,
(not UserRole.Guest, >= 3 and <= 10, _) => true,
_ => false
};
La correspondance de modèles is
C# combinée aux opérateurs logiques devient très expressive.
var
Le modèle var
correspond toujours à une expression et affecte son résultat à une nouvelle variable locale. Il est particulièrement utile lorsque vous devez capturer la valeur d'entrée entière d'une expression switch
(ou une partie d'un modèle complexe) pour effectuer des vérifications supplémentaires, plus complexes, au sein d'une clause when
, en particulier celles impliquant des appels de méthodes ou des propriétés qui ne peuvent pas être exprimées directement par d'autres modèles.
Avant :
using System.Collections.Generic;
using System.Linq;
public string AnalyzeNumberSequenceOld(IEnumerable<int> numbers)
{
if (numbers == null)
return "Invalid sequence (null)";
if (numbers.Count() == 0)
return "Empty sequence";
if (numbers.Count() > 5 && numbers.Average() > 10.0)
return "Long sequence with high average";
return "Normal sequence";
}
Après (avec le Modèle var
et la clause when
) :
using System.Collections.Generic;
using System.Linq;
public string AnalyzeNumberSequenceNew(IEnumerable<int> numbers) => numbers switch
{
null => "Invalid sequence (null)",
var list when list.Count() == 0 => "Empty sequence",
var list when list.Count() > 5 && list.Average() > 10.0 => "Long sequence with high average",
_ => "Normal sequence"
};
Ici, var list
capture l'instance IEnumerable<int>
. Cela permet d'exécuter des requêtes LINQ complexes ultérieures (comme Count()
et Average()
) directement sur la variable list
au sein de la clause when
, sans avoir besoin de vérifications de type explicites utilisant is
.
Introduits en C# 11, les modèles de liste permettent de faire correspondre des éléments dans une séquence (comme les tableaux ou List<T>
). Ils sont incroyablement puissants pour valider la structure et le contenu des collections.
Avant :
public bool ValidateLogEntryOld(string[] logParts)
{
if (logParts == null || logParts.Length < 2) return false;
if (logParts[0] == "ERROR" && logParts.Length >= 3 && logParts[1] == "AUTH" && logParts[2] == "FAILED")
return true;
if (logParts[0] == "INFO" && logParts.Length >= 2 && logParts[1] == "CONNECTED")
return true;
if (logParts[0] == "WARN" && logParts.Length >= 4 && logParts[1] == "DISK" && logParts[2] == "SPACE" && int.TryParse(logParts[3], out int space) && space < 10)
return true;
return false;
}
Après (avec les Modèles de Liste) :
public bool ValidateLogEntryNew(string[] logParts) => logParts switch
{
["ERROR", "AUTH", "FAILED", ..] => true, // Error authentication failed
["INFO", "CONNECTED", ..] => true, // Client connected
["WARN", "DISK", "SPACE", var size, ..] when int.TryParse(size, out int space) && space < 10 => true, // Low disk space
_ => false // Any other log entry
};
C'est une fonctionnalité puissante pour la correspondance de modèles sur les séquences, particulièrement utile dans des scénarios comme l'analyse des arguments de ligne de commande ou les entrées de journal avec des structures variables. Le ..
(modèle de tranche) correspond à zéro ou plusieurs éléments, offrant une flexibilité.
Les avantages de la correspondance de modèles s'étendent au-delà du simple sucre syntaxique. En adoptant ces fonctionnalités, les développeurs écrivent intrinsèquement un code qui est :
if-else
répétitifs et des casts explicites réduit le volume de code, ce qui entraîne moins de code à maintenir et moins d'opportunités d'erreurs.switch
, en particulier, nécessitent souvent une correspondance de modèles exhaustive. Le compilateur C# aide en avertissant les développeurs si une expression switch
ne couvre pas toutes les valeurs d'entrée possibles, aidant ainsi à prévenir les exceptions d'exécution. Cette exhaustivité imposée par le compilateur améliore la correction du logiciel.La correspondance de modèles a transformé C# en un langage qui offre des solutions élégantes pour la gestion complexe des données et le flux de contrôle. Des vérifications de type de base à la déconstruction d'objets et à la validation de séquences, les capacités offertes par C# 8.0+ sont des outils essentiels pour tout développeur de logiciels moderne. Adopter ces fonctionnalités puissantes améliore non seulement la qualité du code, mais rationalise également le processus de développement, contribuant à des applications plus fiables et maintenables. Les développeurs qui s'appuient encore sur d'anciens styles d'instructions if-else
ou switch
peuvent grandement bénéficier de l'incorporation de ces modèles, rendant leurs programmes C# plus robustes et agréables à utiliser.