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.
Tip
Neu bei der Entwicklung von Software? Beginnen Sie zuerst mit den Lernprogrammen " Erste Schritte ". Sie stellen Klassen, Methoden und Kontrollfluss vor.
Haben Sie Erfahrung in einer anderen Sprache? In diesem Lernprogramm wird gezeigt, wie C#-Muster das Objektverhalten deutlich ausdrücken können, wenn Regeln vom aktuellen Zustand eines Objekts abhängen.
In diesem Lernprogramm erstellen Sie eine Konsolen-App, die die Regeln für eine Kanalschleuse modelliert.
Eine Schleuse hebt oder senkt Boote zwischen zwei Wasserabschnitten in unterschiedlichen Höhen. Es hat zwei Tore und eine Kammer, deren Wasserstand sich zwischen einem niedrigen und einem hohen Wasserstand ändert. Die Schleuse kann nur dann sicher betrieben werden, wenn die Wasserpegel- und Torpositionen in gültigen Kombinationen bleiben.
In diesem Tutorial erfahren Sie, wie:
- Drücken Sie Objektverhalten anhand des Zustands aus.
- Implementieren Sie diese Regeln mit dem C#-Musterabgleich.
- Verwenden Sie Compilerdiagnosen, um Ihre Implementierung zu überprüfen.
Prerequisites
- Das neueste .NET SDK
- Visual Studio Code Editor
- Das C# DevKit
Installationsanweisungen
Verwenden Sie auf Windows diese WinGet-Konfigurationsdatei, um alle erforderlichen Komponenten zu installieren. Wenn Sie bereits etwas installiert haben, überspringt WinGet diesen Schritt.
- Laden Sie die Datei herunter, und doppelklicken Sie, um sie auszuführen.
- Lesen Sie den Lizenzvertrag, geben Sie yein, und wählen Sie Geben Sie ein, wenn Sie zur Annahme aufgefordert werden.
- Wenn Sie in Ihrer Taskleiste eine blinkende Eingabeaufforderung für die Benutzerkontensteuerung (User Account Control, UAC) erhalten, können Sie die Installation fortsetzen.
Auf anderen Plattformen müssen Sie jede dieser Komponenten separat installieren.
- Laden Sie das empfohlene Installationsprogramm von der .NET SDK-Downloadseite herunter , und doppelklicken Sie, um es auszuführen. Die Downloadseite erkennt Ihre Plattform und empfiehlt das neueste Installationsprogramm für Ihre Plattform.
- Laden Sie das neueste Installationsprogramm von der Startseite Visual Studio Code herunter, und doppelklicken Sie darauf, um es auszuführen. Diese Seite erkennt auch Ihre Plattform, und der Link sollte für Ihr System korrekt sein.
- Klicken Sie auf der Erweiterungsseite C# DevKit auf die Schaltfläche "Installieren". Dadurch wird Visual Studio-Code geöffnet und gefragt, ob Sie die Erweiterung installieren oder aktivieren möchten. Wählen Sie "Installieren" aus.
Erstellen Sie eine Simulation einer Schleuse
Eine Schleuse erhöht und senkt Boote zwischen Wasserstraßen auf unterschiedlichen Ebenen. In diesem Lernprogramm verfügt die simulierte Schleuse über ein unteres Schleusentor, ein oberes Schleusentor und Wasser, das entweder niedrig oder hoch sein kann.
Im normalen Betrieb tritt ein Boot ein, wenn der Wasserstand innerhalb der Schleuse mit dem Niveau auf der Einfahrtsseite übereinstimmt. Sobald sich das Boot im Inneren befindet, schließen sich beide Tore. Der Wasserstand ändert sich, um sich an die Unterwasserseite anzupassen, und dann wird das Schleusentor geöffnet. Um das Modell sicher zu halten, kann sich der Wasserstand nur ändern, wenn beide Tore geschlossen sind, und ein Tor kann nur geöffnet werden, wenn der Wasserstand der jeweiligen Seite entspricht.
Sie können diese Regeln mit einer CanalLock Klasse modellieren. Es stellt Befehle bereit, um das Tor zu öffnen oder zu schließen und den Wasserstand zu heben oder zu senken. Außerdem werden Eigenschaften verfügbar gemacht, die den aktuellen Zustand der Sperre melden.
Klasse definieren
Erstellen Sie ein Konsolenprojekt, und fügen Sie dann eine Klasse mit dem Namen CanalLockhinzu. Entwerfen Sie zunächst die öffentliche API, und implementieren Sie die Methoden zunächst nicht:
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 die Schleuse mit geschlossenen Toren und dem niedrigen Wasserstand. Fügen Sie als Nächstes den folgenden Code in Main ein, um Ihre erste Implementierung anzuleiten:
// 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 nun eine erste Implementierung hinzu, die jeden Zustandswert ändert, ohne die Sicherheitsregeln zu erzwingen:
// 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;
}
Diese ersten Prüfungen fallen positiv aus. Die Mechanik funktioniert. Fügen Sie als Nächstes einen Test für die erste Fehlerbedingung hinzu. Am Ende der vorherigen Sequenz sind beide Tore geschlossen, und der Wasserstand ist niedrig. Versuchen Sie, 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, da das obere Tor geöffnet wird, wenn es nicht sollte. Ein erster Fix könnte wie folgt aussehen:
// 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 sind erneut erfolgreich. Wenn Sie jedoch weitere Bedingungen hinzufügen, sammeln Sie weitere if Anweisungen. Der Code wird schwieriger zu scannen, da jede Regel von den anderen getrennt ist.
Implementieren Sie die Befehle mit Mustern
Eine klarere Option besteht darin, Muster zu verwenden, um die gültigen Kombinationen direkt zu beschreiben. Im nächsten Schritt verwendet jeder Schalterausdruck ein Tupel als Mustereingabe. C# wertet dieses Tupel einmal aus, und jeder Zweig der switch-Anweisung testet den aktuellen Zustand des Gates, den Wasserstand und die angeforderte neue Einstellung.
Für das obere Tor können Sie diese Kombinationen wie folgt zusammenfassen:
| Neue Einstellung | Gate-Status | Wasserstand | Result |
|---|---|---|---|
| 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 |
|
|
|
|
|
Die durchgestrichenen Zeilen stellen ungültige interne Zustände dar. Der Switchausdruck kann die gültigen Übergänge direkt codieren.
false bedeutet dennoch, dass das Tor geschlossen ist:
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. Der Compiler warnt außerdem, dass der Switchausdruck nicht erschöpfend ist – er deckt nicht jeden möglichen Wert ab –, da es sich bei WaterLevel um eine Enumeration handelt, und C# lässt jeden Wert des zugrunde liegenden numerischen Typs der Enumeration in diese Enumeration umgewandelt werden, auch einen ohne benanntes Element. Fügen Sie einen letzten Arm mit dem Verwerfenmuster (_) hinzu, um diese unmöglichen internen Zustände zu behandeln:
_ => throw new InvalidOperationException("Invalid internal state"),
Dieser Arm muss zuletzt sein, da das Verwerfungsmuster auf jede verbleibende Eingabe passt.
Sie können dann die früheren Zweige vereinfachen. Das Schließen des Gates ist immer erlaubt, sodass ein Arm die vier separaten geschlossenen Fälle ersetzen kann:
(false, _, _) => false,
Sie können auch die gültigen offenen Fälle kombinieren und den einen Sicherheitsfehler beibehalten:
(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 das Programm erneut aus. Die Tests laufen weiterhin erfolgreich durch. Hier ist die endgültige SetHighGate Implementierung:
// 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"),
};
}
Implementieren Sie die verbleibenden Regeln
Wenden Sie jetzt die gleiche Idee auf SetLowGate und SetWaterLevel an. Fügen Sie zunächst Tests hinzu, die ungültige Vorgänge aufdecken:
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 App erneut aus. Diese Tests schlagen fehl, und die Schleuse erreicht ungültige Zustände. Implementieren Sie die verbleibenden Methoden, indem Sie den vollständigen Zustand abgleichen.
SetLowGate ist ähnlich wie SetHighGate.
SetWaterLevel verwendet den aktuellen Wasserstand sowie beide Torpositionen:
CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
// arms go here
};
Sie haben 16 Kombinationen zu berücksichtigen. Beginnen Sie mit der vollständigen Tabelle, schreiben Sie die switch-Zweige, führen Sie die Tests aus, und vereinfachen Sie dann wiederholte Ergebnisse.
Sind Sie bei ähnlichen Methoden gelandet?
// 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 jetzt bestehen, und die Schleuse sollte ihre Sicherheitsregeln durchsetzen.
Summe
In diesem Lernprogramm haben Sie den Mustervergleich verwendet, um anzugeben, wie sich ein Objekt von einem gültigen Zustand in einen anderen ändern kann. Patterns hielten die zulässigen Übergänge zusammen, sodass Sie sie einfacher vergleichen konnten als mit einer langen Reihe von Verzweigungsanweisungen. Dieser Ansatz funktioniert gut, wenn das Verhalten von der kombinierten Form mehrerer Zustandswerte abhängt.