Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Tip
Nieuw bij het ontwikkelen van software? Begin eerst met de handleiding Aan de slag. Hierin worden klassen, methoden en besturingsstroom geïntroduceerd.
Ervaren in een andere taal? In deze zelfstudie ziet u hoe C#-patronen objectgedrag duidelijk kunnen uitdrukken wanneer regels afhankelijk zijn van de huidige status van een object.
In deze zelfstudie bouwt u een console-app waarmee de regels voor een sluis worden gemodelleerd.
Een sluis brengt boten omhoog of omlaag tussen twee waterniveaus op verschillende hoogten. Het heeft twee poorten en een sluiskolk waarvan het waterniveau verandert tussen een lage instelling en een hoge instelling. De sluis kan alleen veilig werken wanneer het waterniveau en de poortposities in geldige combinaties blijven.
In deze handleiding leer je hoe je:
- Express objectgedrag door op basis van de toestand te matchen.
- Implementeer deze regels met C#-pattern matching.
- Gebruik compilerdiagnostiek om uw implementatie te valideren.
Prerequisites
- De nieuwste .NET SDK
- Visual Studio Code editor
- De C# DevKit
Installatie-instructies
Op Windows wordt dit WinGet-configuratiebestand gebruikt om alle vereisten te installeren. Als u al iets hebt geïnstalleerd, slaat WinGet die stap over.
- Download het bestand en dubbelklik erop om het uit te voeren.
- Lees de gebruiksrechtovereenkomst, typ yen selecteer Enter wanneer u wordt gevraagd om te accepteren.
- Als u een knipperende UAC-prompt (User Account Control) krijgt op de taakbalk, staat u de installatie toe om door te gaan.
Op andere platforms moet u elk van deze onderdelen afzonderlijk installeren.
- Download het aanbevolen installatieprogramma op de downloadpagina van de .NET SDK en dubbelklik erop om het uit te voeren. De downloadpagina detecteert uw platform en raadt het meest recente installatieprogramma voor uw platform aan.
- Download het meest recente installatieprogramma op de startpagina van Visual Studio Code en dubbelklik erop om het uit te voeren. Deze pagina detecteert ook uw platform en de koppeling moet juist zijn voor uw systeem.
- Klik op de knop Installeren op de pagina C# DevKit extensie. Hiermee wordt Visual Studio code geopend en wordt gevraagd of u de extensie wilt installeren of inschakelen. Selecteer 'installeren'.
Een simulatie van een sluis bouwen
Een sluis verhoogt en verlaagt boten tussen waterwegen op verschillende niveaus. In deze zelfstudie heeft de gesimuleerde sluis een onderste poort, een bovenste poort en water dat laag of hoog kan zijn.
Bij normale werking komt een boot binnen wanneer het waterniveau in de sluis overeenkomt met het niveau aan de ingangszijde. Zodra de boot binnen is, sluiten beide poorten. Het waterniveau verandert zodat het overeenkomt met de zijde van de uitgang en vervolgens wordt de uitgangpoort geopend. Om het model veilig te houden, kan het waterniveau alleen veranderen wanneer beide poorten gesloten zijn en kan een poort alleen worden geopend wanneer het waterniveau overeenkomt met die kant.
U kunt deze regels modelleren met een CanalLock klasse. Het biedt opdrachten om een poort te openen of te sluiten en het water te verhogen of te verlagen. Het stelt ook eigenschappen beschikbaar die de huidige status van de vergrendeling rapporteren.
Definieer de klasse
Maak een consoleproject en voeg vervolgens een klasse toe met de naam CanalLock. Begin met het ontwerpen van de openbare API en laat de methoden ongeïmplementeerd:
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}.";
}
Met de voorgaande code wordt de sluis geïnitialiseerd waarbij zowel sluisdeuren gesloten als het waterniveau laag zijn. Voeg vervolgens de volgende code toe aan Main om uw eerste implementatie te begeleiden:
// 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}");
Voeg nu een eerste implementatie toe die elke statuswaarde wijzigt zonder de veiligheidsregels af te dwingen:
// 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;
}
Deze eerste controles slagen. Je hebt de mechanica werkend. Voeg vervolgens een test toe voor de eerste foutvoorwaarde. Aan het einde van de vorige sequentie zijn beide poorten gesloten en is het waterniveau laag. Probeer de bovenste poort te openen:
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}");
Deze test mislukt omdat de bovenste poort wordt geopend wanneer die niet open zou moeten gaan. Een eerste oplossing kan er als volgt uitzien:
// 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");
}
Uw tests zijn opnieuw geslaagd. Maar naarmate u meer voorwaarden toevoegt, verzamelt u meer if statements. De code wordt moeilijker te scannen omdat elke regel van de andere regels is gescheiden.
De commando's implementeren met patronen
Een duidelijkere optie is om patronen te gebruiken om de geldige combinaties rechtstreeks te beschrijven. In de volgende stap gebruikt elke switchexpressie één tuple als patrooninvoer. C# evalueert die tuple eenmaal en elke switch-arm test de huidige gate-status, het waterniveau en de aangevraagde nieuwe instelling.
Voor de bovenste poort kunt u deze combinaties als volgt samenvatten:
| Nieuwe instelling | Poortstatus | Waterpeil | Result |
|---|---|---|---|
| Gesloten | Gesloten | Hoog | Gesloten |
| Gesloten | Gesloten | Laag | Gesloten |
| Gesloten | Open | Hoog | Gesloten |
|
|
|
|
|
| Open | Gesloten | Hoog | Open |
| Open | Gesloten | Laag | Gesloten (fout) |
| Open | Open | Hoog | Open |
|
|
|
|
|
De doorgehaalde rijen duiden ongeldige interne toestanden aan. De switchexpressie kan de geldige overgangen rechtstreeks coderen.
false betekent nog steeds dat de poort gesloten is:
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
};
Probeer deze versie. Uw testen zijn geslaagd. De compiler waarschuwt ook dat de switchexpressie niet volledig is, maar niet elke mogelijke waarde omvat, omdat WaterLevel een enum is en C# elke waarde van het onderliggende numerieke type van de enum toestaat om naar die enum te worden gecast, zelfs een waarde zonder benoemd lid. Voeg een laatste arm toe met het discardpatroon (_) om deze onmogelijke interne toestanden te verwerken:
_ => throw new InvalidOperationException("Invalid internal state"),
Dat patroon moet het laatste zijn omdat het discard-patroon overeenkomt met elke resterende invoer.
Vervolgens kunt u de eerdere takken vereenvoudigen. Het sluiten van de poort is altijd toegestaan, zodat één arm de vier afzonderlijke gesloten gevallen kan vervangen:
(false, _, _) => false,
U kunt ook de geldige openstaande zaken combineren en de veiligheidsfout behouden:
(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"),
Voer het programma opnieuw uit. De tests slagen nog steeds. Dit is de laatste SetHighGate implementatie:
// 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"),
};
}
De resterende regels implementeren
Pas nu hetzelfde idee toe op SetLowGate en SetWaterLevel. Begin met het toevoegen van tests waarmee ongeldige bewerkingen aan het licht worden gebracht:
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}");
Voer de app opnieuw uit. Deze tests mislukken en de sluiskolk bereikt ongeldige toestanden. Implementeer de resterende methoden door te matchen op de volledige toestand.
SetLowGate is vergelijkbaar met SetHighGate.
SetWaterLevel gebruikt het huidige waterniveau plus beide poortposities:
CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
// arms go here
};
U kunt 16 combinaties overwegen. Begin met de volledige tabel, schrijf de switch-armen, voer de tests uit en vereenvoudig vervolgens herhaalde resultaten.
Hebt u uiteindelijk methoden gekregen die vergelijkbaar zijn met deze methoden?
// 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"),
};
}
Uw tests moeten nu slagen en de sluis moet de veiligheidsregels afdwingen.
Overzicht
In deze zelfstudie hebt u patroonovereenkomst gebruikt om aan te geven hoe een object van de ene geldige status naar een andere kan veranderen. Patronen groepeerden de toegestane overgangen, zodat u ze gemakkelijker kon vergelijken dan met een lange reeks vertakkingsinstructies. Deze benadering werkt goed wanneer gedrag afhankelijk is van de gecombineerde structuur van verschillende statuswaarden.