Udostępnij za pośrednictwem


Omówienie dopasowywania wzorca

Dopasowywanie wzorca to technika, w której testujesz wyrażenie w celu określenia, czy ma pewne cechy. Dopasowywanie wzorców w języku C# zapewnia bardziej zwięzłą składnię do sprawdzania wyrażeń i podejmowania działań, gdy wyrażenie zostanie dopasowane. 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 są null. Typ wartości dopuszczającej wartość null można przetestować i przekonwertować na odpowiedni typ bazowy, korzystając z następującego przykładu: null.

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 null, poprzez dodanie wzorca not.

string? message = ReadMessageOrDefault();

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

W poprzednim przykładzie użyto wzorca stałej, aby porównać zmienną do null. Elementem not jest wzorzec logiczny, który pasuje, gdy negowany wzorzec nie pasuje.

Testy typów

Innym typowym zastosowaniem dopasowania wzorca jest przetestowanie zmiennej w celu sprawdzenia, czy jest ona zgodna z danym typem. Na przykład poniższy kod sprawdza, czy zmienna nie jest nullem i implementuje interfejs System.Collections.Generic.IList<T>. 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 null, a także przed typem, który nie implementuje IList.

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. Ostatni przypadek _ to 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 switch zgłasza wyjątek, jeśli badany obiekt nie pasuje do żadnego z ramion przełącznika. Możesz użyć stałych liczbowych zamiast wartości wyliczeniowych. 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 => "solid",
        32 => "solid/liquid transition",
        (> 32) and (< 212) => "liquid",
        212 => "liquid / gas transition",
        > 212 => "gas",
    };

Powyższy kod demonstruje również wzorzec logiczny sprzężenia and, 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. Dwie jawne ramiona przełącznika (32°F i 212°F) obsługują przypadki topnienia 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, a także wszelkie inne refaktoryzacje lub zmiany kolejności, polega na tym, ż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 dwóch członków 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 null, a końcowe pasują do dowolnej innej 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 wzór pozycyjny, w którym właściwości są dekomponowane dla wyrażenia.

Można również dopasować właściwość do { }, która pasuje do dowolnej wartości innej niż null. Rozważ następującą deklarację, która przechowuje miary z opcjonalną adnotacją:

public record class Observation(int Value, string Units, string Name)
{
    public string? Annotation { get; set; }
}

Możesz sprawdzić, czy dana obserwacja ma adnotację inną niż null, używając następującego wyrażenia dopasowania wzorca:

if (observation.Annotation is { })
{
    Console.WriteLine($"Observation description: {observation.Annotation}");
}

Wzorce listy

Elementy można sprawdzić na liście lub tablicy za pomocą wzorca listowego. 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. Wyrażenie switch jest oparte na drugim polu, które określa rodzaj transakcji oraz 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 łańcucha reprezentującego kwotę. Wyrażenie oblicza kwotę do dodania lub odejmowania z salda.

Wzorce listowe umożliwiają dopasowanie się do struktury sekwencji elementów danych. Możesz użyć wzorców discard i slice do dopasowania lokalizacji elementó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ż