Omówienie dopasowywania wzorca

Dopasowywanie wzorca to technika, w której testujesz wyrażenie w celu określenia, czy ma pewne cechy. Dopasowywanie wzorca języka C# zapewnia bardziej zwięzłą składnię testowania wyrażeń i podejmowanie akcji w przypadku dopasowania wyrażenia. Wyrażenie "is " obsługuje dopasowywanie wzorca do testowania wyrażenia i warunkowo deklarowanie nowej zmiennej do wyniku tego wyrażenia. Wyrażenie "switch " umożliwia wykonywanie akcji na podstawie pierwszego zgodnego wzorca dla wyrażenia. Te dwa wyrażenia obsługują bogate słownictwo wzorców.

Ten artykuł zawiera omówienie scenariuszy, w których można używać dopasowywania wzorców. Te techniki mogą poprawić czytelność i poprawność kodu. Aby zapoznać się z pełnym omówieniem wszystkich wzorców, które można zastosować, zobacz artykuł na temat wzorców w dokumentacji językowej.

Sprawdzanie wartości null

Jednym z najbardziej typowych scenariuszy dopasowywania wzorców jest zapewnienie, że wartości nie nullsą . Typ wartości dopuszczanej do wartości null można przetestować i przekonwertować na jego typ bazowy podczas testowania, null korzystając z następującego przykładu:

int? maybe = 12;

if (maybe is int number)
{
    Console.WriteLine($"The nullable int 'maybe' has the value {number}");
}
else
{
    Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
}

Powyższy kod to wzorzec deklaracji do testowania typu zmiennej i przypisywania go do nowej zmiennej. Reguły językowe sprawiają, że ta technika jest bezpieczniejsza niż wiele innych. Zmienna number jest dostępna tylko i przypisana w prawdziwej części klauzuli if . Jeśli spróbujesz uzyskać dostęp do niego w innym miejscu, w klauzuli else lub po if bloku, kompilator zgłasza błąd. Po drugie, ponieważ nie używasz == operatora, ten wzorzec działa, gdy typ przeciąża == operatora. Dzięki temu jest to idealny sposób sprawdzania wartości referencyjnych o wartościach null, dodając not wzorzec:

string? message = ReadMessageOrDefault();

if (message is not null)
{
    Console.WriteLine(message);
}

W poprzednim przykładzie użyto wzorca stałej do porównania zmiennej z null. Jest not to wzorzec logiczny, który jest zgodny, gdy negowany wzorzec nie jest zgodny.

Testy typów

Innym typowym zastosowaniem dopasowania wzorca jest przetestowanie zmiennej w celu sprawdzenia, czy jest ona zgodna z danym typem. Na przykład następujące testy kodu, jeśli zmienna ma wartość inną niż null i implementuje System.Collections.Generic.IList<T> interfejs. Jeśli tak, używa ICollection<T>.Count właściwości na tej liście, aby znaleźć indeks środkowy. Wzorzec deklaracji nie jest zgodny z wartością null , niezależnie od typu czasu kompilacji zmiennej. Poniższy kod chroni przed elementem null, oprócz ochrony przed typem, który nie implementuje IListelementu .

public static T MidPoint<T>(IEnumerable<T> sequence)
{
    if (sequence is IList<T> list)
    {
        return list[list.Count / 2];
    }
    else if (sequence is null)
    {
        throw new ArgumentNullException(nameof(sequence), "Sequence can't be null.");
    }
    else
    {
        int halfLength = sequence.Count() / 2 - 1;
        if (halfLength < 0) halfLength = 0;
        return sequence.Skip(halfLength).First();
    }
}

Te same testy można zastosować w wyrażeniu switch , aby przetestować zmienną dla wielu różnych typów. Te informacje umożliwiają tworzenie lepszych algorytmów na podstawie określonego typu czasu wykonywania.

Porównywanie wartości dyskretnych

Możesz również przetestować zmienną, aby znaleźć dopasowanie dla określonych wartości. Poniższy kod przedstawia jeden przykład, w którym testujesz wartość dla wszystkich możliwych wartości zadeklarowanych w wyliczeniem:

public State PerformOperation(Operation command) =>
   command switch
   {
       Operation.SystemTest => RunDiagnostics(),
       Operation.Start => StartSystem(),
       Operation.Stop => StopSystem(),
       Operation.Reset => ResetToReady(),
       _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),
   };

W poprzednim przykładzie pokazano wysyłanie metody na podstawie wartości wyliczenia. _ Ostatnim przypadkiem jest wzorzec odrzucenia, który pasuje do wszystkich wartości. Obsługuje wszystkie warunki błędów, w których wartość nie jest zgodna z jedną ze zdefiniowanych enum wartości. Jeśli pominięto to ramię przełącznika, kompilator ostrzega, że wyrażenie wzorca nie obsługuje wszystkich możliwych wartości wejściowych. W czasie wykonywania wyrażenie zgłasza wyjątek, switch jeśli badany obiekt nie pasuje do żadnego z ramion przełącznika. Można użyć stałych liczbowych zamiast zestawu wartości wyliczenia. Możesz również użyć tej podobnej techniki dla wartości ciągów stałych reprezentujących polecenia:

public State PerformOperation(string command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

W poprzednim przykładzie pokazano ten sam algorytm, ale używa wartości ciągów zamiast wyliczenia. Ten scenariusz jest używany, jeśli aplikacja odpowiada na polecenia tekstowe zamiast zwykłego formatu danych. Począwszy od języka C# 11, można również użyć elementu Span<char> lub , ReadOnlySpan<char>aby przetestować wartości ciągów stałych, jak pokazano w poniższym przykładzie:

public State PerformOperation(ReadOnlySpan<char> command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

We wszystkich tych przykładach wzorzec odrzucenia zapewnia obsługę wszystkich danych wejściowych. Kompilator pomaga, upewniając się, że każda możliwa wartość wejściowa jest obsługiwana.

Wzorce relacyjne

Możesz użyć wzorców relacyjnych, aby przetestować, jak wartość jest porównywana z stałymi. Na przykład poniższy kod zwraca stan wody na podstawie temperatury w fahrenheita:

string WaterState(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        (> 32) and (< 212) => "liquid",
        < 32 => "solid",
        > 212 => "gas",
        32 => "solid/liquid transition",
        212 => "liquid / gas transition",
    };

Powyższy kod demonstruje również wzorzec logiczny sprzężeniaand, aby sprawdzić, czy oba wzorce relacyjne są zgodne. Można również użyć wzorca rozłącznego or , aby sprawdzić, czy dowolny wzorzec jest zgodny. Dwa wzorce relacyjne są otoczone nawiasami, których można używać wokół dowolnego wzorca w celu uzyskania jasności. Ostatnie dwie ramiona przełącznika obsługują przypadki topnienia punktu i punktu wrzenia. Bez tych dwóch ramion kompilator ostrzega, że logika nie obejmuje wszystkich możliwych danych wejściowych.

Powyższy kod demonstruje również inną ważną funkcję, którą kompilator udostępnia dla wyrażeń dopasowywania wzorców: kompilator ostrzega cię, jeśli nie obsłużysz każdej wartości wejściowej. Kompilator wyświetla również ostrzeżenie, jeśli wzorzec ramienia przełącznika jest objęty poprzednim wzorcem. Zapewnia to swobodę refaktoryzacji i zmiany kolejności wyrażeń przełącznika. Innym sposobem na napisanie tego samego wyrażenia może być:

string WaterState2(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        < 32 => "solid",
        32 => "solid/liquid transition",
        < 212 => "liquid",
        212 => "liquid / gas transition",
        _ => "gas",
};

Kluczowa lekcja w poprzednim przykładzie i wszelkie inne refaktoryzacja lub zmiana kolejności jest to, że kompilator sprawdza, czy kod obsługuje wszystkie możliwe dane wejściowe.

Wiele danych wejściowych

Wszystkie omówione do tej pory wzorce sprawdzały jedno dane wejściowe. Można pisać wzorce, które badają wiele właściwości obiektu. Rozważmy następujący Order rekord:

public record Order(int Items, decimal Cost);

Poprzedni typ rekordu pozycyjnego deklaruje dwa elementy członkowskie w jawnych pozycjach. Najpierw pojawia się element Items, a następnie kolejność Cost. Aby uzyskać więcej informacji, zobacz Rekordy.

Poniższy kod analizuje liczbę elementów i wartość zamówienia, aby obliczyć cenę obniżoną:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        { Items: > 10, Cost: > 1000.00m } => 0.10m,
        { Items: > 5, Cost: > 500.00m } => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

Pierwsze dwa ramiona badają dwie właściwości obiektu Order. Trzeci sprawdza tylko koszt. Następne testy względem nullelementu i końcowe są zgodne z dowolną inną wartością. Order Jeśli typ definiuje odpowiednią Deconstruct metodę, można pominąć nazwy właściwości ze wzorca i użyć dekonstrukcji do zbadania właściwości:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        ( > 10,  > 1000.00m) => 0.10m,
        ( > 5, > 50.00m) => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

Powyższy kod demonstruje wzorzec pozycyjny, w którym właściwości są dekonstrukturowane dla wyrażenia.

Wzorce listy

Elementy można sprawdzić na liście lub tablicy przy użyciu wzorca listy. Wzorzec listy zapewnia metodę stosowania wzorca do dowolnego elementu sekwencji. Ponadto można zastosować wzorzec odrzucenia (_), aby dopasować dowolny element, lub zastosować wzorzec wycinka, aby dopasować zero lub więcej elementów.

Wzorce list są cennym narzędziem, gdy dane nie są zgodne ze zwykłą strukturą. Możesz użyć dopasowania wzorca, aby przetestować kształt i wartości danych zamiast przekształcać je w zestaw obiektów.

Rozważmy następujący fragment pliku tekstowego zawierającego transakcje bankowe:

04-01-2020, DEPOSIT,    Initial deposit,            2250.00
04-15-2020, DEPOSIT,    Refund,                      125.65
04-18-2020, DEPOSIT,    Paycheck,                    825.65
04-22-2020, WITHDRAWAL, Debit,           Groceries,  255.73
05-01-2020, WITHDRAWAL, #1102,           Rent, apt, 2100.00
05-02-2020, INTEREST,                                  0.65
05-07-2020, WITHDRAWAL, Debit,           Movies,      12.57
04-15-2020, FEE,                                       5.55

Jest to format CSV, ale niektóre wiersze mają więcej kolumn niż inne. Jeszcze gorzej w przypadku przetwarzania jedna kolumna w typie WITHDRAWAL zawiera tekst wygenerowany przez użytkownika i może zawierać przecinek w tekście. Wzorzec listy zawierający wzorzec odrzucenia, stały wzorzec i wzorzec var do przechwytywania danych przetwarzania wartości w tym formacie:

decimal balance = 0m;
foreach (string[] transaction in ReadRecords())
{
    balance += transaction switch
    {
        [_, "DEPOSIT", _, var amount]     => decimal.Parse(amount),
        [_, "WITHDRAWAL", .., var amount] => -decimal.Parse(amount),
        [_, "INTEREST", var amount]       => decimal.Parse(amount),
        [_, "FEE", var fee]               => -decimal.Parse(fee),
        _                                 => throw new InvalidOperationException($"Record {string.Join(", ", transaction)} is not in the expected format!"),
    };
    Console.WriteLine($"Record: {string.Join(", ", transaction)}, New balance: {balance:C}");
}

Powyższy przykład przyjmuje tablicę ciągów, gdzie każdy element jest jednym polem w wierszu. Klucze switch wyrażeń w drugim polu, które określa rodzaj transakcji i liczbę pozostałych kolumn. Każdy wiersz zapewnia, że dane są w poprawnym formacie. Wzorzec odrzucenia (_) pomija pierwsze pole z datą transakcji. Drugie pole odpowiada typowi transakcji. Pozostałe dopasowania elementów są pomijane do pola z kwotą. Ostateczne dopasowanie używa wzorca var do przechwytywania reprezentacji ciągu kwoty. Wyrażenie oblicza kwotę do dodania lub odejmowania z salda.

Wzorce listy umożliwiają dopasowanie kształtu sekwencji elementów danych. Aby dopasować lokalizację elementów, należy użyć wzorców odrzucania i wycinków . Używasz innych wzorców, aby dopasować cechy poszczególnych elementów.

Ten artykuł zawiera przewodnik po rodzajach kodu, który można napisać za pomocą dopasowywania wzorców w języku C#. W poniższych artykułach przedstawiono więcej przykładów użycia wzorców w scenariuszach oraz pełne słownictwo wzorców dostępnych do użycia.

Zobacz też