Verwenden des Musterabgleichs zum Gestalten des Verhaltens von Klassen für besseren CodeUse pattern matching to build your class behavior for better code

Die Features für den Musterabgleich in C# stellen die Syntax zum Ausdrücken Ihrer Algorithmen bereit.The pattern matching features in C# provide syntax to express your algorithms. Mithilfe dieser Techniken können Sie das Verhalten in Ihren Klassen implementieren.You can use these techniques to implement the behavior in your classes. Sie können einen objektorientierten Klassenentwurf mit einer datenorientierten Implementierung kombinieren, um bei der Modellierung von Objekten aus der realen Welt präzisen Code bereitzustellen.You can combine object-oriented class design with a data-oriented implementation to provide concise code while modeling real-world objects.

In diesem Tutorial lernen Sie Folgendes:In this tutorial, you'll learn how to:

  • Ausdrücken Ihrer objektorientierten Klassen mithilfe von DatenmusternExpress your object oriented classes using data patterns.
  • Implementieren dieser Muster mithilfe der C#-Features zum MusterabgleichImplement those patterns using C#'s pattern matching features.
  • Nutzen der Compilerdiagnose zum Überprüfen Ihrer ImplementierungLeverage compiler diagnostics to validate your implementation.

VoraussetzungenPrerequisites

Sie müssen Ihren Computer zur Ausführung von .NET 5 einrichten, einschließlich des C# 9.0-Compilers.You’ll need to set up your machine to run .NET 5, including the C# 9.0 compiler. Der C# 9.0-Compiler steht ab Visual Studio 2019 Version 16.8 Preview oder .NET 5.0 SDK Preview zur Verfügung.The C# 9.0 compiler is available starting with Visual Studio 2019 version 16.8 preview or the .NET 5.0 SDK preview.

Erstellen einer Simulation einer KanalschleuseBuild a simulation of a canal lock

In diesem Tutorial erstellen Sie eine C#-Klasse, die eine Kanalschleuse simuliert.In this tutorial, you'll build a C# class that simulates a canal lock. Eine Kanalschleuse ist, kurz gesagt, eine Anlage, die Schiffe hebt und senkt, wenn sie sich zwischen zwei unterschiedlich hohen Wasserständen bewegen.Briefly, a canal lock is a device that raises and lowers boats as they travel between two stretches of water at different levels. Eine Schleuse hat zwei Tore und einen Mechanismus zur Änderung des Wasserstands.A lock has two gates and some mechanism to change the water level.

Im Normalbetrieb fährt ein Schiff in eines der Tore ein, wenn der Wasserstand in der Schleuse dem Wasserstand auf der Seite entspricht, auf der das Schiff hineinfährt.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. Einmal in der Schleuse wird der Wasserstand entsprechend dem Wasserstand geändert, bei dem das Boot die Schleuse verlässt.Once in the lock, the water level is changed to match the water level where the boat will leave the lock. Sobald der Wasserstand mit dieser Seite übereinstimmt, öffnet sich das Tor auf der Ausfahrtseite.Once the water level matches that side, the gate on the exit side opens. Sicherheitsmaßnahmen stellen sicher, dass ein Schiffsführer keine gefährliche Situation im Kanal herbeiführen kann.Safety measures make sure an operator can't create a dangerous situation in the canal. Der Wasserstand kann nur geändert werden, wenn beide Tore geschlossen sind.The water level can be changed only when both gates are closed. Es darf höchstens ein Tor geöffnet sein.At most one gate can be open. Um ein Tor zu öffnen, muss der Wasserstand in der Schleuse mit dem Wasserstand außerhalb des zu öffnenden Tores übereinstimmen.To open a gate, the water level in the lock must match the water level outside the gate being opened.

Sie können eine C#-Klasse erstellen, um dieses Verhalten zu modellieren.You can build a C# class to model this behavior. Eine CanalLock-Klasse muss Befehle zum Öffnen oder Schließen eines der Tore bieten.A CanalLock class would support commands to open or close either gate. Sie muss andere Befehle aufweisen, mit denen sich der Wasserstand heben oder senken lässt.It would have other commands to raise or lower the water. Die Klasse muss auch Eigenschaften zum Lesen der aktuellen Stellung der beiden Tore und des Wasserstands unterstützen.The class should also support properties to read the current state of both gates and the water level. Sicherheitsmaßnahmen werden mithilfe Ihrer Methoden implementiert.Your methods implement the safety measures.

Definieren einer KlasseDefine a class

Sie erstellen eine Konsolenanwendung, um Ihre CanalLock-Klasse zu testen.You'll build a console application to test your CanalLock class. Erstellen Sie entweder in Visual Studio oder mit der .NET CLI ein neues Konsolenprojekt für .NET 5.Create a new console project for .NET 5 using either Visual Studio or the .NET CLI. Fügen Sie dann eine neue Klasse hinzu, und nennen Sie sie CanalLock.Then, add a new class and name it CanalLock. Als Nächstes entwerfen Sie Ihre öffentliche API, ohne jedoch die Methoden zu implementieren: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}.";
}

Der vorstehende Code initialisiert das Objekt so, dass beide Tore geschlossen sind und der Wasserstand niedrig ist.The preceding code initializes the object so both gates are closed, and the water level is low. Schreiben Sie als Nächstes den folgenden Testcode in Ihre Main-Methode, um Sie bei der Erstellung einer ersten Implementierung der Klasse zu leiten: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}");

Fügen Sie als Nächstes der CanalLock-Klasse eine erste Implementierung jeder Methode hinzu.Next, add a first implementation of each method in the CanalLock class. Der folgende Code implementiert die Methoden der Klasse, ohne dass die Sicherheitsregeln berücksichtigt werden.The following code implements the methods of the class without concern to the safety rules. Sicherheitstests fügen Sie später hinzu: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;
}

Die Tests, die Sie bisher geschrieben haben, werden bestanden.The tests you've written so far pass. Sie haben die Grundlagen implementiert.You've implemented the basics. Schreiben Sie nun einen Test für die erste Fehlerbedingung.Now, write a test for the first failure condition. Am Ende der vorherigen Tests sind beide Tore geschlossen, und der Wasserstand ist auf niedrig festgelegt.At the end of the previous tests, both gates are closed, and the water level is set to low. Fügen Sie einen Test hinzu, um zu versuchen, das obere Tor zu öffnen: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}");

Dieser Test schlägt fehl, da das Tor geöffnet wird.This test fails because the gate opens. Als erste Implementierung können Sie dies mit dem folgendem Code beheben: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");
}

Die Tests werden bestanden.Your tests pass. Aber je mehr Tests Sie hinzufügen, desto mehr if-Klauseln werden Sie hinzufügen und verschiedene Eigenschaften testen.But, as you add more tests, you'll add more and more if clauses and test different properties. Schon bald werden diese Methoden zu kompliziert, je mehr Bedingungen Sie hinzufügen.Soon, these methods will get too complicated as you add more conditionals.

Implementieren der Befehle mit MusternImplement the commands with patterns

Eine bessere Möglichkeit bieten Muster , um festzustellen, ob sich das Objekt in einem gültigen Zustand zur Ausführung eines Befehls befindet.A better way is to use patterns to determine if the object is in a valid state to execute a command. Sie können ausdrücken, ob ein Befehl in Abhängigkeit von drei Variablen erlaubt ist: Stellung des Tores, Wasserstand und die neue Einstellung: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:

Neue EinstellungNew setting Stellung des ToresGate state WasserstandWater Level ErgebnisResult
GeschlossenClosed GeschlossenClosed HochHigh GeschlossenClosed
GeschlossenClosed GeschlossenClosed NiedrigLow GeschlossenClosed
GeschlossenClosed GeöffnetOpen HochHigh GeöffnetOpen
GeschlossenClosed GeöffnetOpen NiedrigLow GeschlossenClosed
ÖffnenOpen GeschlossenClosed HochHigh GeöffnetOpen
GeöffnetOpen GeschlossenClosed NiedrigLow Geschlossen (Fehler)Closed (Error)
GeöffnetOpen GeöffnetOpen HochHigh GeöffnetOpen
GeöffnetOpen GeöffnetOpen NiedrigLow Geschlossen (Fehler)Closed (Error)

In der vierten und letzten Zeile der Tabelle ist der Text durchgestrichen, weil sie ungültig ist.The fourth and last rows in the table have strike through text because they're invalid. Der Code, den Sie jetzt hinzufügen, muss sicherstellen, dass das Hochwassertor niemals bei niedrigem Wasserstand geöffnet wird.The code you're adding now should make sure the high water gate is never opened when the water is low. Diese Zustände können als ein einziger Switch-Ausdruck programmiert werden (denken Sie daran, dass false „Geschlossen“ bedeutet):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
};

Versuchen Sie diese Version.Try this version. Die Tests werden bestanden und der Code bestätigt.Your tests pass, validating the code. Die vollständige Tabelle zeigt die möglichen Kombinationen von Eingaben und Ergebnissen.The full table shows the possible combinations of inputs and results. Das bedeutet, dass Sie und andere Entwickler schnell einen Blick auf die Tabelle werfen können, um festzustellen, dass Sie alle möglichen Eingaben abgedeckt haben.That means you and other developers can quickly look at the table and see that you've covered all the possible inputs. Zur Vereinfachung kann auch der Compiler beitragen.Even easier, the compiler can help as well. Nachdem Sie den vorherigen Code hinzugefügt haben, sehen Sie, dass der Compiler eine Warnung generiert: CS8524 bedeutet, dass der Switch-Ausdruck nicht alle möglichen Eingaben abdeckt.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. Der Grund für diese Warnung ist, dass eine der Eingaben den Typ enum hat.The reason for that warning is that one of the inputs is an enum type. Der Compiler interpretiert „alle möglichen Eingaben“ als alle Eingaben des zugrunde liegenden Typs, typischerweise int.The compiler interprets "all possible inputs" as all inputs from the underlying type, typically an int. Dieser switch-Ausdruck prüft nur die in enum deklarierten Werte.This switch expression only checks the values declared in the enum. Um die Warnung zu entfernen, können Sie für den letzten Teil des Ausdrucks ein Muster des Typs „Alle abfangen und entsorgen“ hinzufügen.To remove the warning, you can add a catch-all discard pattern for the last arm of the expression. Diese Bedingung löst eine Ausnahme aus, da sie eine ungültige Eingabe angibt:This condition throws an exception, because it indicates invalid input:

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

Der vorherige Switch-Arm muss der letzte in Ihrem switch-Ausdruck sein, da er mit allen Eingaben übereinstimmt.The preceding switch arm must be last in your switch expression because it matches all inputs. Experimentieren Sie, indem Sie ihn in der Reihenfolge nach vorn verschieben.Experiment by moving it earlier in the order. Dies verursacht den Compilerfehler CS8510 für nicht erreichbaren Code in einem Muster.That causes a compiler error CS8510 for unreachable code in a pattern. Die natürliche Struktur von Switch-Ausdrücken ermöglicht es dem Compiler, Fehler und Warnungen für mögliche Fehlersituationen zu generieren.The natural structure of switch expressions enables the compiler to generate errors and warnings for possible mistakes. Das „Sicherheitsnetz“ des Compilers erleichtert Ihnen, fehlerfreien Code in weniger Iterationen zu erstellen, und bietet Ihnen die Möglichkeit, Switch-Arme mit Platzhaltern frei zu kombinieren.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. Der Compiler löst Fehler aus, wenn Ihre Kombination zu unerreichbaren Armen führt, mit denen Sie nicht gerechnet haben, und Warnungen, wenn Sie einen benötigten Arm entfernen.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.

Die erste Änderung besteht darin, alle Arme zu kombinieren, bei denen der Befehl lautet, das Tor zu schließen. Das ist stets erlaubt.The first change is to combine all the arms where the command is to close the gate; that's always allowed. Fügen Sie den folgenden Code als ersten Arm in den Switch-Ausdruck ein:Add the following code as the first arm in your switch expression:

(false, _, _) => false,

Nachdem Sie den vorherigen Switch-Arm hinzugefügt haben, erhalten Sie vier Compilerfehler, einen in jedem der Arme, in denen der Befehl false ist.After you add the previous switch arm, you'll get four compiler errors, one on each of the arms where the command is false. Diese Arme sind bereits durch den neu hinzugefügten Arm abgedeckt.Those arms are already covered by the newly added arm. Sie können diese vier Zeilen sicher entfernen.You can safely remove those four lines. Sie wollten mit diesem neuen Switch-Arm diese Bedingungen ersetzen.You intended this new switch arm to replace those conditions.

Als Nächstes können Sie die vier Arme vereinfachen, bei denen der Befehl lautet, das Tor zu öffnen.Next, you can simplify the four arms where the command is to open the gate. In beiden Fällen, in denen der Wasserstand hoch ist, kann das Tor geöffnet werden.In both cases where the water level is high, the gate can be opened. (In einem ist es bereits geöffnet.) Ein Fall, in dem der Wasserstand niedrig ist, löst eine Ausnahme aus, der andere darf nicht passieren.(In one, it's already open.) One case where the water level is low throws an exception, and the other shouldn't happen. Es sollte sicher sein, die gleiche Ausnahme auszulösen, wenn sich die Schleuse bereits in einer ungültigen Stellung befindet.It should be safe to throw the same exception if the water lock is already in an invalid state. Sie können die folgenden Vereinfachungen für diese Arme vornehmen: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"),

Führen Sie die Tests erneut aus, die bestanden werden.Run your tests again, and they pass. Hier ist die endgültige Version der SetHighGate-Methode: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"),
    };
}

Eigenständiges Implementieren der MusterImplement patterns yourself

Nachdem Sie die Technik kennengelernt haben, füllen Sie die Methoden SetLowGate und SetWaterLevel selbst aus.Now that you've seen the technique, fill in the SetLowGate and SetWaterLevel methods yourself. Beginnen Sie mit dem Hinzufügen des folgenden Codes, um ungültige Vorgänge in diesen Methoden zu testen: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}");

Führen Sie Ihre Anwendung erneut aus.Run your application again. Sie erleben, wie die neuen Tests fehlschlagen und die Kanalschleuse in einen ungültigen Zustand gerät.You can see the new tests fail, and the canal lock gets into an invalid state. Versuchen Sie, die restlichen Methoden selbst zu implementieren.Try to implement the remaining methods yourself. Die Methode zur Festlegung des unteren Tores sollte ähnlich wie die Methode zur Festlegung des oberen Tores sein.The method to set the lower gate should be similar to the method to set the upper gate. Die Methode zum Ändern des Wasserstands hat unterschiedliche Prüfungen, sollte aber eine ähnliche Struktur aufweisen.The method that changes the water level has different checks, but should follow a similar structure. Möglicherweise finden Sie es hilfreich, dasselbe Verfahren auch für die Methode zu verwenden, mit der der Wasserstand festgelegt wird.You may find it helpful to use the same process for the method that sets the water level. Beginnen Sie mit allen vier Eingaben: Stellung der beiden Tore, aktueller Wasserstand und geforderter neuer Wasserstand.Start with all four inputs: The state of both gates, the current state of the water level, and the requested new water level. Der Switch-Ausdruck sollte wie folgt beginnen:The switch expression should start with:

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

Es gibt insgesamt 16 auszufüllende Switch-Arme.You'll have 16 total switch arms to fill in. Testen und vereinfachen Sie das Ganze im Anschluss.Then, test and simplify.

Haben Sie Methoden in etwa wie folgt gestaltet?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"),
    };
}

Die Tests sollten bestanden werden, und die Schleuse sollte sicher betrieben werden.Your tests should pass, and the canal lock should operate safely.

ZusammenfassungSummary

In diesem Tutorial haben Sie gelernt, mithilfe eines Musterabgleichs den internen Zustand eines Objekts zu überprüfen, ehe Änderungen an diesem Zustand vorgenommen werden.In this tutorial, you learned to use pattern matching to check the internal state of an object before applying any changes to that state. Sie können Kombinationen von Eigenschaften überprüfen.You can check combinations of properties. Sobald Sie Tabellen für beliebige dieser Übergänge erstellt haben, testen Sie Ihren Code, und vereinfachen ihn dann hinsichtlich Lesbarkeit und Wartbarkeit.Once you've built tables for any of those transitions, you test your code, then simplify for readability and maintainability. Aus diesen anfänglichen Refactorings können sich weitere Refactorings ergeben, die den internen Zustand validieren oder andere API-Änderungen verwalten.These initial refactorings may suggest further refactorings that validate internal state or manage other API changes. In diesem Tutorial wurden Klassen und Objekte mit einem eher datenorientierten, musterbasierten Ansatz kombiniert, um diese Klassen zu implementieren.This tutorial combined classes and objects with a more data-oriented, pattern-based approach to implement those classes.