Spis treści Poprzednia strona: Typy Następna strona: Łańcuchy znaków
Wyliczenie [enumeration] należy traktować tak naprawdę jako typ całkowitoliczbowy (tzn. byte, sbyte, short, ushort, int, uint, long albo ulong), w którym definiujemy kilka nazwanych stałych. Deklaracja wyliczenia jest w zasadzie deklaracją klasy, czyli może wystąpić w klasie (jako tzw. klasa zagnieżdżona) albo poza klasą (jako po prostu kolejna klasa w przestrzeni nazw). Przykłady z enum:
namespace Testy { enum Zwierz { Ryba, Płaz, Gad, Ptak, Ssak, Bezżuchwowiec } class TestyEnum { enum Signum : sbyte { Negative = -1, Zero = 0, Positive } static void Main(string[] args) { Signum s = (Signum)3; Console.WriteLine(Signum.Positive); Console.WriteLine(1 + Signum.Positive); Console.WriteLine((int)Signum.Positive); Console.WriteLine(s); string z = Enum.GetName(typeof(Signum), 0); Console.WriteLine(z); Console.WriteLine(typeof(Zwierz)); Console.WriteLine(typeof(Signum)); Console.WriteLine(s.GetType()); Console.WriteLine(Enum.GetUnderlyingType(typeof(Zwierz))); Console.WriteLine(Enum.GetUnderlyingType(typeof(Signum))); Console.WriteLine(Enum.ToObject(typeof(Signum), -1)); Console.ReadKey(true); } } }
W liniach od 3 do 11 mamy deklarację wyliczenia Zwierz. W nawiasach klamrowych jest lista stałych oddzielonych przecinkami. Pierwsza stała o nazwie Ryba ma domyślnie wartość 0, a kolejne stałe mają wartości o jeden większe od poprzedniej.
Jeśli nie podamy jaki jest typ całkowity wyliczenia [underlying type], to domyślnie jest nim int. Linia 29 spowoduje wypisanie na konsoli System.Int32 przy pomocy statycznej metody GetUnderlyingType() klasy System.Enum. Metodzie tej przekazujemy jako parametr obiekt klasy Type, czyli typ. W języku C# jest kilka sposobów na dostanie się do typu, z których podstawowe dwa to: przez nazwę typu jeśli ją znamy i przez obiekt jeśli go mamy. Przez nazwę typu możemy otrzymać typ dzięki specjalnej pseudofunkcji używającej słowa kluczowego typeof(). Przez obiekt możemy otrzymać typ dzięki metodzie GetType(), którą posiada każdy obiekt (dziedziczy ją po obiekcie podstawowym object). Przykład wykorzystania metody GetType znajduje się w linii 28.
Jeśli chcemy wymusić jakiś konkretny typ całkowity wyliczenia (na przykład sbyte zamiast domyślnego int), to robimy to tak, jak w linii 15, po dwukropku zaraz po nazwie wyliczenia. Dzięki temu zmienna s z linii 19 będzie zajmować 1 bajt pamięci zamiast 4 bajtów. Jak to sprawdzić? Podobną do typeof() pseudofunkcją jest sizeof(), która podaje rozmiar typu w bajtach i tak: sizeof(int) zwraca wartość 4, a sizeof(sbyte) wartość 1. Stała Signum.Positive będzie miała wartość 1, bo jest następna po wartości 0.
W linii 19 mamy dowód na to, że typ wyliczeniowy może przyjmować wartości inne niż te podane w jego definicji. Może on przyjmować każdą wartość z typu całkowitego, na którym jest zdefiniowany. Nie wystarczy napisać s = 3, ale trzeba tak s = (Signum)3. Oznacza to, że nie istnieje niejawna konwersja [implicite conversion] typu całkowitego na wyliczeniowy. Istnieje natomiast konwersja jawna [explicite conversion] i ją właśnie przeprowadzamy wykonując rzutowanie na typ Signum przy pomocy nawiasu s = (Signum)3. To ogólna reguła. Aby przeprowadzić konwersję z jednego typu na drugi (w tym wypadku z int na Signum), musi istnieć możliwość konwersji jawnej (przez rzutowanie) albo niejawnej (po prostu przez przypisanie lub podstawienie za argument metody). Na przykład liczby całkowite mają niejawne konwersje na liczby zmiennoprzecinkowe. Jeśli uważasz, że s = 3 jest łatwiejsze niż s = (Signum)3, masz tylko połowę racji. Typ wyliczeniowy służy do tego, aby przechowywać w nim tylko wartości zdefiniowanych stałych i właśnie dlatego przechowywanie innych wartości zostało specjalnie utrudnione (wymaga jawnej konwersji). Kiedy będziemy tworzyć własne klasy (a klasy to przecież typy) będziemy mogli sami definiować wszystkie możliwe konwersje jawne i niejawne na inne klasy. Zobaczymy, że to dość potężne narzędzie.
Co wypisze na konsoli linia 20? Wartość 1? Nie. Wypisze słowo Positive, czyli nazwę stałej. Jak to możliwe skoro to zwykła liczba? Właśnie dlatego, że to nie jest zwykła liczba, to jest wyliczenie. Tak zostało zaprojektowane żeby wypisać właśnie tekst a nie liczbę. Aby otrzymać liczbę musimy rzutować na typ całkowity tak jak w linii 22, która wypisze wartość stałej Signum.Positive, czyli 1. Linia 21 wypisze 2 dlatego, że Signum nie zawiera nazwy dla liczby 2 (tak, dodanie int do Signum nie powoduje zmiany typu na int, typ pozostaje Signum). Linia 23 na tej samej zasadzie wypisze oczywiście 3.
Dlaczego w ogóle do metody Console.WriteLine() możemy przekazać liczbę, a nie tekst? W zasadzie możemy tej metodzie przekazać dowolny obiekt. Liczba (tak jak każdy inny obiekt) zostanie automatycznie zamieniona na tekst przez metodę WriteLine(). W jaki sposób? Metoda WriteLine() wewnętrznie wywołuje metodę ToString() obiektu, która zamienia każdy obiekt na jego reprezentację tekstową (jeśli wie jak to zrobić, bo w przeciwnym razie wynikiem jest tylko pełna nazwa typu). Tak, metodę ToString() ma każdy obiekt, nawet obiekt string (wtedy metoda ta nic nie robi oprócz zwrócenia jako wartość tego samego obiektu).
W linii 24 mamy przykład jak z liczby (tutaj 0) uzyskać nazwę stałej typu wyliczeniowego (jeśli istnieje, bo w przeciwnym razie dostaniemy pusty tekst, czyli stałą String.Empty). Linia 31 pokazuje przykład jak z liczby uzyskać wartość typu wyliczeniowego w inny sposób niż przez rzutowanie.
Na pewno zwrócił twoją uwagę (a jeśli nie, to powinien zwrócić) wynik linii 26 i 27. Wypisanie typu (obiektu klasy Type) daje pełną nazwę tego typu (pełną, czyli razem z przestrzenią nazw). O ile Testy.Zwierz jest oczywistą nazwą typu wyliczeniowego Zwierz, bo umieściliśmy jego definicję poza klasą TestyEnum, czyli stanowi on osobną (kolejną) klasę w przestrzeni nazw Testy (i zgodnie z tym, co mówiliśmy poprzednio powinien znaleźć się w osobnym pliku – akurat ta reguła często nie dotyczy właśnie wyliczeń). Dobrze, ale dlaczego pełna nazwa typu Signum, to Testy.TestyEnum+Signum, a nie Testy.TestyEnum.Signum? Co tam robi symbol plusa zamiast kropki? Plus oznacza w tym wypadku, że Signum jest klasą zagnieżdżoną w klasie TestyEnum.
Czasami warto sprawdzić czy dana liczba jest wyliczeniem. Służy do tego metoda IsEnum() klasy Type. Jeśli liczba.GetType().IsEnum() zwraca wartość logiczną true, to liczba jest wyliczeniem i prawdopodobnie jest jedną ze stałych tam nazwanych. Właśnie, prawdopodobnie, bo widzieliśmy już, że wyliczenie może też przechowywać inne wartości. Ale po co? Dlaczego twórcy języka C# po prostu nie zabronili dostępu do innych wartości? Z dwóch powodów: dla zwiększenia wydajności (sprawdzanie czy wartość jest dozwolona za każdym razem byłoby zbyt czasochłonne – dla komputera rzecz jasna) i dla możliwości przechowywania flag.
Ostatnią kwestią związaną z wyliczeniami jest właśnie koncepcja flag. Flagi bitowe to sposób przechowywania wielu wartości logicznych w postaci pojedynczych bitów. Bity te są zakodowane w liczbach całkowitych. Wyliczenie może reprezentować flagi jeśli opatrzymy je tzw. atrybutem Flags. Oto przykład:
[Flags] enum Intel_x86_FLAGS16 : ushort { Carry = 1, Parity = 4, Adjust = 16, Zero = 64, Sign = 128, Trap = 256, Interrupt = 512, Direction = 1024, Overflow = 2048, IOPL_Mask = 4096 + 8192, // to samo, co (4096 | 8192) NT = 16384, ZeroCarry = Zero | Carry // = 65 }
W linii 1 mamy atrybut Flags, który dekoruje wyliczenie. Na razie wystarczy wiedzieć, że atrybut to „coś” w nawiasach kwadratowych przed definicją. Atrybuty są częścią metadanych i są zapisywane w assembly. Więcej o atrybutach w rozdziale o refleksji. Co powoduje ten atrybut? Niewiele, to raczej przypomnienie dla programisty o sposobie korzystania z tego wyliczenia. Zmienia się jednak działanie metody ToString() wywoływanej na zmiennych typu wyliczeniowego jeśli zawierają kombinacje flag (np.: IOPL_Mask albo ZeroCarry z przykładu). Sprawdź to. Stałym w definicji wyliczenia flag należy przypisywać wartości, które są potęgami 2 albo ich kombinacje bitowe (logiczne sumy – OR). Zmiennym flagowym (zmiennym typu wyliczeniowego opatrzonego atrybutem Flags) możemy przypisać dowolną kombinację tych stałych. Atrybut Flags nie zmienia domyślnego sposobu przydzielania wartości dla stałych, tzn. potęgi 2 nie przydzielają się automatycznie i trzeba je wszystkie przypisać do stałych jawnie.
- Aby sprawdzić czy flaga MyEnum.F jest ustawiona w zmiennej x używamy wyrażenia logicznego ((x & MyEnum.F) == MyEnum.F) albo od wersji 4.0 .NET Framework x.HasFlag(MyEnum.F);
- Aby ustawić flagę (bez względu na jej poprzedni stan) używamy x |= MyEnum.F;
- Aby przełączyć flagę na przeciwną x ^= MyEnum.F;
- Aby wyzerować flagę x &= ~MyEnum.F;
Spis treści Poprzednia strona: Typy Następna strona: Łańcuchy znaków