Menu

Wyrażenie switch w C#: składnia, wzorce, discard i when

Wyrażenie switch z C# 8 zamienia wartość na wynik za pomocą ramion ze wzorcami zamiast case i break. Składnia, discard _, wzorce właściwości, krotek i relacyjne, and/or/not, warunki when, ostrzeżenia o niepełnym pokryciu i SwitchExpressionException, z odpowiednikami w C# 7 do uruchomienia.

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

Wyrażenie switch mapuje wartość wejściową na wynik. Dodano je w C# 8 i zastępuje ono częsty wzorzec instrukcji switch, w której każdy case tylko coś zwraca albo przypisuje:

// C# 8 and later
string StatusText(int code) => code switch
{
    200 => "OK",
    301 or 302 => "Redirect",   // 'or' needs C# 9
    404 => "Not Found",
    >= 500 => "Server Error",   // relational patterns need C# 9
    _ => "Unknown"
};

Czytaj je od góry do dołu: wartość przed switch jest po kolei sprawdzana z każdym ramieniem, a wynik daje pierwsze ramię, którego wzorzec pasuje. Nie ma słów kluczowych case, break ani default; _ (wzorzec discard) pasuje do wszystkiego i służy jako wartość awaryjna.

W C# 7 to samo mapowanie to instrukcja switch w małej metodzie, z return w każdym case:

Wynik:

200 OK
302 Redirect
404 Not Found
503 Server Error
418 Unknown

Forma wyrażenia jest krótsza głównie dlatego, że każde ramię to jedna linia i znika ciało metody. Jest też wyrażeniem, więc może stać wszędzie tam, gdzie wartość: jako argument, w miejscu wstawienia interpolacji, w return, w inicjalizatorze pola.

Zasady składni

var result = input switch
{
    pattern1 => value1,
    pattern2 when condition => value2,
    _ => fallback
};
  • Najpierw jest wartość wejściowa, potem switch. To odwrotność formy instrukcji.
  • Każde ramię to pattern => expression. Prawa strona musi być pojedynczym wyrażeniem; przy większej logice wywołaj metodę.
  • Ramiona rozdziela się przecinkami. Przecinek po ostatnim ramieniu jest dozwolony.
  • Całość jest wyrażeniem, więc instrukcja, która go używa, kończy się ; po nawiasie zamykającym.
  • Wszystkie ramiona muszą dawać wartości wspólnego typu. var x = n switch { 0 => "zero", _ => 0 }; się nie kompiluje (CS8506, brak najlepszego typu). Od C# 9 rozstrzyga to zadeklarowany typ docelowy: object x = n switch { 0 => "zero", _ => 0 }; się kompiluje.
  • Każde ramię musi dawać wartość, więc ramię nie może wywołać metody void, takiej jak Console.WriteLine, a wyrażenie switch nie może stać samo jako instrukcja (CS0201). Do akcji używaj instrukcji switch.
  • Ramię może zamiast wartości rzucić wyjątek: _ => throw new ArgumentOutOfRangeException(nameof(input)).
  • Kompilator odrzuca ramię, do którego nigdy nie da się dojść, bo wcześniejsze ramię już je pokrywa, błędem CS8510, The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match. Umieszczaj szczegółowe ramiona przed ogólnymi.

Wzorce relacyjne i logiczne (C# 9)

C# 9 dodał <, <=, >, >= jako wzorce oraz operatory łączące and, or i not. Dzięki nim tabele zakresów czyta się jak specyfikację:

// C# 9 and later
string Grade(int score) => score switch
{
    < 0 or > 100 => throw new ArgumentOutOfRangeException(nameof(score)),
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    >= 60 => "D",
    _ => "F"
};

string AgeGroup(int age) => age switch
{
    < 13 => "child",
    >= 13 and <= 19 => "teen",
    _ => "adult"
};

bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

Ponieważ ramiona są sprawdzane po kolei, >= 80 widzi tylko wyniki poniżej 90. Wzorce relacyjne przyjmują po prawej stronie tylko stałe; żeby porównać ze zmienną, użyj warunku when. Odpowiednikiem tabeli zakresów w C# 7 jest łańcuch if:

Wynik:

95 -> A
83 -> B
71 -> C
64 -> D
12 -> F

not najczęściej występuje jako not null: x is not null albo ramię not null => x.Name.

Wzorce właściwości

Wzorzec właściwości dopasowuje składniki obiektu: { Property: pattern, ... }. W połączeniu z wyrażeniem switch wyraża reguły biznesowe bez łańcucha warunków &&:

// C# 8 and later
decimal ShippingCost(Order order) => order switch
{
    { Total: >= 100m } => 0m,                       // relational inside: C# 9
    { Country: "US", Express: true } => 15m,
    { Country: "US" } => 5m,
    { Express: true } => 30m,
    null => throw new ArgumentNullException(nameof(order)),
    _ => 12m
};

Żeby ramię pasowało, musi pasować każda wymieniona właściwość. Pusty wzorzec właściwości { } pasuje do każdej wartości różnej od null. C# 10 dodał rozszerzone wzorce właściwości dla zagnieżdżonych składników: { Customer.Address.Country: "US" } zamiast { Customer: { Address: { Country: "US" } } }.

Wzorce krotek

Switch na krotce dopasowuje kilka wartości naraz, co jest czystym sposobem na wyrażenie tabeli decyzyjnej:

// C# 9 and later (the 'or' pattern)
string Winner(string a, string b) => (a, b) switch
{
    ("rock", "scissors") or ("scissors", "paper") or ("paper", "rock") => "player 1",
    var (x, y) when x == y => "draw",
    _ => "player 2"
};

var (x, y) dekonstruuje krotkę na dwie zmienne, które warunek when może porównać. W C# 7 zwykłym zamiennikiem jest połączenie wartości w jeden klucz albo użycie słownika wyników:

Wynik:

player 1
player 2
draw

Wzorce typów i when

Ramię może sprawdzić typ w czasie działania i powiązać zmienną, a klauzula when dodaje dowolny warunek, którego składnia wzorców nie potrafi wyrazić:

// C# 8 and later
double Area(Shape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle r when r.Width == r.Height => r.Width * r.Width,
    Rectangle r => r.Width * r.Height,
    null => throw new ArgumentNullException(nameof(shape)),
    _ => throw new NotSupportedException(shape.GetType().Name)
};

Wersja w C# 7 używa wzorca typu is w łańcuchu if, który wiąże zmienną w ten sam sposób:

Wynik:

Circle: 12.57
Rectangle: 13.50

Pełne pokrycie i SwitchExpressionException

Wyrażenie switch musi dać wartość dla każdego wejścia. Gdy kompilator widzi wejścia, do których nie pasuje żadne ramię, ostrzega:

warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '0' is not covered.

To ostrzeżenie, a nie błąd, więc kod nadal się buduje. Jeśli w czasie działania pojawi się niedopasowana wartość, wyrażenie rzuca System.Runtime.CompilerServices.SwitchExpressionException, którego komunikat kończy się niedopasowaną wartością (Unmatched value was 3.).

Typy wyliczeniowe to przypadek szczególny. Nawet gdy każdy nazwany element ma swoje ramię, zmienna enum może przechowywać dowolną liczbę całkowitą ((Size)3), więc kompilator zgłasza CS8524 dla "nienazwanej wartości enum":

// C# 8 and later
enum Size { Small, Medium, Large }

string Code(Size s) => s switch
{
    Size.Small => "S",
    Size.Medium => "M",
    Size.Large => "L",
    // warning CS8524 without the next arm
    _ => throw new ArgumentOutOfRangeException(nameof(s))
};

Dodaj jawne ramię _, które rzuca czytelny wyjątek. To dokumentuje założenie, wycisza ostrzeżenie i daje jaśniejszy błąd niż SwitchExpressionException, jeśli kiedyś pojawi się zła wartość.

Słownik jako alternatywa

Gdy ramiona to czyste pary stała-wartość, a tabela może rosnąć albo pochodzić z konfiguracji, Dictionary robi to samo w każdej wersji C# i można go zbudować w czasie działania:

Wynik:

19.00
11.50
0

Nieznany kraj dostaje stawkę 0m. Iloczyn typu decimal ma tyle miejsc po przecinku, ile oba czynniki razem, dlatego 100m * 0.19m wypisuje 19.00, a stawka zerowa wypisuje 0.

Wyrażenie switch a instrukcja switch

instrukcja switchwyrażenie switch
Dostępne odC# 1C# 8
Daje wartośćnietak
Składnia gałęzicase X: ... break;X => value,
Wartość awaryjnadefault:_ =>
Kilka instrukcji w gałęzitaknie, wywołaj metodę
Ostrzeżenie o niepełnym pokryciunietak (CS8509)
Brak dopasowania w czasie działanianic się nie wykonujeSwitchExpressionException

Obie formy przyjmują te same wzorce (typy, stałe, właściwości, krotki, relacyjne), więc nauka jednej uczy drugiej. Wybór dotyczy kształtu: wartość przechodzi przez wyrażenie, a ciąg akcji przez instrukcję.

Najczęściej zadawane pytania

Czym jest wyrażenie switch w C#?

Wyrażenie switch, dodane w C# 8, zwraca wartość: string text = code switch { 200 => "OK", 404 => "Not Found", _ => "Unknown" };. Wartość wejściowa stoi przed słowem switch, każde ramię ma postać pattern => result, ramiona rozdziela się przecinkami i nie ma słów kluczowych case, break ani default.

Jaki jest przypadek domyślny w wyrażeniu switch w C#?

Wzorzec discard _ pasuje do wszystkiego, więc _ => "Unknown" jako ostatnie ramię pełni rolę default. Bez ramienia łapiącego wszystko kompilator ostrzega (CS8509), gdy jakieś wejście nie jest pokryte, a niedopasowana wartość w czasie działania rzuca SwitchExpressionException.

Jak dopasować kilka wartości w jednym ramieniu wyrażenia switch?

Użyj wzorca or z C# 9: "sat" or "sun" => "weekend". Dla zakresów połącz wzorce relacyjne przez and: >= 13 and <= 19 => "teen". W C# 8 trzeba wypisać te wartości jako osobne ramiona z tym samym wynikiem.

Czy w wyrażeniu switch w C# można użyć when?

Tak. Ramię może dodać warunek po wzorcu: Order o when o.Total > 100 => 0m. Ramię pasuje tylko wtedy, gdy pasuje wzorzec i warunek when jest prawdziwy; w przeciwnym razie obliczanie przechodzi do następnego ramienia.

Wyrażenie switch czy instrukcja switch?

Używaj wyrażenia, gdy każda gałąź tylko daje wartość, na przykład przy mapowaniu statusu na etykietę albo obliczaniu ceny. Używaj instrukcji, gdy gałęzie wykonują kilka instrukcji, mają efekty uboczne albo muszą wyjść z pętli. Wyrażenie daje też ostrzeżenia o niepełnym pokryciu, których instrukcja nie daje.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ