Enum w C# — kompletny przewodnik z pułapkami, o których nikt nie mówi
public class ZamowienieZle
{
public string Status { get; set; } // "Nowe", "Wysłane", "Anulowane"... albo "wysłane", "Wysłano", literówka?
}
Reprezentowanie skończonego zbioru opcji jako stringa albo liczby to zaproszenie do błędów: kompilator nie sprawdzi literówki w "Wysłąne", IntelliSense nie podpowie dostępnych wartości, a porównanie status == "wysłane" po cichu zwróci false przez różnicę wielkości liter. enum rozwiązuje dokładnie ten problem.
Podstawowa składnia
public enum OrderStatus
{
Nowe, // 0
Oplacone, // 1
Wyslane, // 2
Anulowane // 3
}
var status = OrderStatus.Wyslane;
Console.WriteLine(status); // "Wyslane"
Console.WriteLine((int)status); // 2
Każda wartość enum domyślnie dostaje kolejny numer int, zaczynając od 0. Kompilator pilnuje, żeby zmienna typu OrderStatus mogła przyjąć tylko jedną z czterech zdefiniowanych nazw — literówka w OrderStatus.Wyslne nie skompiluje się, w przeciwieństwie do stringa.
Jawne wartości i typ bazowy
public enum HttpStatusKod : short // domyślnie enum bazuje na int, tu wymuszamy short (mniej pamięci)
{
Ok = 200,
NieZnaleziono = 404,
BladSerwera = 500
}
Możesz jawnie przypisać wartości liczbowe (przydatne, gdy enum odzwierciedla istniejący standard, jak kody HTTP) i zmienić typ bazowy z domyślnego int na byte, short, long itd. — istotne, gdy enum trafia do bazy danych albo protokołu sieciowego, gdzie liczy się każdy bajt.
Switch na enum
string OpisStatusu(OrderStatus status) => status switch
{
OrderStatus.Nowe => "Czeka na płatność",
OrderStatus.Oplacone => "Przygotowywane do wysyłki",
OrderStatus.Wyslane => "W drodze",
OrderStatus.Anulowane => "Anulowane",
_ => throw new ArgumentOutOfRangeException(nameof(status))
};
Analizator w Visual Studio/Rider ostrzeże, jeśli switch nie obsłuży wszystkich wartości enum — dodanie nowej wartości do OrderStatus bez zaktualizowania tego switcha da widoczne ostrzeżenie, zamiast cichego pominięcia przypadku w runtime.
[Flags] — enum jako kombinacja bitów
[Flags]
public enum Uprawnienia
{
Brak = 0,
Odczyt = 1, // 0001
Zapis = 2, // 0010
Usuwanie = 4, // 0100
Administrator = 8 // 1000
}
var uprawnienia = Uprawnienia.Odczyt | Uprawnienia.Zapis; // łączenie flag bitowym OR
Console.WriteLine(uprawnienia); // "Odczyt, Zapis"
Console.WriteLine(uprawnienia.HasFlag(Uprawnienia.Zapis)); // True
Atrybut [Flags] pozwala łączyć kilka wartości enum w jedną zmienną operatorem | — każda wartość musi być osobnym bitem (1, 2, 4, 8, 16…), nie kolejną liczbą (1, 2, 3, 4), inaczej kombinacje zaczną się nakładać. To standardowy sposób modelowania zestawu niezależnych opcji (uprawnienia, dni tygodnia, flagi konfiguracyjne) bez osobnej tabeli w bazie danych.
Parse i TryParse — konwersja ze stringa
string wejscie = "Wyslane";
OrderStatus status1 = Enum.Parse<OrderStatus>(wejscie); // rzuci wyjątek, jeśli string nie pasuje
bool ok = Enum.TryParse<OrderStatus>(wejscie, out var status2); // bezpieczna wersja, zwraca bool
TryParse to bezpieczniejszy wybór, gdy string pochodzi z zewnątrz (formularz, API, plik konfiguracyjny) — nie chcesz, żeby literówka użytkownika wywaliła aplikację nieobsłużonym wyjątkiem. Domyślnie Parse/TryParse są wrażliwe na wielkość liter — "wyslane" (małą literą) nie sparsuje się bez dodatkowego parametru ignoreCase: true.
Pułapka: enum to tylko liczba — można wrzucić nieprawidłową wartość
OrderStatus status = (OrderStatus)99; // ❌ kompiluje się i działa — mimo że 99 nie istnieje w enum
Console.WriteLine(status); // wypisze "99", nie nazwę
Console.WriteLine(status == OrderStatus.Nowe); // False, ale to wciąż "nieprawidłowa" wartość
enum w C# to w gruncie rzeczy nazwany zestaw liczb całkowitych — kompilator nie pilnuje, żeby rzutowanie z int zawsze trafiało w jedną z legalnych nazw. Jeśli wartość przychodzi z zewnątrz (baza danych, deserializacja JSON, parametr API), zawsze warto zweryfikować:
if (!Enum.IsDefined(typeof(OrderStatus), status))
throw new ArgumentException($"Nieprawidłowy status: {(int)status}");
Pułapka: zmiana kolejności wartości psuje dane zapisane w bazie
// Wersja 1 aplikacji:
public enum OrderStatus { Nowe, Oplacone, Wyslane, Anulowane } // Wyslane = 2, zapisane w bazie jako 2
// Wersja 2 — ktoś "posprzątał" kolejność:
public enum OrderStatus { Nowe, Anulowane, Oplacone, Wyslane } // teraz 2 = Oplacone!
Jeśli zapisujesz enum do bazy danych jako liczbę (a nie string), zmiana kolejności deklaracji cicho zmienia znaczenie już zapisanych danych — rekord, który wczoraj oznaczał “Wysłane”, dziś czyta się jako “Opłacone”. To jeden z najbardziej podstępnych bugów związanych z enum, bo nic się nie wywala — dane po prostu zaczynają kłamać. Dwa bezpieczne rozwiązania: zawsze przypisuj jawne wartości liczbowe (Nowe = 0, Oplacone = 1, ...) i nigdy ich nie zmieniaj, albo zapisuj enum do bazy jako string (.ToString()), kosztem odrobiny miejsca.
Iterowanie po wszystkich wartościach
foreach (OrderStatus status in Enum.GetValues<OrderStatus>())
{
Console.WriteLine($"{(int)status}: {status}");
}
// 0: Nowe
// 1: Oplacone
// 2: Wyslane
// 3: Anulowane
Przydatne przy generowaniu list rozwijanych w UI albo walidacji “czy ta wartość jest jedną z możliwych”. Enum.GetNames<OrderStatus>() da Ci to samo jako string[], jeśli potrzebujesz tylko nazw bez rzutowania.
Pułapka wydajnościowa: ToString() na enum jest wolniejszy niż się wydaje
for (int i = 0; i < 1_000_000; i++)
{
var opis = status.ToString(); // w gorącej pętli to zaskakująco kosztowne
}
ToString() na enum historycznie korzystał z refleksji, co czyni go zauważalnie wolniejszym niż ToString() na zwykłej liczbie — nieistotne przy pojedynczym wywołaniu, ale realne w gorącej pętli przetwarzającej miliony rekordów (np. serializacja logów). Jeśli profiler pokaże Enum.ToString() jako źródło spowolnienia, rozwiązaniem jest ręczny switch zwracający stały string zamiast polegać na domyślnej konwersji.
Kiedy enum to za mało — smart enum
Jeśli każda wartość powinna nieść dodatkowe dane albo zachowanie (np. OrderStatus.Anulowane powinien “wiedzieć”, że nie da się z niego przejść do Wyslane), zwykły enum nie wystarczy — to sygnał do wzorca smart enum (klasa z prywatnym konstruktorem i statycznymi instancjami zamiast surowych wartości liczbowych). To już jednak temat na osobny artykuł — na start w 95% przypadków zwykły enum załatwia sprawę.
Podsumowanie
enum zamienia zbiór magicznych stringów albo liczb w nazwany, sprawdzany przez kompilator zestaw wartości — literówka nie przejdzie, IntelliSense podpowie opcje, switch ostrzeże o nieobsłużonym przypadku. Pamiętaj o dwóch pułapkach: enum to wciąż tylko liczba, więc rzutowanie nieprawidłowej wartości nie rzuci błędu samo z siebie (waliduj przez Enum.IsDefined), a zmiana kolejności deklaracji potrafi po cichu zepsuć dane już zapisane w bazie — przypisuj jawne wartości liczbowe i ich nie zmieniaj.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →