Samouczek: podstawy dopasowywania wzorców

Wskazówka

Dopiero zaczynasz programować oprogramowanie? Najpierw zacznij od samouczków Wprowadzenie . Wprowadzają klasy, metody i przepływ sterowania.

Czy masz doświadczenie w pracy w innym języku? W tym samouczku pokazano, jak wzorce języka C# mogą wyraźnie wyrażać zachowanie obiektu, gdy reguły zależą od bieżącego stanu obiektu.

W tym samouczku utworzysz aplikację konsolową, która modeluje reguły śluzy kanałowej.

Śluza kanałowa podnosi lub obniża łodzie między dwoma odcinkami wody na różnych wysokościach. Ma dwa wrota i komorę, której poziom wody zmienia się między niskim ustawieniem a wysokim ustawieniem. Śluza może działać bezpiecznie tylko wtedy, gdy poziom wody i położenia bramy pozostają w prawidłowych kombinacjach.

W tym poradniku nauczysz się, jak:

  • Wyrażaj zachowanie obiektu, dopasowując względem stanu.
  • Zaimplementuj te reguły za pomocą dopasowywania wzorców języka C#.
  • Użyj diagnostyki kompilatora, aby zweryfikować implementację.

Prerequisites

Instrukcje instalacji

W systemie Windows ten plik konfiguracyjny WinGet służy do instalacji wszystkich wymagań wstępnych. Jeśli masz już coś zainstalowanego, usługa WinGet pominie ten krok.

  1. Pobierz plik i kliknij dwukrotnie, aby go uruchomić.
  2. Przeczytaj umowę licencyjną, wpisz yi wybierz pozycję Wprowadź po wyświetleniu monitu o zaakceptowanie.
  3. Jeśli na pasku zadań zostanie wyświetlony monit kontroli konta użytkownika (UAC), zezwól na kontynuowanie instalacji.

Na innych platformach należy zainstalować każdy z tych składników oddzielnie.

  1. Pobierz zalecany instalator ze strony pobierania zestawu SDK .NET i kliknij dwukrotnie, aby go uruchomić. Strona pobierania wykrywa platformę i zaleca najnowszy instalator twojej platformy.
  2. Pobierz najnowszy instalator z strony głównej programu Visual Studio Code i kliknij dwukrotnie, aby go uruchomić. Ta strona wykrywa również platformę, a link powinien być poprawny dla twojego systemu.
  3. Kliknij przycisk "Zainstaluj" na stronie rozszerzenia C# DevKit. Spowoduje to otwarcie Visual Studio kodu i pytanie, czy chcesz zainstalować lub włączyć rozszerzenie. Wybierz pozycję "Zainstaluj".

Tworzenie symulacji śluzy kanałowej

Śluza kanałowa podnosi i obniża łodzie między drogami wodnymi na różnych poziomach. W tym samouczku symulowana śluza ma dolną bramę, górną bramę i wodę, która może być niska lub wysoka.

W normalnej pracy łódź wchodzi, gdy poziom wody wewnątrz śluzy pasuje do poziomu po stronie wejścia. Gdy łódź znajduje się wewnątrz, obie bramy zamykają się. Poziom wody dostosowuje się do poziomu wody po stronie wyjścia, a następnie otwiera się brama wyjścia. Aby zapewnić bezpieczeństwo modelu, poziom wody może ulec zmianie tylko wtedy, gdy obie bramy są zamknięte, a brama może być otwarta tylko wtedy, gdy poziom wody odpowiada poziomowi po tej stronie.

Te reguły można modelować za pomocą klasy CanalLock. Udostępnia polecenia, aby otworzyć lub zamknąć dowolną z bram i podnieść lub obniżyć wodę. Udostępnia również właściwości, które zgłaszają bieżący stan blokady.

Zdefiniuj klasę

Utwórz projekt konsoli, a następnie dodaj klasę o nazwie CanalLock. Zacznij od zaprojektowania publicznego interfejsu API i pozostawienia niezaimplementowanych metod:

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}.";
}

Powyższy kod inicjuje śluzę z zamkniętymi bramami i niskim poziomem wody. Następnie dodaj następujący kod do Main, aby ułatwić pierwszą implementację:

// 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}");

Teraz dodaj pierwszą implementację, która zmienia każdą wartość stanu bez wymuszania reguł bezpieczeństwa:

// 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;
}

Te pierwsze sprawdzenia zakończyły się powodzeniem. Mechanika działa. Następnie dodaj test dla pierwszego warunku błędu. Na końcu poprzedniej sekwencji obie bramy są zamknięte, a poziom wody jest niski. Spróbuj otworzyć górną bramę:

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}");

Ten test kończy się niepowodzeniem, ponieważ górna brama otwiera się, gdy nie powinna. Pierwsza poprawka może wyglądać następująco:

// 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");
}

Twoje testy znów przechodzą. Ale w miarę dodawania większej liczby warunków kumulujesz więcej if instrukcji. Kod staje się trudniejszy do skanowania, ponieważ każda reguła jest oddzielona od innych.

Zaimplementuj polecenia za pomocą wzorców

Jaśniejszą opcją jest użycie wzorców do bezpośredniego opisania prawidłowych kombinacji. W następnym kroku każde wyrażenie przełącznika używa jednej krotki jako wejścia wzorca. Język C# ocenia krotkę raz, a każde ramię przełącznika sprawdza bieżący stan śluzy, poziom wody i żądane nowe ustawienie.

Dla górnej bramy można podsumować te kombinacje w następujący sposób:

Nowe ustawienie Stan bramy Poziom wody Result
Zamknięte Zamknięte Wysoki Zamknięte
Zamknięte Zamknięte Low Zamknięte
Zamknięte Open Wysoki Zamknięte
Zamknięto Otwórz Low Zamknięto
Open Zamknięte Wysoki Open
Open Zamknięte Low Zamknięte (błąd)
Open Open Wysoki Open
Otwórz Otwórz Low Zamknięte (błąd)

Wiersze przekreślone reprezentują nieprawidłowe stany wewnętrzne. Wyrażenie przełącznika może kodować prawidłowe przejścia bezpośrednio. false nadal oznacza, że brama jest zamknięta:

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
};

Wypróbuj tę wersję. Twoje testy zakończyły się pomyślnie. Kompilator ostrzega również, że wyrażenie przełącznika nie jest wyczerpujące — nie obejmuje każdej możliwej wartości — ponieważ WaterLevel jest wyliczeniem, a język C# umożliwia rzutowanie dowolnej wartości bazowego typu liczbowego do tego wyliczenia, nawet jednego bez nazwanego elementu członkowskiego. Dodaj końcowe ramię ze wzorcem odrzucenia (_), aby obsłużyć te niemożliwe stany wewnętrzne:

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

To ramię musi być ostatnie, ponieważ wzorzec odrzucenia pasuje do wszystkich pozostałych danych wejściowych.

Następnie można uprościć wcześniejsze gałęzie. Zamknięcie bramy jest zawsze dozwolone, więc jedno ramię może zastąpić cztery oddzielne przypadki zamknięcia:

(false, _, _) => false,

Można również połączyć prawidłowe otwarte sprawy i zachować jeden błąd zabezpieczeń:

(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"),

Uruchom ponownie program. Testy nadal przechodzą. Oto ostateczna SetHighGate implementacja:

// 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"),
    };
}

Wdróż pozostałe reguły

Teraz zastosuj ten sam pomysł do SetLowGate i SetWaterLevel. Zacznij od dodania testów, które uwidaczniają nieprawidłowe operacje:

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}");

Ponownie uruchom aplikację. Testy te kończą się niepowodzeniem, a śluza kanałowa osiąga nieprawidłowe stany. Zaimplementuj pozostałe metody, wykonując dopasowanie względem pełnego stanu. SetLowGate jest podobny do SetHighGate. SetWaterLevel używa bieżącego poziomu wody oraz obu pozycji bramy:

CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
    // arms go here
};

Masz 16 kombinacji do rozważenia. Zacznij od pełnej tabeli, napisz ramiona przełącznika, uruchom testy, a następnie uprość powtarzające się wyniki.

Czy ostatecznie zastosowano metody podobne do tych?

// 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"),
    };
}

Testy powinny teraz przejść, a śluza kanałowa powinna teraz egzekwować swoje zasady bezpieczeństwa.

Podsumowanie

W tym samouczku użyto dopasowywania wzorców, aby wyrazić, jak obiekt może zmienić się z jednego prawidłowego stanu na inny. Wzorce utrzymywały dozwolone przejścia razem, dzięki czemu można je było łatwiej porównać niż w przypadku długiej serii instrukcji rozgałęziania. Takie podejście działa dobrze, gdy zachowanie zależy od połączonego kształtu kilku wartości stanu.