• C# i .NET
  • Switch expression w C# bez pułapek - składnia i wzorce

Switch expression w C# bez pułapek - składnia i wzorce

Bruno Krawczyk 6 czerwca 2026
Kod demonstruje działanie instrukcji switch w C# i jej kompilację do kodu maszynowego. Różne implementacje switch pokazują, jak warunki są sprawdzane.

Spis treści

Gdy jedna wartość może prowadzić do kilku różnych wyników, klasyczny switch szybko obrasta w case, break i zmienne pomocnicze. W C# wyrażenie switch expression pozwala zapisać taką logikę jako krótkie przypisanie wartości, a przy tym korzystać z dopasowania wzorców, zakresów, typów i właściwości. Pokażę składnię, praktyczne przykłady, różnice względem instrukcji switch oraz sytuacje, w których lepiej wybrać inne rozwiązanie.

Najważniejsze informacje o wyrażeniach switch w C#

  • Zwracają wartość, więc można przypisać ich wynik do zmiennej, zwrócić go z metody albo użyć w właściwości.
  • Wykorzystują wzorce dopasowania, między innymi stałe, typy, właściwości, krotki i zakresy liczbowe.
  • Gałęzie są sprawdzane od góry do dołu, dlatego ich kolejność ma znaczenie.
  • Symbol _ pełni rolę domyślnego dopasowania i pomaga obsłużyć wszystkie pozostałe przypadki.
  • Brak pasującej gałęzi może zakończyć się SwitchExpressionException, nawet jeśli kompilator zgłosi tylko ostrzeżenie.

Kod C# z instrukcją `switch` do tworzenia kształtów. Widać fragment kodu, gdzie `switch` obsługuje różne typy kształtów.

Jak działa wyrażenie switch w C#

Najważniejsza różnica względem tradycyjnej instrukcji polega na tym, że wyrażenie switch zawsze produkuje wynik. Po lewej stronie znajduje się wartość wejściowa, a po prawej lista ramion, czyli par wzorzec i zwracane wyrażenie.

int GetPriority(string status) =>
    status switch
    {
        "urgent" => 3,
        "normal" => 2,
        "low" => 1,
        _ => 0
    };

Każde ramię ma postać wzorzec => wynik. C# wybiera pierwszy pasujący wzorzec, oblicza jego prawą stronę i kończy dopasowanie. Podkreślam to, bo kolejność nie jest wyłącznie kwestią stylu. Bardziej szczegółowe warunki powinny znaleźć się przed ogólniejszymi.

Wyniki wszystkich ramion muszą dać się sprowadzić do wspólnego typu. Nie muszą być zapisane jako proste literały. Możesz zwrócić obiekt, wywołać metodę, utworzyć krotkę albo użyć kolejnego wyrażenia.

string FormatRole(User user) =>
    user.Role switch
    {
        Role.Admin => "Administrator",
        Role.Editor => "Redaktor",
        Role.Reader => "Czytelnik",
        _ => "Nieznana rola"
    };

To dobrze pasuje do metod jednolinijkowych i właściwości obliczanych. W praktyce najwięcej zyskuję wtedy, gdy logika sprowadza się do mapowania jednej wartości na inną, bez efektów ubocznych po drodze.

Wzorce pozwalają wyjść poza zwykłe porównanie wartości

Najprostsze przypadki używają stałych, na przykład nazw statusów, wartości enuma czy kodów HTTP. Siła tego mechanizmu zaczyna być widoczna dopiero wtedy, gdy dopasowanie dotyczy kształtu danych, a nie tylko ich równości.

Zakresy liczbowe

Wzorce relacyjne zapisuje się za pomocą operatorów <, <=, > i >=. Dzięki nim nie trzeba budować kilku warunków if połączonych operatorami logicznymi.

string GetTemperatureMessage(double temperature) =>
    temperature switch
    {
        < 0 => "Mróz",
        >= 0 and < 15 => "Chłodno",
        >= 15 and < 25 => "Przyjemnie",
        _ => "Gorąco"
    };

W tym przykładzie and łączy dwa warunki w jeden wzorzec. Możesz też użyć or oraz not, na przykład not null. Taki zapis jest zwykle czytelniejszy niż ręczne sprawdzanie granic i zmniejsza ryzyko pomyłki przy przedziałach.

Wzorce właściwości i typów

Jeżeli obiekt ma kilka cech wpływających na wynik, nie musisz rozbijać logiki na serię zagnieżdżonych instrukcji. Wzorzec właściwości pozwala sprawdzić wybrane pola bez wcześniejszego przypisywania obiektu do zmiennej pomocniczej.

string GetDiscount(Customer customer) =>
    customer switch
    {
        { IsPremium: true, OrdersCount: >= 10 } => "20%",
        { IsPremium: true } => "10%",
        { OrdersCount: 0 } => "5% na pierwsze zamówienie",
        _ => "Brak rabatu"
    };

Możesz również dopasować typ i od razu otrzymać zmienną o właściwym typie.

string DescribeMessage(Message message) =>
    message switch
    {
        TextMessage { Text.Length: > 100 } text => $"Długi tekst: {text.Text.Length} znaków",
        TextMessage text => $"Tekst: {text.Text}",
        ImageMessage image => $"Obraz {image.Width}x{image.Height}",
        _ => "Nieobsługiwany typ wiadomości"
    };

Ten styl dobrze współpracuje z rekordami i hierarchiami modeli domenowych. Trzeba jednak zachować umiar. Gdy wzorzec zaczyna zawierać wiele zagnieżdżonych właściwości, nazwij regułę osobną metodą, bo krótszy kod nie zawsze oznacza prostszy kod.

Krotki i kilka wartości wejściowych

Wyrażenie przełączające może analizować krotkę, gdy wynik zależy od więcej niż jednej wartości. To przydatne na przykład przy wyznaczaniu uprawnień na podstawie roli i stanu konta.

string GetAccessLevel(Role role, bool isActive) =>
    (role, isActive) switch
    {
        (Role.Admin, true) => "Pełny dostęp",
        (Role.Editor, true) => "Edycja danych",
        (_, false) => "Konto nieaktywne",
        _ => "Dostęp tylko do odczytu"
    };

Warto zauważyć, że ramię (_, false) obejmuje każdą rolę przy nieaktywnym koncie. To dobry przykład świadomego użycia ogólnego wzorca po wcześniejszych, bardziej precyzyjnych przypadkach.

Wyrażenie switch czy instrukcja switch

Obie konstrukcje rozwiązują podobny problem, ale pasują do innych rodzajów logiki. Wyrażenie wybieram wtedy, gdy chcę obliczyć jedną wartość. Instrukcja sprawdza się lepiej, gdy każda gałąź wykonuje kilka operacji, modyfikuje stan albo kończy metodę w inny sposób.

Cecha Wyrażenie switch Instrukcja switch
Rezultat Zwraca wartość Wykonuje instrukcje
Składnia gałęzi Wzorzec i => case, instrukcje i często break
Najlepsze zastosowanie Mapowanie, klasyfikacja, wybór wyniku Operacje uboczne, logowanie, zmiana stanu
Ryzyko Zbyt złożone wzorce utrudniają czytanie Więcej kodu i większa szansa na pomyłkę w przepływie

Porównanie z if i else jest podobne. Dla dwóch prostych warunków if często będzie naturalniejszy. Kiedy przypadków przybywa, dopasowanie wzorców porządkuje kod i wyraźnie pokazuje, jakie dane są obsługiwane.

Nie próbuję jednak przepisywać każdej instrukcji na siłę. Jeżeli gałąź ma wywołać kilka metod, zapisać zdarzenie i zmienić kilka pól, klasyczny switch może być bardziej komunikatywny. Przy skokach między gałęziami przydatne będzie także omówienie goto w C#, choć w nowym kodzie takie sterowanie stosuję bardzo oszczędnie.

Kompletność dopasowania i typowe pułapki

Najczęstszy błąd polega na założeniu, że skoro kod się kompiluje, to każda wartość ma obsłużony przypadek. Kompilator potrafi ostrzec o niekompletnym dopasowaniu, ale w czasie działania brak pasującego ramienia może zakończyć się wyjątkiem.

string ToLabel(int value) =>
    value switch
    {
        1 => "Jeden",
        2 => "Dwa"
    };

Bez ramienia domyślnego kod nie definiuje zachowania dla innych liczb. Najprostsza naprawa to dodanie _, ale nie zawsze powinien on zwracać cichą wartość zastępczą.

string ToLabel(int value) =>
    value switch
    {
        1 => "Jeden",
        2 => "Dwa",
        _ => throw new ArgumentOutOfRangeException(nameof(value))
    };

Takie jawne zgłoszenie błędu jest lepsze, gdy nieznana wartość oznacza uszkodzone dane albo błąd programistyczny. Z kolei _ => "Nieznany" pasuje do danych zewnętrznych, na przykład wartości odczytanych z API, gdzie nowe warianty mogą pojawić się po wdrożeniu aplikacji.

Przeczytaj również: Mutex czy SemaphoreSlim? Jak wybrać w C# i .NET

Null i kolejność ramion

Wzorzec _ dopasowuje również null, dlatego może pełnić rolę zabezpieczenia na końcu. Wzorce typów, takie jak string text, nie dopasują wartości null, więc osobny przypadek null bywa potrzebny.

string Describe(string? value) =>
    value switch
    {
        null => "Brak wartości",
        { Length: 0 } => "Pusty tekst",
        string text => $"Tekst o długości {text.Length}",
    };

Uważaj też na zbyt ogólne wzorce umieszczone za wysoko. Ramię _ albo szeroki wzorzec typu może zasłonić kolejne przypadki, a kompilator zgłosi wtedy błąd nieosiągalnego dopasowania. Czytam gałęzie od góry jak reguły w filtrze: najpierw wyjątki, potem przypadki ogólne.

Praktyczne zastosowania w aplikacji .NET

Najbardziej użyteczne przykłady pojawiają się w miejscach, gdzie dane z jednej warstwy trzeba zamienić na model lub decyzję dla innej warstwy. Dobrym przypadkiem jest mapowanie statusu odpowiedzi HTTP na rezultat aplikacji.

ApiResult MapResponse(HttpResponseMessage response) =>
    (int)response.StatusCode switch
    {
        >= 200 and < 300 => ApiResult.Success(),
        401 or 403 => ApiResult.Unauthorized(),
        404 => ApiResult.NotFound(),
        >= 500 => ApiResult.ServerError(),
        _ => ApiResult.Failure()
    };

W jednym miejscu widać cały kontrakt mapowania, a zakresy statusów nie są ukryte w kilku instrukcjach warunkowych. Podobnie można przekształcać komendy na odpowiednie handlery, zdarzenia domenowe na komunikaty logów albo konfigurację na konkretną strategię działania.

W kodzie korzystającym z LINQ rozdzielam odpowiedzialności. Wyrażenie switch dobrze nadaje się do wyliczenia wartości w Select, ale nie zastępuje operacji łączenia kolekcji. Gdy trzeba połączyć dane z dwóch źródeł, przydatna będzie składnia Join w LINQ, a dopiero później wynik można sklasyfikować za pomocą wzorców.

var labels = orders
    .Select(order => new
    {
        order.Id,
        Category = order.Total switch
        {
            < 100 => "Małe zamówienie",
            >= 100 and < 500 => "Standardowe zamówienie",
            _ => "Duże zamówienie"
        }
    });

To połączenie jest czytelne, bo LINQ odpowiada za pracę na kolekcji, a dopasowanie za decyzję dotyczącą pojedynczego elementu. Gdy jedna z tych konstrukcji zaczyna przejmować wszystkie obowiązki, zwykle jest to sygnał, że logikę trzeba rozbić na nazwane metody.

Jak stosować tę konstrukcję bez utraty czytelności

Najlepiej zaczynać od prostego pytania: czy mam jedną wartość wejściową i chcę wyznaczyć na jej podstawie jeden wynik? Jeśli tak, wyrażenie będzie dobrym kandydatem. Jeżeli odpowiedź brzmi „chcę wykonać serię działań”, klasyczna instrukcja albo osobna metoda prawdopodobnie sprawdzi się lepiej.

  • Umieszczaj najbardziej szczegółowe wzorce przed ogólnymi.
  • Dodawaj ramię domyślne, gdy dane mogą zawierać nieznane wartości.
  • Zamiast rozbudowanego wzorca wydziel regułę do nazwanej metody.
  • Nie mieszaj w jednym wyrażeniu mapowania, logowania i modyfikowania stanu.
  • Włącz analizator stylu IDE0066 tylko wtedy, gdy preferencja zespołu jest spójna z charakterem projektu.

Moja praktyczna granica jest prosta: jeśli trzeba dłużej tłumaczyć, co oznacza rząd zagnieżdżonych wzorców, kod przestał być zwięzły w dobrym sensie. Lepiej wtedy nazwać reguły biznesowe i zostawić w wyrażeniu jedynie przejrzyste rozgałęzienie.

Dobry moment na użycie wzorców w codziennym kodzie

Wyrażenie switch daje najwięcej, gdy porządkuje decyzję, która wcześniej była rozproszona między if, zmiennymi pomocniczymi i ręcznymi konwersjami. W C# od wersji 8 jest pełnoprawnym narzędziem do pracy z pattern matchingiem, ale jego wartość nie polega na samym skróceniu liczby linii.

Stosuj je do mapowania i klasyfikacji, pilnuj kompletności przypadków, a dla efektów ubocznych wybieraj konstrukcję, która lepiej pokazuje przepływ działania. Taka dyscyplina sprawia, że kod jest nie tylko krótszy, lecz przede wszystkim łatwiejszy do rozbudowy, testowania i bezpiecznego utrzymania.

FAQ - Najczęstsze pytania

Wyrażenie switch sprawdza się, gdy na podstawie jednej wartości chcesz obliczyć i zwrócić jeden wynik, na przykład etykietę, status lub obiekt. Instrukcja switch będzie lepsza, gdy gałęzie wykonują kilka operacji, modyfikują stan, logują zdarzenia albo mają efekty uboczne.

Użyj wzorców relacyjnych z operatorami <, <=, > i >=, łącząc je słowami and lub or. Przykładowo zapis >= 15 and < 25 dopasowuje wartości od 15 do mniej niż 25.

Brak dopasowania może zakończyć się wyjątkiem SwitchExpressionException, nawet jeśli kompilator zgłosił tylko ostrzeżenie. Dodaj ramię _, aby zwrócić wartość domyślną albo jawnie zgłosić ArgumentOutOfRangeException, gdy nieznana wartość oznacza błąd danych.

Gałęzie są sprawdzane od góry do dołu, a C# wybiera pierwszy pasujący wzorzec. Najpierw umieszczaj przypadki szczegółowe, takie jak konkretne typy lub kombinacje właściwości, a dopiero później wzorce ogólne i ramię _.

W LINQ możesz użyć wyrażenia switch w metodzie Select do sklasyfikowania pojedynczego elementu kolekcji, na przykład zamiany wartości zamówienia na kategorię. LINQ odpowiada wtedy za pracę na kolekcji, a switch za wybór wyniku dla jednego elementu.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

pattern matching
linq
wzorce relacyjne
wyrażenia switch
krotki
Autor Bruno Krawczyk
Bruno Krawczyk
Mam na imię Bruno i od 8 lat zgłębiam tajniki programowania w ekosystemie .NET, chmury Azure oraz sztucznej inteligencji. Moja przygoda z technologią zaczęła się od ciekawości, jak złożone systemy mogą ułatwiać codzienne życie i rozwiązywać realne problemy. Dziś moją misją jest dzielenie się tą wiedzą, starając się przybliżyć nawet najbardziej skomplikowane zagadnienia w sposób zrozumiały i przystępny dla każdego. W moich artykułach na kursdotnet.pl skupiam się na praktycznych aspektach, analizuję najnowsze trendy i weryfikuję informacje, aby dostarczyć Wam treści, które są nie tylko dokładne i aktualne, ale przede wszystkim użyteczne w Waszej własnej ścieżce rozwoju technologicznego.

Udostępnij artykuł

Napisz komentarz