Utilisez des critères spéciaux pour générer le comportement de votre classe pour un meilleur codeUse pattern matching to build your class behavior for better code

Les fonctionnalités de critères spéciaux en C# fournissent une syntaxe pour exprimer vos algorithmes.The pattern matching features in C# provide syntax to express your algorithms. Vous pouvez utiliser ces techniques pour implémenter le comportement dans vos classes.You can use these techniques to implement the behavior in your classes. Vous pouvez combiner la conception de classes orientée objet avec une implémentation orientée données pour fournir un code concis lors de la modélisation d’objets réels.You can combine object-oriented class design with a data-oriented implementation to provide concise code while modeling real-world objects.

Ce didacticiel vous montre comment effectuer les opérations suivantes :In this tutorial, you'll learn how to:

  • Exprimez vos classes orientées objet à l’aide de modèles de données.Express your object oriented classes using data patterns.
  • Implémentez ces modèles à l’aide des fonctionnalités de critères spéciaux de C#.Implement those patterns using C#'s pattern matching features.
  • Tirez parti des diagnostics du compilateur pour valider votre implémentation.Leverage compiler diagnostics to validate your implementation.

PrérequisPrerequisites

Vous devez configurer votre ordinateur pour exécuter .NET 5, y compris le compilateur C# 9,0.You’ll need to set up your machine to run .NET 5, including the C# 9.0 compiler. Le compilateur C# 9,0 est disponible à partir de Visual Studio 2019 version 16,8 Preview ou de la version préliminaire du kit de développement logiciel (SDK) .net 5,0.The C# 9.0 compiler is available starting with Visual Studio 2019 version 16.8 preview or the .NET 5.0 SDK preview.

Créer une simulation d’un canal de verrouillageBuild a simulation of a canal lock

Dans ce didacticiel, vous allez générer une classe C# qui simule un verrou de canal.In this tutorial, you'll build a C# class that simulates a canal lock. Brièvement, un canal de verrouillage est un appareil qui soulève et abaisse les canots lorsqu’ils se déplacent entre deux étirés d’eau à différents niveaux.Briefly, a canal lock is a device that raises and lowers boats as they travel between two stretches of water at different levels. Un verrou a deux portes et un mécanisme pour modifier le niveau d’eau.A lock has two gates and some mechanism to change the water level.

Dans son fonctionnement normal, un bateau entre l’un des portails, tandis que le niveau d’eau du verrou correspond au niveau de l’eau sur le côté du bateau.In its normal operation, a boat enters one of the gates while the water level in the lock matches the water level on the side the boat enters. Une fois dans le verrou, le niveau d’eau est modifié pour correspondre au niveau d’eau où le bateau laisse le verrou.Once in the lock, the water level is changed to match the water level where the boat will leave the lock. Une fois que le niveau d’eau correspond à ce côté, la porte du côté de sortie s’ouvre.Once the water level matches that side, the gate on the exit side opens. Les mesures de sécurité assurent qu’un opérateur ne peut pas créer une situation dangereuse dans le canal.Safety measures make sure an operator can't create a dangerous situation in the canal. Le niveau d’eau ne peut être modifié que lorsque les deux portes sont fermées.The water level can be changed only when both gates are closed. Au plus une porte peut être ouverte.At most one gate can be open. Pour ouvrir une porte, le niveau d’eau dans le verrou doit correspondre au niveau d’eau en dehors de la porte qui est ouverte.To open a gate, the water level in the lock must match the water level outside the gate being opened.

Vous pouvez générer une classe C# pour modéliser ce comportement.You can build a C# class to model this behavior. Une CanalLock classe prend en charge les commandes pour ouvrir ou fermer l’une ou l’autre des portes.A CanalLock class would support commands to open or close either gate. Il aurait d’autres commandes pour augmenter ou diminuer l’eau.It would have other commands to raise or lower the water. La classe doit également prendre en charge les propriétés pour lire l’état actuel des portes et du niveau d’eau.The class should also support properties to read the current state of both gates and the water level. Vos méthodes implémentent les mesures de sécurité.Your methods implement the safety measures.

Définir une classeDefine a class

Vous allez générer une application console pour tester votre CanalLock classe.You'll build a console application to test your CanalLock class. Créez un nouveau projet de console pour .NET 5 à l’aide de Visual Studio ou de l’interface de commande .NET.Create a new console project for .NET 5 using either Visual Studio or the .NET CLI. Ensuite, ajoutez une nouvelle classe et nommez-la CanalLock .Then, add a new class and name it CanalLock. Ensuite, concevez votre API publique, mais laissez les méthodes non implémentées :Next, design your public API, but leave the methods not implemented:

public enum WaterLevel
{
    Low,
    High
}
public class CanalLock
{
    // Query canal lock state:
    public WaterLevel CanalLockWaterLevel { get; private set; } = WaterLevel.Low;
    public bool HighWaterGateOpen { get; private set; } = false;
    public bool LowWaterGateOpen { get; private set; } = false;

    // Change the upper gate.
    public void SetHighGate(bool open)
    {
        throw new NotImplementedException();
    }

    // Change the lower gate.
    public void SetLowGate(bool open)
    {
        throw new NotImplementedException();
    }

    // Change water level.
    public void SetWaterLevel(WaterLevel newLevel)
    {
        throw new NotImplementedException();
    }

    public override string ToString() =>
        $"The lower gate is {(LowWaterGateOpen ? "Open" : "Closed")}. " +
        $"The upper gate is {(HighWaterGateOpen ? "Open" : "Closed")}. " +
        $"The water level is {CanalLockWaterLevel}.";
}

Le code précédent Initialise l’objet afin que les deux portes soient fermées et que le niveau d’eau soit faible.The preceding code initializes the object so both gates are closed, and the water level is low. Ensuite, écrivez le code de test suivant dans votre Main méthode pour vous guider lors de la création d’une première implémentation de la classe :Next, write the following test code in your Main method to guide you as you create a first implementation of the class:

// Create a new canal lock:
var canalGate = new CanalLock();

// State should be doors closed, water level low:
Console.WriteLine(canalGate);

canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate:  {canalGate}");

Console.WriteLine("Boat enters lock from lower gate");

canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate:  {canalGate}");

canalGate.SetWaterLevel(WaterLevel.High);
Console.WriteLine($"Raise the water level: {canalGate}");
Console.WriteLine(canalGate);

canalGate.SetHighGate(open: true);
Console.WriteLine($"Open the higher gate:  {canalGate}");

Console.WriteLine("Boat exits lock at upper gate");
Console.WriteLine("Boat enters lock from upper gate");

canalGate.SetHighGate(open: false);
Console.WriteLine($"Close the higher gate: {canalGate}");

canalGate.SetWaterLevel(WaterLevel.Low);
Console.WriteLine($"Lower the water level: {canalGate}");

canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate:  {canalGate}");

Console.WriteLine("Boat exits lock at upper gate");

canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate:  {canalGate}");

Ensuite, ajoutez une première implémentation de chaque méthode dans la CanalLock classe.Next, add a first implementation of each method in the CanalLock class. Le code suivant implémente les méthodes de la classe sans se préoccuper des règles de sécurité.The following code implements the methods of the class without concern to the safety rules. Vous ajouterez des tests de sécurité plus tard :You'll add safety tests later:

// Change the upper gate.
public void SetHighGate(bool open)
{
    HighWaterGateOpen = open;
}

// Change the lower gate.
public void SetLowGate(bool open)
{
    LowWaterGateOpen = open;
}

// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
    CanalLockWaterLevel = newLevel;
}

Les tests que vous avez écrits jusqu’à présent passent.The tests you've written so far pass. Vous avez implémenté les concepts de base.You've implemented the basics. À présent, écrivez un test pour la première condition d’échec.Now, write a test for the first failure condition. À la fin des tests précédents, les deux portes sont fermées et le niveau d’eau est défini sur faible.At the end of the previous tests, both gates are closed, and the water level is set to low. Ajoutez un test pour essayer d’ouvrir la porte supérieure :Add a test to try opening the upper gate:

Console.WriteLine("=============================================");
Console.WriteLine("     Test invalid commands");
// Open "wrong" gate (2 tests)
try
{
    canalGate = new CanalLock();
    canalGate.SetHighGate(open: true);
}
catch (InvalidOperationException)
{
    Console.WriteLine("Invalid operation: Can't open the high gate. Water is low.");
}
Console.WriteLine($"Try to open upper gate: {canalGate}");

Ce test échoue car la porte s’ouvre.This test fails because the gate opens. En guise de première implémentation, vous pouvez résoudre le problème à l’aide du code suivant :As a first implementation, you could fix it with the following code:

// Change the upper gate.
public void SetHighGate(bool open)
{
    if (open && (CanalLockWaterLevel == WaterLevel.High))
        HighWaterGateOpen = true;
    else if (open && (CanalLockWaterLevel == WaterLevel.Low))
        throw new InvalidOperationException("Cannot open high gate when the water is low");
}

Vos tests réussissent.Your tests pass. Toutefois, à mesure que vous ajoutez d’autres tests, vous ajouterez de plus en plus de if clauses et testerez différentes propriétés.But, as you add more tests, you'll add more and more if clauses and test different properties. Bientôt, ces méthodes seront trop compliquées à mesure que vous ajouterez des conditions supplémentaires.Soon, these methods will get too complicated as you add more conditionals.

Implémenter les commandes avec des modèlesImplement the commands with patterns

Une meilleure méthode consiste à utiliser des modèles pour déterminer si l’objet est dans un état valide pour exécuter une commande.A better way is to use patterns to determine if the object is in a valid state to execute a command. Vous pouvez exprimer si une commande est autorisée en tant que fonction de trois variables : l’état de la porte, le niveau de l’eau et le nouveau paramètre :You can express if a command is allowed as a function of three variables: the state of the gate, the level of the water, and the new setting:

Nouveau paramètreNew setting État de la porteGate state Niveau d’eauWater Level RésultatsResult
FerméClosed FerméClosed ImportanteHigh FerméClosed
FerméClosed FerméClosed FaibleLow FerméClosed
FerméClosed OuvrirOpen ImportanteHigh OuvrirOpen
FerméClosed OuvrirOpen FaibleLow FerméClosed
OuvrirOpen FerméClosed ImportanteHigh OuvrirOpen
OuvrirOpen FerméClosed FaibleLow Fermé (erreur)Closed (Error)
OuvrirOpen OuvrirOpen ImportanteHigh OuvrirOpen
OuvrirOpen OuvrirOpen FaibleLow Fermé (erreur)Closed (Error)

Les quatrième et dernière lignes de la table ont du texte barré, car elles ne sont pas valides.The fourth and last rows in the table have strike through text because they're invalid. Le code que vous ajoutez maintenant doit s’assurer que la porte de haute-mer n’est jamais ouverte lorsque l’eau est faible.The code you're adding now should make sure the high water gate is never opened when the water is low. Ces États peuvent être codés comme une expression de commutateur unique (n’oubliez pas que false indique « fermé ») :Those states can be coded as a single switch expression (remember that false indicates "Closed"):

HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
{
    (false, false, WaterLevel.High) => false,
    (false, false, WaterLevel.Low) => false,
    (false, true, WaterLevel.High) => false,
    (false, true, WaterLevel.Low) => false, // should never happen
    (true, false, WaterLevel.High) => true,
    (true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
    (true, true, WaterLevel.High) => true,
    (true, true, WaterLevel.Low) => false, // should never happen
};

Essayez cette version.Try this version. Vos tests réussissent, en validant le code.Your tests pass, validating the code. Le tableau complet indique les combinaisons possibles d’entrées et de résultats.The full table shows the possible combinations of inputs and results. Cela signifie que vous et d’autres développeurs pouvez consulter rapidement le tableau et voir que vous avez couvert toutes les entrées possibles.That means you and other developers can quickly look at the table and see that you've covered all the possible inputs. Encore plus facile, le compilateur peut également vous aider.Even easier, the compiler can help as well. Après avoir ajouté le code précédent, vous pouvez voir que le compilateur génère un avertissement : CS8524 indique que l’expression de switch ne couvre pas toutes les entrées possibles.After you add the previous code, you can see that the compiler generates a warning: CS8524 indicates the switch expression doesn't cover all possible inputs. La raison de cet avertissement est que l’une des entrées est un enum type.The reason for that warning is that one of the inputs is an enum type. Le compilateur interprète « toutes les entrées possibles » comme toutes les entrées du type sous-jacent, généralement int .The compiler interprets "all possible inputs" as all inputs from the underlying type, typically an int. Cette switch expression vérifie uniquement les valeurs déclarées dans le enum .This switch expression only checks the values declared in the enum. Pour supprimer l’avertissement, vous pouvez ajouter un modèle de rejet tous pour le dernier ARM de l’expression.To remove the warning, you can add a catch-all discard pattern for the last arm of the expression. Cette condition lève une exception, car elle indique une entrée non valide :This condition throws an exception, because it indicates invalid input:

_  => throw new InvalidOperationException("Invalid internal state"),

Le bras du commutateur précédent doit être le dernier dans votre switch expression, car il correspond à toutes les entrées.The preceding switch arm must be last in your switch expression because it matches all inputs. Essayez en la déplaçant plus tôt dans l’ordre.Experiment by moving it earlier in the order. Cela provoque une erreur du compilateur CS8510 pour le code inaccessible dans un modèle.That causes a compiler error CS8510 for unreachable code in a pattern. La structure naturelle des expressions de commutateur permet au compilateur de générer des erreurs et des avertissements pour les erreurs éventuelles.The natural structure of switch expressions enables the compiler to generate errors and warnings for possible mistakes. Le « filet de sécurité » du compilateur facilite la création d’un code correct en moins d’itérations, et la liberté de combiner des bras de commutateur avec des caractères génériques.The compiler "safety net" makes it easier for you to create correct code in fewer iterations, and the freedom to combine switch arms with wildcards. Le compilateur émettra des erreurs si votre combinaison produit des bras inaccessibles et des avertissements si vous supprimez un ARM qui est nécessaire.The compiler will issue errors if your combination results in unreachable arms you didn't expect, and warnings if you remove an arm that's needed.

La première modification consiste à combiner tous les bras où la commande doit fermer la porte ; Cette autorisation est toujours autorisée.The first change is to combine all the arms where the command is to close the gate; that's always allowed. Ajoutez le code suivant en tant que premier bras dans votre expression de commutateur :Add the following code as the first arm in your switch expression:

(false, _, _) => false,

Une fois que vous avez ajouté le bras du commutateur précédent, vous obtenez quatre erreurs de compilation, une sur chacun des bras où la commande est false .After you add the previous switch arm, you'll get four compiler errors, one on each of the arms where the command is false. Ces bras sont déjà couverts par le bras qui vient d’être ajouté.Those arms are already covered by the newly added arm. Vous pouvez supprimer ces quatre lignes en toute sécurité.You can safely remove those four lines. Vous avez souhaité que ce nouveau bras de commutateur remplace ces conditions.You intended this new switch arm to replace those conditions.

Ensuite, vous pouvez simplifier les quatre bras où la commande doit ouvrir la porte.Next, you can simplify the four arms where the command is to open the gate. Dans les deux cas où le niveau d’eau est élevé, la porte peut être ouverte.In both cases where the water level is high, the gate can be opened. (Dans un, il est déjà ouvert.) Un cas où le niveau d’eau est faible lève une exception et l’autre ne doit pas se produire.(In one, it's already open.) One case where the water level is low throws an exception, and the other shouldn't happen. Il doit être possible de lever la même exception en toute sécurité si le verrouillage de l’eau est déjà dans un État non valide.It should be safe to throw the same exception if the water lock is already in an invalid state. Vous pouvez effectuer les simplifications suivantes pour ces bras :You can make the following simplifications for those arms:

(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),

Exécutez à nouveau vos tests, et ils réussissent.Run your tests again, and they pass. Voici la version finale de la SetHighGate méthode :Here's the final version of the SetHighGate method:

// Change the upper gate.
public void SetHighGate(bool open)
{
    HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
    {
        (false, _,    _)               => false,
        (true, _,     WaterLevel.High) => true,
        (true, false, WaterLevel.Low)  => throw new InvalidOperationException("Cannot open high gate when the water is low"),
        _                              => throw new InvalidOperationException("Invalid internal state"),
    };
}

Implémentez vous-même des modèlesImplement patterns yourself

Maintenant que vous avez vu la technique, renseignez les SetLowGate SetWaterLevel méthodes et vous-même.Now that you've seen the technique, fill in the SetLowGate and SetWaterLevel methods yourself. Commencez par ajouter le code suivant pour tester les opérations non valides sur ces méthodes :Start by adding the following code to test invalid operations on those methods:

Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetWaterLevel(WaterLevel.High);
    canalGate.SetLowGate(open: true);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't open the lower gate. Water is high.");
}
Console.WriteLine($"Try to open lower gate: {canalGate}");
// change water level with gate open (2 tests)
Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetLowGate(open: true);
    canalGate.SetWaterLevel(WaterLevel.High);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't raise water when the lower gate is open.");
}
Console.WriteLine($"Try to raise water with lower gate open: {canalGate}");
Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetWaterLevel(WaterLevel.High);
    canalGate.SetHighGate(open: true);
    canalGate.SetWaterLevel(WaterLevel.Low);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't lower water when the high gate is open.");
}
Console.WriteLine($"Try to lower water with high gate open: {canalGate}");

Réexécutez votre application.Run your application again. Vous pouvez voir que les nouveaux tests échouent et que le canal de verrouillage est dans un État non valide.You can see the new tests fail, and the canal lock gets into an invalid state. Essayez d’implémenter vous-même les méthodes restantes.Try to implement the remaining methods yourself. La méthode permettant de définir la porte inférieure doit être semblable à la méthode pour définir la porte supérieure.The method to set the lower gate should be similar to the method to set the upper gate. La méthode qui modifie le niveau d’eau a des contrôles différents, mais elle doit suivre une structure similaire.The method that changes the water level has different checks, but should follow a similar structure. Il peut s’avérer utile d’utiliser le même processus pour la méthode qui définit le niveau d’eau.You may find it helpful to use the same process for the method that sets the water level. Commencez avec les quatre entrées : l’état des deux portes, l’état actuel du niveau d’eau et le nouveau niveau d’eau demandé.Start with all four inputs: The state of both gates, the current state of the water level, and the requested new water level. L’expression de Switch doit commencer par :The switch expression should start with:

CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
    // elided
};

Vous aurez 16 bras de commutateur total à remplir.You'll have 16 total switch arms to fill in. Ensuite, testez et simplifiez.Then, test and simplify.

Avez-vous fait des méthodes comme ceci ?Did you make methods something like this?

// Change the lower gate.
public void SetLowGate(bool open)
{
    LowWaterGateOpen = (open, LowWaterGateOpen, CanalLockWaterLevel) switch
    {
        (false, _, _) => false,
        (true, _, WaterLevel.Low) => true,
        (true, false, WaterLevel.High) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
        _ => throw new InvalidOperationException("Invalid internal state"),
    };
}

// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
    CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
    {
        (WaterLevel.Low, WaterLevel.Low, true, false) => WaterLevel.Low,
        (WaterLevel.High, WaterLevel.High, false, true) => WaterLevel.High,
        (WaterLevel.Low, _, false, false) => WaterLevel.Low,
        (WaterLevel.High, _, false, false) => WaterLevel.High,
        (WaterLevel.Low, WaterLevel.High, false, true) => throw new InvalidOperationException("Cannot lower water when the high gate is open"),
        (WaterLevel.High, WaterLevel.Low, true, false) => throw new InvalidOperationException("Cannot raise water when the low gate is open"),
        _ => throw new InvalidOperationException("Invalid internal state"),
    };
}

Vos tests doivent réussir et le canal de verrouillage doit fonctionner en toute sécurité.Your tests should pass, and the canal lock should operate safely.

RésuméSummary

Dans ce didacticiel, vous avez appris à utiliser des critères spéciaux pour vérifier l’état interne d’un objet avant d’appliquer les modifications apportées à cet État.In this tutorial, you learned to use pattern matching to check the internal state of an object before applying any changes to that state. Vous pouvez vérifier les combinaisons de propriétés.You can check combinations of properties. Une fois que vous avez créé des tables pour l’une de ces transitions, vous testez votre code, puis vous simplifiez la lisibilité et la maintenabilité.Once you've built tables for any of those transitions, you test your code, then simplify for readability and maintainability. Ces refactorisations initiales peuvent suggérer des refactorisations supplémentaires qui valident l’état interne ou gèrent d’autres modifications de l’API.These initial refactorings may suggest further refactorings that validate internal state or manage other API changes. Ce didacticiel combine des classes et des objets avec une approche plus orientée données et basée sur les modèles pour implémenter ces classes.This tutorial combined classes and objects with a more data-oriented, pattern-based approach to implement those classes.