Vue d’ensemble des critères spéciaux

Les critères spéciaux sont une technique dans laquelle vous testez une expression pour déterminer si elle présente certaines caractéristiques. Les critères spéciaux C# fournissent une syntaxe plus concise pour tester des expressions et prendre des mesures lorsqu’une expression correspond. L’ « expressionis » prend en charge les critères spéciaux pour tester une expression et déclarer conditionnellement une nouvelle variable au résultat de cette expression. L' « expression switch » vous permet d’effectuer des actions basées sur les premiers critères spéciaux pour une expression. Ces deux expressions prennent en charge un vocabulaire riche de modèles.

Cet article fournit une vue d’ensemble des scénarios dans lesquels vous pouvez utiliser les critères spéciaux. Ces techniques peuvent améliorer la lisibilité et l’exactitude de votre code. Pour une présentation complète de tous les modèles que vous pouvez appliquer, consultez l’article sur les modèles dans la référence sur le langage.

Vérifications de la valeur Null

L’un des scénarios les plus courants de critères spéciaux consiste à s’assurer que les valeurs ne sont pas null. Vous pouvez tester et convertir un type de valeur acceptant Null en son type sous-jacent tout en testant null en utlisant de l’exemple suivant :

int? maybe = 12;

if (maybe is int number)
{
    Console.WriteLine($"The nullable int 'maybe' has the value {number}");
}
else
{
    Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
}

Le code précédent est un modèle de déclaration pour tester le type de la variable et l’affecter à une nouvelle variable. Les règles du langage rendent cette technique plus sûre que bien d’autres. La variable number est uniquement accessible et affectée dans la partie vraie de la clause if. Si vous essayez d’y accéder ailleurs, soit dans la clause else, soit après le bloc if, le compilateur émet une erreur. Deuxièmement, comme vous n’utilisez pas l’opérateur ==, ce modèle fonctionne quand un type surcharge l’opérateur ==. Cela en fait un moyen idéal de vérifier les valeurs de référence null, en ajoutant le modèle not :

string? message = ReadMessageOrDefault();

if (message is not null)
{
    Console.WriteLine(message);
}

L’exemple précédent utilisait un modèle constant pour comparer la variable à null. not est un modèle logique qui correspond lorsque le modèle annulé ne correspond pas.

Tests de type

Une autre utilisation courante pour les critères spéciaux consiste à tester une variable pour voir si elle correspond à un type donné. Par exemple, le code suivant teste si une variable n’est pas null et implémente l’interface System.Collections.Generic.IList<T>. Si c’est le cas, il utilise la propriété ICollection<T>.Count de cette liste pour rechercher l’index central. Le modèle de déclaration ne correspond pas à une valeur null, quel que soit le type de compilation de la variable. Le code ci-dessous protège contre null, en plus de la protection contre un type qui n’implémente pas IList.

public static T MidPoint<T>(IEnumerable<T> sequence)
{
    if (sequence is IList<T> list)
    {
        return list[list.Count / 2];
    }
    else if (sequence is null)
    {
        throw new ArgumentNullException(nameof(sequence), "Sequence can't be null.");
    }
    else
    {
        int halfLength = sequence.Count() / 2 - 1;
        if (halfLength < 0) halfLength = 0;
        return sequence.Skip(halfLength).First();
    }
}

Les mêmes tests peuvent être appliqués dans une expression switch pour tester une variable sur plusieurs types différents. Vous pouvez utiliser ces informations pour créer de meilleurs algorithmes en fonction du type d’exécution spécifique.

Comparer des valeurs discrètes

Vous pouvez également tester une variable pour trouver une correspondance sur des valeurs spécifiques. Le code suivant montre un exemple où vous testez une valeur par rapport à toutes les valeurs possibles déclarées dans une énumération :

public State PerformOperation(Operation command) =>
   command switch
   {
       Operation.SystemTest => RunDiagnostics(),
       Operation.Start => StartSystem(),
       Operation.Stop => StopSystem(),
       Operation.Reset => ResetToReady(),
       _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),
   };

L’exemple précédent illustre un dispatch de méthode basé sur la valeur d’une énumération. Le dernier cas _ est un modèle discard qui correspond à toutes les valeurs. Il gère toutes les conditions d’erreur où la valeur ne correspond pas à l’une des valeurs définies enum. Si vous omettez cette branche switch, le compilateur vous signale que l’expression de votre modèle ne permet pas de gérer toutes les valeurs d’entrée possibles. Au moment de l’exécution, l’expression switch lève une exception si l’objet examiné ne correspond à aucun des bras de commutateur. Vous pouvez utiliser des constantes numériques au lieu d’un ensemble de valeurs d’énumération. Vous pouvez également utiliser cette technique similaire pour les valeurs de chaîne constante qui représentent les commandes :

public State PerformOperation(string command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

L’exemple précédent montre le même algorithme, mais utilise des valeurs de chaîne au lieu d’une énumération. Vous pouvez utiliser ce scénario si votre application répond à des commandes de texte au lieu d’un format de données standard. À compter de C# 11, vous pouvez également utiliser Span<char> ou ReadOnlySpan<char> pour tester les valeurs de chaîne constante, comme indiqué dans l’exemple suivant :

public State PerformOperation(ReadOnlySpan<char> command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

Dans tous ces exemples, le modèle discard garantit que vous gérez chaque entrée. Le compilateur vous aide en vous assurant que chaque valeur d’entrée possible est gérée.

Modèles relationnels

Vous pouvez utiliser des modèles relationnels pour tester la comparaison d’une valeur à des constantes. Par exemple, le code suivant retourne l’état de l’eau en fonction de la température en Fahrenheit :

string WaterState(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        (> 32) and (< 212) => "liquid",
        < 32 => "solid",
        > 212 => "gas",
        32 => "solid/liquid transition",
        212 => "liquid / gas transition",
    };

Le code précédent illustre également le andmodèle logique conjonctif pour vérifier que les deux modèles relationnels correspondent. Vous pouvez également utiliser un modèle disjonctif or pour vérifier que l’un ou l’autre modèle correspond. Les deux modèles relationnels sont entourés de parenthèses, que vous pouvez utiliser autour de n’importe quel modèle pour plus de clarté. Les deux derniers bras de commutateur gèrent les cas pour le point de fusion et le point d’ébullition. Sans ces deux bras, le compilateur vous avertit que votre logique ne couvre pas toutes les entrées possibles.

Le code précédent illustre également une autre fonctionnalité importante que le compilateur fournit pour les expressions de correspondance de modèle : le compilateur vous avertit si vous ne gérez pas chaque valeur d’entrée. Le compilateur vous signale également si le modèle d’une branche switch est déjà couvert par un modèle précédent. Cela vous donne la liberté de refactoriser et de réorganiser les expressions de commutateur. Une autre façon d’écrire la même expression peut être :

string WaterState2(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        < 32 => "solid",
        32 => "solid/liquid transition",
        < 212 => "liquid",
        212 => "liquid / gas transition",
        _ => "gas",
};

La leçon clé de l’exemple précédent, ainsi que toute autre refactorisation ou réorganisation, est que le compilateur valide que votre code gère toutes les entrées possibles.

Entrées multiples

Tous les modèles abordés jusqu’à présent ont vérifié une entrée. Vous pouvez écrire des modèles qui examinent plusieurs propriétés d’un objet. Prenez l’enregistrement Order suivant en compte :

public record Order(int Items, decimal Cost);

Le type d’enregistrement positionnel précédent déclare deux membres à des positions explicites. S’affiche d’abord Items, puis la commande Cost. Pour plus d’informations, consultez Enregistrements.

Le code suivant examine le nombre d’éléments et la valeur d’une commande pour calculer un prix réduit :

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        { Items: > 10, Cost: > 1000.00m } => 0.10m,
        { Items: > 5, Cost: > 500.00m } => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

Les deux premiers bras examinent deux propriétés de Order. Le troisième examine uniquement le coût. La suivante vérifie par rapport à null, et la finale correspond à toute autre valeur. Si le type Order définit une méthode appropriée Deconstruct, vous pouvez omettre les noms de propriétés du modèle et utiliser la déconstruction pour examiner les propriétés :

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        ( > 10,  > 1000.00m) => 0.10m,
        ( > 5, > 50.00m) => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

Le code précédent illustre le modèle positionnel où les propriétés sont déconstruites pour l’expression.

Modèles de liste

Vous pouvez vérifier les éléments d’une liste ou d’un tableau à l’aide d’un modèle de liste. Un modèle de liste permet d’appliquer un modèle à n’importe quel élément d’une séquence. En outre, vous pouvez appliquer le modèle discard (_) pour qu’il corresponde à n’importe quel élément, ou appliquer un modèle de tranche pour correspondre à zéro ou plusieurs éléments.

Les modèles de liste sont un outil précieux lorsque les données ne suivent pas une structure régulière. Vous pouvez utiliser les critères spéciaux pour tester la forme et les valeurs des données au lieu de les transformer en un ensemble d’objets.

Considérez l’extrait suivant d’un fichier texte contenant des transactions bancaires :

04-01-2020, DEPOSIT,    Initial deposit,            2250.00
04-15-2020, DEPOSIT,    Refund,                      125.65
04-18-2020, DEPOSIT,    Paycheck,                    825.65
04-22-2020, WITHDRAWAL, Debit,           Groceries,  255.73
05-01-2020, WITHDRAWAL, #1102,           Rent, apt, 2100.00
05-02-2020, INTEREST,                                  0.65
05-07-2020, WITHDRAWAL, Debit,           Movies,      12.57
04-15-2020, FEE,                                       5.55

Il s’agit d’un format CSV, mais certaines lignes ont plus de colonnes que d’autres. Pire encore pour le traitement, une colonne du type WITHDRAWAL contient du texte généré par l’utilisateur et peut contenir une virgule dans le texte. Un modèle de liste qui comprend le modèle discard, le modèle constant et le modèle var pour capturer les données de la valeur traite les données dans ce format :

decimal balance = 0m;
foreach (string[] transaction in ReadRecords())
{
    balance += transaction switch
    {
        [_, "DEPOSIT", _, var amount]     => decimal.Parse(amount),
        [_, "WITHDRAWAL", .., var amount] => -decimal.Parse(amount),
        [_, "INTEREST", var amount]       => decimal.Parse(amount),
        [_, "FEE", var fee]               => -decimal.Parse(fee),
        _                                 => throw new InvalidOperationException($"Record {string.Join(", ", transaction)} is not in the expected format!"),
    };
    Console.WriteLine($"Record: {string.Join(", ", transaction)}, New balance: {balance:C}");
}

L’exemple précédent prend un tableau de chaînes, où chaque élément est un champ dans la ligne. Les clés d’expression switch sur le deuxième champ, qui détermine le type de transaction et le nombre de colonnes restantes. Chaque ligne garantit que les données sont au bon format. Le modèle discard (_) ignore le premier champ, avec la date de la transaction. Le deuxième champ correspond au type de transaction. L’élément restant correspond au champ avec le montant. La correspondance finale utilise le modèle var pour capturer la représentation sous forme de chaîne du montant. L’expression calcule le montant à ajouter ou à soustraire du solde.

Les modèles de liste vous permettent de faire correspondre sur la forme d’une séquence d’éléments de données. Vous utilisez les modèles discard et tranche pour correspondre à l’emplacement des éléments. Vous utilisez d’autres modèles pour faire correspondre les caractéristiques des éléments individuels.

Cet article fournit une présentation des types de code que vous pouvez écrire avec les critères spéciaux en C#. Les articles suivants présentent d’autres exemples d’utilisation de modèles dans des scénarios et le vocabulaire complet des modèles disponibles.

Voir aussi