Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Die Musterabgleichsfeatures in C# stellen eine Syntax bereit, um Ihre Algorithmen auszudrücken. Sie können diese Techniken verwenden, um das Verhalten in Ihren Klassen zu implementieren. Sie können objektorientiertes Klassendesign mit einer datenorientierten Implementierung kombinieren, um präzisen Code bereitzustellen und gleichzeitig reale Objekte zu modellieren.
In diesem Tutorial lernen Sie Folgendes:
- Ausdrücken Sie ihre objektorientierten Klassen mithilfe von Datenmustern.
- Implementieren Sie diese Muster mithilfe der Musterabgleichsfeatures von C#.
- Nutzen Sie die Compilerdiagnose, um Ihre Implementierung zu überprüfen.
Voraussetzungen
- Das neueste .NET SDK
- Visual Studio Code-Editor
- Das C# DevKit
Erstellen einer Simulation einer Kanalsperre
In diesem Lernprogramm erstellen Sie eine C#-Klasse, die eine Kanalsperresimuliert. Kurz gesagt ist eine Kanalsperre ein Gerät, das Boote aufhebt und senkt, während sie zwischen zwei Wasserabschnitten auf unterschiedlichen Ebenen reisen. Eine Schleuse hat zwei Tore und einen Mechanismus, um den Wasserstand zu ändern.
Während seines normalen Betriebs fährt ein Boot in eines der Tore ein, während der Wasserstand in der Schleuse mit dem Wasserstand auf der Seite übereinstimmt, auf der das Boot einfährt. Sobald sie sich in der Sperre befindet, wird der Wasserstand so geändert, dass er dem Wasserstand entspricht, auf dem das Boot die Sperre verlässt. Sobald der Wasserstand mit dieser Seite übereinstimmt, wird das Tor auf der Ausgangsseite geöffnet. Sicherheitsmaßnahmen stellen sicher, dass ein Betreiber keine gefährliche Situation im Kanal schaffen kann. Der Wasserstand kann nur geändert werden, wenn beide Tore geschlossen sind. Höchstens ein Tor kann geöffnet sein. Um ein Tor zu öffnen, muss der Wasserstand in der Sperre mit dem Wasserspiegel außerhalb des zu öffnenden Tors übereinstimmen.
Sie können eine C#-Klasse erstellen, um dieses Verhalten zu modellieren. Eine CanalLock
Klasse würde Befehle zum Öffnen oder Schließen eines Gates unterstützen. Es hätte andere Befehle, um das Wasser zu heben oder zu senken. Die Klasse muss auch Eigenschaften zum Lesen der aktuellen Stellung der beiden Tore und des Wasserstands unterstützen. Ihre Methoden implementieren die Sicherheitsmaßnahmen.
Definieren einer Klasse
Sie erstellen eine Konsolenanwendung, um Ihre CanalLock
Klasse zu testen. Erstellen Sie ein neues Konsolenprojekt für .NET 5 mit Visual Studio oder der .NET CLI. Fügen Sie dann eine neue Klasse hinzu, und nennen Sie sie CanalLock
. Entwerfen Sie als Nächstes Ihre öffentliche API, lassen Sie die Methoden jedoch zunächst nicht implementiert.
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 vorangehende Code initialisiert das Objekt, sodass beide Tore geschlossen werden und der Wasserstand niedrig ist. Schreiben Sie als Nächstes den folgenden Testcode in Die Main
-Methode, um Sie beim Erstellen einer ersten Implementierung der Klasse zu unterstützen:
// 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}");
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 eine erste Implementierung jeder Methode in der CanalLock
Klasse hinzu. Der folgende Code implementiert die Methoden der Klasse ohne Bedenken für die Sicherheitsregeln. Sie fügen später Sicherheitstests hinzu:
// 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. Sie haben die Grundlagen implementiert. Schreiben Sie nun einen Test für die erste Fehlerbedingung. Am Ende der vorherigen Tests werden beide Tore geschlossen, und der Wasserstand wird auf niedrig eingestellt. Fügen Sie einen Test hinzu, um zu versuchen, das obere Tor zu öffnen:
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, weil das Tor geöffnet wird. Als erste Implementierung könnten Sie sie mit dem folgenden Code beheben:
// 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");
}
Ihre Tests bestehen. Wenn Sie jedoch weitere Tests hinzufügen, fügen Sie weitere if
Klauseln hinzu und testen verschiedene Eigenschaften. Bald werden diese Methoden zu kompliziert, wenn Sie weitere Bedingungen hinzufügen.
Implementieren der Befehle mit Mustern
Eine bessere Möglichkeit besteht darin, Muster zu verwenden, um festzustellen, ob sich das Objekt in einem gültigen Zustand befindet, um einen Befehl auszuführen. Sie können ausdrücken, ob ein Befehl als Funktion von drei Variablen zulässig ist: den Zustand des Gates, den Wasserstand und die neue Einstellung:
Neue Einstellung | Stellung des Tores | Wasserstand | Ergebnis |
---|---|---|---|
Geschlossen | Geschlossen | Hoch | Geschlossen |
Geschlossen | Geschlossen | Niedrig | Geschlossen |
Geschlossen | Öffnen | Hoch | Geschlossen |
Öffnen | Geschlossen | Hoch | Öffnen |
Öffnen | Geschlossen | Niedrig | Geschlossen (Fehler) |
Öffnen | Öffnen | Hoch | Öffnen |
In der vierten und letzten Zeile der Tabelle ist der Text durchgestrichen, weil sie ungültig ist. Der Code, den Sie jetzt hinzufügen, sollte sicherstellen, dass das Hohe Wassertor nie geöffnet wird, wenn das Wasser niedrig ist. Diese Zustände können als einzelner Switchausdruck codiert werden (denken Sie daran, dass false
"Geschlossen" angibt):
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
};
Probieren Sie diese Version aus. Ihre Tests sind erfolgreich, was den Code validiert. Die vollständige Tabelle zeigt die möglichen Kombinationen aus Eingaben und Ergebnissen. Das bedeutet, dass Sie und andere Entwickler schnell die Tabelle betrachten und sehen können, dass Sie alle möglichen Eingaben behandelt haben. Zur Vereinfachung kann auch der Compiler beitragen. Nachdem Sie den vorherigen Code hinzugefügt haben, können Sie sehen, dass der Compiler eine Warnung generiert: CS8524- gibt an, dass der Switchausdruck nicht alle möglichen Eingaben abdeckt. Der Grund für diese Warnung ist, dass einer der Eingaben ein enum
Typ ist. Der Compiler interpretiert "alle möglichen Eingaben" als alle Eingaben aus dem zugrunde liegenden Typ, in der Regel eine int
. Dieser switch
Ausdruck überprüft nur die in der enum
deklarierten Werte. 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. Diese Bedingung löst eine Ausnahme aus, da sie eine ungültige Eingabe angibt:
_ => throw new InvalidOperationException("Invalid internal state"),
Der vorhergehende Schalterarm muss in Ihrem switch
Ausdruck zuletzt vorhanden sein, da er mit allen Eingaben übereinstimmt. Experimentieren Sie, indem Sie es weiter oben in der Reihenfolge verschieben. Dies führt zu einem Compilerfehler CS8510- für nicht erreichbaren Code in einem Muster. Die natürliche Struktur von Switchausdrücken ermöglicht dem Compiler, Fehler und Warnungen für mögliche Irrtümer zu generieren. Der Compiler "Safety Net" erleichtert es Ihnen, korrekten Code in weniger Iterationen zu erstellen, und die Freiheit, Schalterarme mit Platzhaltern zu kombinieren. Der Compiler gibt Fehler aus, wenn ihre Kombination zu nicht erreichbaren Armen führt, die Sie nicht erwartet haben, und Warnungen, wenn Sie einen erforderlichen Arm entfernen.
Die erste Änderung besteht darin, alle Arme zu kombinieren, bei denen der Befehl lautet, das Tor zu schließen, was immer zulässig ist. Fügen Sie den folgenden Code als ersten Arm in Ihrem Switchausdruck hinzu:
(false, _, _) => false,
Nachdem Sie den vorherigen Schalterarm hinzugefügt haben, erhalten Sie vier Compilerfehler, eine auf jedem der Arme, in denen der Befehl false
ist. Diese Arme sind bereits durch den neu hinzugefügten Arm bedeckt. Sie können diese vier Zeilen sicher entfernen. Sie haben diesen neuen Schalterarm zum Ersetzen dieser Bedingungen vorgesehen.
Als Nächstes können Sie die vier Arme, die für den Befehl zum Öffnen des Tores zuständig sind, vereinfachen. In beiden Fällen, in denen der Wasserstand hoch ist, kann das Tor geöffnet werden. (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. Es sollte sicher sein, dieselbe Ausnahme auszulösen, wenn sich die Wassersperre bereits in einem ungültigen Zustand befindet. Sie können die folgenden Vereinfachungen für diese Waffen vornehmen:
(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. Dies ist die endgültige Version der SetHighGate
-Methode:
// 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"),
};
}
Muster selbst implementieren
Nachdem Sie die Technik gesehen haben, füllen Sie die SetLowGate
und SetWaterLevel
Methoden selbst aus. Fügen Sie zunächst den folgenden Code hinzu, um ungültige Vorgänge für diese Methoden zu testen:
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 die Anwendung erneut aus. Sie können sehen, dass die neuen Tests fehlschlagen, und die Kanalsperre wird in einen ungültigen Zustand versetzt. Versuchen Sie, die verbleibenden Methoden selbst zu implementieren. Die Methode zum Festlegen des unteren Tors sollte der Methode zum Festlegen des oberen Tors ähneln. Die Methode, die den Wasserstand ändert, weist unterschiedliche Prüfungen auf, sollte jedoch einer ähnlichen Struktur folgen. Möglicherweise ist es hilfreich, denselben Prozess für die Methode zu verwenden, die den Wasserspiegel festlegt. Beginnen Sie mit allen vier Eingaben: Der Zustand beider Tore, der aktuelle Zustand des Wasserspiegels und der angeforderte neue Wasserstand. Der Switch-Ausdruck sollte wie folgt beginnen:
CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
// elided
};
Es gibt insgesamt 16 auszufüllende Switch-Arme. Testen und vereinfachen Sie dann.
Haben Sie Methoden auf diese Weise erstellt?
// 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 low gate when the water is high"),
_ => 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"),
};
}
Ihre Tests sollten bestehen, und die Kanalsperre sollte sicher funktionieren.
Zusammenfassung
In diesem Lernprogramm haben Sie gelernt, den Musterabgleich zu verwenden, um den internen Zustand eines Objekts zu überprüfen, bevor Sie Änderungen auf diesen Zustand anwenden. Sie können Kombinationen von Eigenschaften überprüfen. Nachdem Sie Tabellen für einen dieser Übergänge erstellt haben, testen Sie Ihren Code, und vereinfachen Sie dann die Lesbarkeit und Wartung. Diese anfänglichen Umgestaltungen können weitere Umgestaltungen vorschlagen, die den internen Zustand überprüfen oder andere API-Änderungen verwalten. In diesem Lernprogramm wurden Klassen und Objekte mit einem datenorientierten, musterbasierten Ansatz kombiniert, um diese Klassen zu implementieren.