Enum (typ wyliczeniowy) to typ, którego wartości tworzą stały zestaw nazwanych stałych: statusy zamówień, dni tygodnia, poziomy logowania. Pod spodem każda nazwa jest liczbą całkowitą, ale system typów pilnuje, żeby OrderStatus nie pomylić ze zwykłym int ani z innym enumem.
Deklarowanie i używanie enuma
Wymień nazwy składowych w nawiasach klamrowych. Domyślnie pierwsza ma wartość 0, a każda kolejna o jeden więcej:
Wynik:
Paid
On its way
True
2
Enum to prawdziwy typ: metody przyjmującej OrderStatus nie da się przez pomyłkę wywołać z 3 ani z LogLevel. Enumy to typy wartościowe, więc nigdy nie są null i porównują się przez == po wartości.
Jawne wartości i typ bazowy
Liczby możesz przypisać sam. Ma to znaczenie zawsze, gdy liczba opuszcza twój program (kolumna w bazie danych, status HTTP, format pliku), bo wtedy zmiana numeracji psuje zapisane dane:
Wynik:
404
Created
418
1
Byte
Zwróć uwagę na dwie rzeczy. Rzutowanie int na enum nigdy nie kończy się błędem: (HttpStatus)418 to poprawna wartość, która po prostu nie ma nazwy, i wypisuje się jako liczba. A typ bazowy może być dowolnym typem całkowitym (byte, short, long, ...), co ma znaczenie tylko w kodzie wrażliwym na rozmiar danych; int jest domyślny i prawie zawsze jest właściwym wyborem.
Gdy później dodajesz składowe, dodawaj je na końcu albo nadawaj jawne wartości. Wstawienie Refunded między Paid a Shipped po cichu zmienia numer każdej kolejnej składowej.
Enum na string
ToString() zwraca nazwę składowej, której używają też Console.WriteLine i interpolacja napisów. Ciągi formatu zmieniają wynik:
Wynik:
Warning
2
00000002
[Warning]
Error
Error
Needs attention
Nazwy składowych to identyfikatory, więc nie mogą zawierać spacji i nie są tłumaczone. Dla tekstu pokazywanego użytkownikom mapuj wartości samodzielnie, tak jak robi to Label, albo przez Dictionary<LogLevel, string>. W niektórych projektach każda składowa dostaje atrybut [Description("Needs attention")], odczytywany przez refleksję; strona o refleksji i atrybutach pokazuje, jak działa takie wyszukiwanie.
String na enum: Parse i TryParse
Enum.Parse zamienia nazwę z powrotem na wartość i rzuca ArgumentException, jeśli nic nie pasuje. Enum.TryParse zamiast tego zwraca false, a tego właśnie chcesz dla wszelkich danych, których nie kontrolujesz:
Wynik:
Large
Medium
Parse threw ArgumentException for Huge
small parsed=True value=Small defined=True
XL parsed=False value=Small defined=True
2 parsed=True value=Large defined=True
7 parsed=True value=7 defined=False
Pułapką są dwa ostatnie wiersze. Obie metody akceptują napisy liczbowe, więc "7" poprawnie parsuje się do Size, który nie ma nazwy. A nieudane TryParse ustawia wynik na 0, czyli tutaj na poprawnie wyglądające Small. Gdy tekst pochodzi z parametrów zapytania, pliku konfiguracyjnego lub formularza, zawsze sprawdzaj zarówno wartość zwracaną, jak i Enum.IsDefined:
if (Enum.TryParse(input, true, out Size size) && Enum.IsDefined(typeof(Size), size))
{
// safe to use size
}
.NET Core 2.0 i nowsze dodają generyczne Enum.Parse<Size>("Large"), które nie wymaga rzutowania.
Wypisywanie wszystkich wartości
Enum.GetValues zwraca każdą składową, posortowaną według wartości liczbowej (porównywanej jako liczba bez znaku, więc składowe ujemne są na końcu); Enum.GetNames zwraca ich nazwy. Tak wypełnisz listę rozwijaną albo zwalidujesz dane względem każdej opcji:
Wynik:
Free 0 EUR/month
Starter 9 EUR/month
Pro 29 EUR/month
Team 99 EUR/month
Free | Starter | Pro | Team
3 paid plans
Enum.GetValues(typeof(Plan)) zwraca zwykłe Array, stąd Cast<Plan>() przed LINQ. W .NET 5 i nowszych Enum.GetValues<Plan>() zwraca od razu typowaną tablicę Plan[].
Flags: łączenie wartości
Niektóre enumy opisują zestaw opcji, a nie jeden wybór: uprawnienia do plików, dni otwarcia sklepu, kanały powiadomień. Nadaj każdej składowej własny bit (1, 2, 4, 8, ...), dodaj None = 0 i oznacz enum atrybutem [Flags]. Wartości łączą się wtedy przez |:
Wynik:
Read, Share
Editor, Share
True
False
Editor
3
Read, Delete
True
Co robi każdy operator: | ustawia bity, & ~X je czyści, ^ je przełącza, a (value & X) != 0 lub value.HasFlag(X) je sprawdza. HasFlag(X) znaczy "wszystkie bity X są ustawione", więc HasFlag(None) jest prawdziwe dla każdej wartości, a HasFlag(Editor) wymaga zarówno Read, jak i Write.
Zwróć uwagę na drugą linię: gdy nazwana kombinacja pokrywa część ustawionych bitów, ToString jej używa, więc Read | Write | Share wypisuje się jako Editor, Share. Pamiętaj o tym, zanim zaczniesz parsować wynik ToString czymkolwiek innym niż Enum.Parse.
Atrybut nie zmienia arytmetyki. Zmienia formatowanie: bez [Flags] Read | Share wypisuje się jako 9, bo żadna pojedyncza składowa nie ma tej wartości. Z atrybutem ToString i Parse działają z postacią rozdzieloną przecinkami. Składowe nadal muszą być potęgami dwójki; zapis Read, Write, Delete z domyślną numeracją (0, 1, 2) sprawia, że Write | Delete równa się 3, czyli wartości bez sensu.
Switch na enumie
switch to naturalny sposób reagowania na enum. Dodaj gałąź default, bo zmienna enuma może przechowywać wartości bez nazwy:
switch (status)
{
case OrderStatus.Pending:
case OrderStatus.Paid:
return "Preparing";
case OrderStatus.Shipped:
return "On the way";
case OrderStatus.Delivered:
return "Delivered";
default:
return "Unknown";
}
Od C# 8 wyrażenie switch jest krótsze. Bez ramienia _ kompilator ostrzega: CS8509, gdy brakuje nazwanej składowej, i CS8524, gdy wszystkie nazwy są obsłużone, ale wartości bez nazwy, takie jak (OrderStatus)7, nie:
string text = status switch
{
OrderStatus.Pending or OrderStatus.Paid => "Preparing", // 'or' pattern: C# 9
OrderStatus.Shipped => "On the way",
OrderStatus.Delivered => "Delivered",
OrderStatus.Cancelled => "Cancelled",
_ => throw new ArgumentOutOfRangeException(nameof(status)),
};
Wartości domyślne i niezdefiniowane
Wartość domyślna każdego enuma to 0, niezależnie od tego, czy jakaś składowa ma tę wartość. Dają ją pola, elementy tablic i nieudane TryParse. Projektuj z myślą o tym:
- Niech
0będzie znaczącą składową "nie ustawiono" (None,Unknown), a nie prawdziwym wyborem. Inaczej niezainicjalizowane pole po cichu odczyta się jako pierwsza prawdziwa opcja. - Liczby z zewnątrz waliduj przez
Enum.IsDefined. Dla enumów[Flags]IsDefinedzwracafalsedla kombinacji bez nazwy (Read | Share), więc zamiast tego sprawdzaj bity:(value & ~Permissions.All) == 0ze składowąAllpokrywającą każdy bit.
Częste błędy
- Poleganie wyłącznie na
TryParse. Napisy liczbowe się parsują, a nieudane parsowanie daje0. DodajEnum.IsDefined. - Poleganie na niejawnej numeracji zapisywanych wartości. Wstawienie składowej zmienia numery kolejnych. Każdemu enumowi, który jest zapisywany, nadaj jawne wartości.
- Flagi, które nie są potęgami dwójki. Domyślna numeracja (0, 1, 2, 3) nakłada bity na siebie. Używaj 1, 2, 4, 8 lub
1 << n. - Pokazywanie
ToString()użytkownikom. Nazwy składowych to identyfikatory w kodzie. Mapuj wartości na tekst do wyświetlania. - Brak
defaultw switch. Enum może przechowywać wartości spoza nazwanych składowych.
Najczęściej zadawane pytania
Jak zamienić enum na string w C#?
Wywołaj ToString(): OrderStatus.Shipped.ToString() zwraca "Shipped", a interpolacja napisów robi to samo. ToString("D") daje zamiast tego liczbę. Dla nazwy znanej w czasie kompilacji nameof(OrderStatus.Shipped) jest stałą. Dla tekstu dla użytkownika, ze spacjami lub tłumaczeniami, mapuj wartości na napisy samodzielnie (przez switch lub słownik) zamiast polegać na nazwie składowej.
Jak zamienić string na enum w C#?
Użyj Enum.TryParse<OrderStatus>(text, true, out var status), które zwraca false zamiast rzucać wyjątek, gdy tekst nie pasuje do żadnej składowej (true wyłącza rozróżnianie wielkości liter). Enum.Parse(typeof(OrderStatus), text) przy błędnych danych rzuca ArgumentException. Obie metody akceptują też napisy liczbowe, takie jak "42", więc gdy dane pochodzą od użytkowników, sprawdź wynik przez Enum.IsDefined.
Jak konwertować między enumem a int w C#?
Rzutuj w dowolnym kierunku: int code = (int)OrderStatus.Paid; i var status = (OrderStatus)2;. Rzutowanie z int nigdy nie kończy się błędem, nawet dla liczb bez pasującej składowej; wynikiem jest wartość enuma, która wypisuje się jako liczba. Gdy liczba pochodzi z zewnątrz, zwaliduj ją przez Enum.IsDefined(typeof(OrderStatus), value).
Jak przejść pętlą po wszystkich wartościach enuma w C#?
foreach (OrderStatus s in Enum.GetValues(typeof(OrderStatus))) odwiedza każdą składową w kolejności ich wartości liczbowych. Od .NET 5 istnieje wersja generyczna, Enum.GetValues<OrderStatus>(), która nie wymaga rzutowania. Enum.GetNames(typeof(OrderStatus)) zwraca nazwy jako napisy.
Co robi [Flags] na enumie w C#?
Oznacza enum, którego wartości to bity przeznaczone do łączenia przez |, na przykład Read | Write. Nadaj każdej składowej potęgę dwójki (1, 2, 4, 8) i dodaj None = 0. Atrybut sprawia, że ToString() wypisuje kombinacje jako "Read, Write", i pozwala Enum.Parse odczytać ten format z powrotem. Bit sprawdzisz przez HasFlag albo (value & Permissions.Write) != 0.