Menu

Pattern matching w C#: is, wzorce w switch, wzorce właściwości i list

Jak działa dopasowywanie wzorców w C#: operator is z wzorcami typu i stałej, instrukcje switch z wzorcami w case i when, wyrażenia switch, wzorce właściwości, krotek i pozycyjne, wzorce relacyjne i logiczne (and, or, not), wzorce list oraz w której wersji C# pojawił się każdy z nich.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Dopasowywanie wzorców (pattern matching) sprawdza, czy wartość ma określony kształt: "czy to Circle?", "czy to null?", "czy jej Status to "paid", a Total przekracza 100?". Gdy test się powiedzie, wzorzec może też wyciągnąć części wartości do nowych zmiennych. Zaczęło się skromnie w C# 7 od operatora is i rozrastało w każdej kolejnej wersji, dlatego ta strona podaje wersję każdej formy.

Wzorzec typu z is (C# 7)

Przed C# 7 sprawdzenie typu i użycie go wymagało dwóch kroków: as i sprawdzenia null albo is i rzutowania. Wzorzec typu robi obie rzeczy naraz i daje ci otypowaną zmienną:

Wynik:

card ending 4242: 49.90
transfer from PT50...: 1200
no payment
other payment: 5

p is CardPayment card jest prawdziwe tylko wtedy, gdy p to niepusty CardPayment (albo klasa z niego pochodna), a wtedy card przechowuje go z właściwym typem statycznym. Wartość null nigdy nie pasuje do wzorca typu, dlatego płatność null przechodzi dalej do sprawdzenia null.

Zasięg i pewne przypisanie

Zmienna wzorca istnieje w otaczającym bloku, ale kompilator pozwala ją odczytać tylko tam, gdzie wiadomo, że dopasowanie się powiodło. Dzięki temu działa klauzula ochronna (guard clause), w której wychodzisz wcześniej, a zmiennej używasz później:

Wynik:

19.99
42 is over 40
price must be text

boxed is int n && n > 40 pokazuje drugi częsty kształt: zmiennej można używać po prawej stronie &&, bo ta strona wykonuje się tylko po udanym dopasowaniu. Po prawej stronie || nie jest przypisana i kompilator to zgłasza (CS0165).

Wzorce też rozpakowują (unboxing): boxed is int n udaje się dla opakowanego int i daje wartość bez rzutowania, które mogłoby rzucić wyjątek. Dla typów wartościowych nullable maybe is int n udaje się dokładnie wtedy, gdy maybe ma wartość.

Wzorce stałej i null

Stała to również wzorzec. x is null to najczęściej używany, a C# 7 pozwala też na x is 0, status is "paid" albo składową enuma:

if (customer is null) return;          // null check that ignores any == overload
if (count is 0) Console.WriteLine("empty");
if (level is LogLevel.Error) Alert();

Wiele zespołów woli is null od == null, bo nie może go przekierować przeciążenie operatora w danym typie. C# 9 dodaje formę zaprzeczoną, is not null.

Wzorce w instrukcjach switch (C# 7)

Tymi samymi wzorcami typu można oznaczać przypadki case, a klauzula when dodaje warunek. Przypadki są sprawdzane od góry do dołu, więc ustaw je od szczegółowych do ogólnych:

static string Describe(object o)
{
    switch (o)
    {
        case null:                           return "nothing";
        case int n when n < 0:               return "negative number";
        case int n:                          return $"number {n}";
        case string s when s.Length == 0:    return "empty text";
        case string s:                       return $"text of {s.Length} chars";
        case IEnumerable<int> list:          return "a sequence of numbers";
        default:                             return o.GetType().Name;
    }
}

Describe(-4);                 // negative number
Describe("");                 // empty text
Describe(new List<int>());    // a sequence of numbers
Describe(2.5);                // Double

Kompilator sprawdza kolejność: przypadek, który nigdy nie może pasować, bo obejmuje go wcześniejszy, to błąd CS8120. default jest zawsze sprawdzane na końcu, gdziekolwiek stoi. Ta sama logika zapisana łańcuchami if/is, jak w przykładzie z płatnościami powyżej, działa w każdej wersji C# od 7 wzwyż.

Wyrażenia switch (C# 8)

Wyrażenie switch to zwięzła forma dla "oblicz jedną wartość z kilku przypadków". Najpierw wartość, potem switch, a potem gałęzie pattern => result oddzielone przecinkami, z _ jako przypadkiem domyślnym:

public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;

static double Area(Shape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle { Width: var w, Height: var h } => w * h,
    _ => throw new ArgumentException("Unknown shape", nameof(shape)),
};

Area(new Rectangle(3, 4));   // 12

Jeśli w czasie działania żadna gałąź nie pasuje, rzucany jest SwitchExpressionException, a kompilator ostrzega (CS8509), gdy widzi, że jakieś dane wejściowe nie są obsłużone. Składnię opisuje osobno strona o wyrażeniu switch; reszta tej strony dotyczy wzorców, które możesz umieścić w gałęziach.

Wzorce właściwości (C# 8, rozszerzone w C# 10)

Wzorzec właściwości dopasowuje właściwości obiektu do zagnieżdżonych wzorców, w nawiasach klamrowych. Obiekt musi też być różny od null, więc samo { } oznacza "nie null":

public record Address(string City, string Country);
public record Order(decimal Total, string Status, Address ShipTo, int Items);

static string Shipping(Order order) => order switch
{
    { Status: "cancelled" }                   => "no shipment",
    { ShipTo.Country: "PT", Total: >= 50m }   => "free, domestic",     // C# 10 dotted form
    { ShipTo.Country: "PT" }                  => "4.90, domestic",
    { Total: > 200m }                         => "free, international",
    _                                         => "12.00, international",
};

Shipping(new Order(60m, "paid", new Address("Porto", "PT"), 2));    // free, domestic
Shipping(new Order(260m, "paid", new Address("Lyon", "FR"), 2));    // free, international

W C# 8 i 9 wymagana jest forma zagnieżdżona: { ShipTo: { Country: "PT" } }. C# 10 pozwala na skrót z kropkami użyty powyżej. Wzorce właściwości działają też z is, dzięki czemu sprawdzenia wielu warunków czyta się jak opis: if (order is { Status: "paid", Items: > 0 }).

Wzorce relacyjne i logiczne (C# 9)

C# 9 dodał porównania (<, <=, >, >=) i łączniki and, or i not, które zamieniają przedziały we wzorce:

static string Grade(int score) => score switch
{
    < 0 or > 100      => "invalid",
    >= 90             => "A",
    >= 75 and < 90    => "B",
    >= 50             => "C",
    _                 => "F",
};

Grade(82);    // B
Grade(101);   // invalid

if (input is not null and not "") { ... }
if (c is >= 'a' and <= 'z' or >= 'A' and <= 'Z') { ... }   // and binds tighter than or

Odpowiednikiem w C# 7 jest łańcuch if/else if albo switch z klauzulami when (case int s when s >= 90:).

Wzorce krotek i pozycyjne (C# 8)

Przełączanie na kilku wartościach naraz wykonuje się przez switch na krotce:

static string Quadrant(int x, int y) => (x, y) switch
{
    (0, 0)       => "origin",
    (> 0, > 0)   => "I",
    (< 0, > 0)   => "II",
    (< 0, < 0)   => "III",
    (> 0, < 0)   => "IV",
    _            => "on an axis",
};

Quadrant(-2, 5);   // II

Wzorzec pozycyjny robi to samo dla każdego typu z metodą Deconstruct, w tym dla rekordów: case Point(0, 0): albo p is Rectangle(var w, var h) && w == h.

Wzorce list (C# 11)

Wzorce list dopasowują tablice i listy według ich elementów. .. pasuje do dowolnej liczby elementów (tzw. slice), a każda pozycja elementu może zawierać dowolny inny wzorzec:

static string Route(string[] args) => args switch
{
    []                         => "help",
    ["add", var item]          => $"add {item}",
    ["remove", var item, ..]   => $"remove {item}",
    [var cmd, ..]              => $"unknown command {cmd}",
};

Route(new[] { "add", "milk" });             // add milk
Route(new[] { "remove", "milk", "now" });   // remove milk

int[] nums = { 1, 2, 3, 4 };
if (nums is [1, .., var last]) Console.WriteLine(last);   // 4

Która wersja co dodała

WzorzecPrzykładWersja
Wzorzec typu ze zmiennąo is Circle cC# 7.0
Wzorzec stałejx is null, n is 0C# 7.0
Wzorzec varx is var vC# 7.0
Wzorce w etykietach case, strażnicy whencase int n when n < 0:C# 7.0
Wyrażenie switch, odrzucenie _x switch { ... }C# 8
Wzorzec właściwości{ Status: "paid" }C# 8
Wzorzec krotki(x, y) switch { (0, 0) => ... }C# 8
Wzorzec pozycyjnyPoint(0, 0)C# 8
Wzorzec relacyjny> 100, <= 0C# 9
Wzorce logicznenot null, >= 1 and <= 5, 'a' or 'b'C# 9
Goły wzorzec typucase Circle:, Circle => ...C# 9
Rozszerzony wzorzec właściwości{ ShipTo.City: "Porto" }C# 10
Wzorzec listy i slice[first, .., last]C# 11

Typowe błędy

  • Kolejność przypadków od ogólnych do szczegółowych. case Payment p: przed case CardPayment c: sprawia, że drugi jest nieosiągalny (CS8120). Szczegółowe najpierw.
  • Oczekiwanie, że wzorzec typu dopasuje null. null is string s jest fałszywe. Obsłuż null osobnym przypadkiem albo przez is null.
  • Odczyt zmiennej wzorca tam, gdzie dopasowanie mogło się nie udać. Po a is int n || ... zmienna n nie jest przypisana (CS0165).
  • Długie switche po typach zamiast metod wirtualnych. Jeśli każda nowa podklasa oznacza edycję tego samego switcha w pięciu miejscach, zachowanie należy do hierarchii klas. Wzorce błyszczą przy danych z zewnątrz (kształty JSON, komunikaty, krotki danych wejściowych) i przy zamkniętych zbiorach typów, które kontrolujesz.
  • Zapominanie o przypadku domyślnym w wyrażeniu switch. Niedopasowane dane rzucają wyjątek w czasie działania; czytaj ostrzeżenie CS8509.

Najczęściej zadawane pytania

Czym jest pattern matching w C#?

Dopasowywanie wzorców sprawdza, czy wartość ma określony kształt, i gdy pasuje, może w tym samym kroku wyciągnąć jej części do zmiennych. if (shape is Circle c) sprawdza typ i daje ci otypowane c; order is { Status: "paid", Total: > 100 } sprawdza właściwości. Wzorce pojawiają się w wyrażeniach is, instrukcjach switch i wyrażeniach switch.

Jak używać is ze zmienną w C#?

if (obj is Customer c) { ... } sprawdza, czy obj jest typu Customer (i nie jest null), i przypisuje go do c typu Customer wewnątrz if. Zastępuje to starszą parę var c = obj as Customer; if (c != null). Zmienna jest na pewno przypisana tylko tam, gdzie wiadomo, że test wypadł pozytywnie, więc if (!(obj is Customer c)) return; pozwala używać c po if.

Jaka jest różnica między is null a == null w C#?

x is null zawsze sprawdza samą referencję. x == null wywołuje operator == danego typu, jeśli ten go przeciąża, co zwykle daje ten sam wynik, ale nie jest to gwarantowane. Odwrotny test to x != null, !(x is null) albo x is not null od C# 9.

Jak działa switch z pattern matching i when w C#?

Etykieta case może zawierać wzorzec i strażnika when: case Order o when o.Total > 100:. Przypadek pasuje tylko wtedy, gdy pasuje wzorzec i warunek jest prawdziwy, a przypadki są sprawdzane od góry do dołu, więc bardziej szczegółowe umieszczaj wyżej. Kompilator zgłasza CS8120, gdy przypadek nigdy nie może zostać osiągnięty, bo wcześniejszy już go obejmuje.

Która wersja C# dodała które wzorce?

C# 7.0: wzorce typu, stałej i var w is i case, a także when. C# 8: wyrażenia switch, wzorce właściwości, krotek i pozycyjne. C# 9: wzorce relacyjne (> 5), logiczne (and, or, not) i gołe wzorce typu. C# 10: rozszerzone wzorce właściwości ({ Address.City: "Porto" }). C# 11: wzorce list ([1, .., var last]).

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ