Flag Enum

Patrząc na niektóre typy wyliczeniowe wbudowane w .NET widać, że są takie, które przyjmują wiele wartości. Pytanie dzisiaj brzmi jak tę mechanikę dodać do swojego typu wyliczeniowego.

We wpisie o typach wyliczeniowych wspomniałem o tym problemie.

A i w tym przykładzie możesz zauważyć pewną nieścisłość. Jak enum może wyrazić , że chmura jest np. pierzasto-kłębiasta.Co jeśli istnieje niesamowita ilość kombinacji wartości. Np. mam typ wyliczeniowy dni tygodnia i chcę aby użytkownik mógł wybrać kombinacje każdego dnia z każdym dniem.

Do nauki tej koncepcji często używa się dni tygodnia więc nie widzę powodu dlaczego ja nie mógłbym tego zrobić.

Akurat nawet istnieje taki wbudowany enum w System.DayOfWeekjednak on przyjmuje tylko pojedyncze wartości. Wygląda tak.

[Serializable]
[ComVisible(true)]
public enum DayOfWeek
{
    Sunday = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6,
}

W tym wpisie napiszę podobny typ wyliczeniowy z tym tylko ,że będzie on mógł przyjmować kilka dni tygodnia.

Definiowanie

Bez zbędnego wydłużania tego wpisu przejdę od razu do rzeczy.

Po pierwsze, gdy chcesz zdefiniować taki typ wyliczeniowy musisz użyć atrybutu [Flags]. Atrybuty są pisane w nawiasach kwadratowych i znajdują się przed deklaracją danego elementu. W poprzednim przykładzie widzisz ,że DayOfWeek ma atrybuty “[ComVisible]” i “[Serializable]” napisane przed deklaracją tego elementu. Nie omówię tutaj co te atrybuty oznaczają. Atrybuty są sygnałem dla kompilatora ,że dany kod powinien być skompilowany w konkretnym stylu bądź do konkretnego celu. Atrybut [Flags] mówi kompilatorowi ,że ten typ wartościowy może przyjmować wiele wartości. Jest to możliwe, ponieważ ten atrybut będzie traktować wartości jak pola bitowe. Atrybut ten może być dodany tylko do typu wyliczeniowego.

Po drugie każde następne wartości w tym typie wyliczeniowym muszą być pomnożone przez dwa i pierwsza wartość musi zaczynać się od 1. Wynika to z tego ,że pola będą traktowane jak pola bitowe.

Wiedząc to wszystko mogę już napisać taki typ wyliczeniowy.

[Flags]enum DniTygodnia
{
    Niedziela = 1,
    Poniedziałek = 2,
    Wtorek = 4,
    Środa = 8,
    Czwartek = 16,
    Piątek = 32,
    Sobota = 64,
}

Mając już ten przykład czas pokazać jak dodać wiele wartości do tego typu wyliczeniowego.

DniTygodnia dniTygodnia = DniTygodnia.Sobota | DniTygodnia.Środa;
DniTygodnia dni = DniTygodnia.Niedziela | DniTygodnia.Poniedziałek;

Console.WriteLine(dniTygodnia.ToString());
Console.WriteLine((int)dniTygodnia); //72 
Console.WriteLine(dni.ToString());
Console.WriteLine((int)dni); //3

W kodzie konsola wyświetli kilka wartości tych typów wyliczeniowych. Zwróć uwagę ,że wartości liczbowe tych zmiennych są w przedziałach pomiędzy pojedynczymi wartościami. Niedziela i poniedziałek ma wartość liczbową trzy . Widać tutaj wyraźnie wzór. Wartość liczbowa poniedziałku została dodana do wartości liczbowej niedzieli, czyli 1 + 2 = 3.

Analogicznie wartość liczbowa soboty i środy to 72, ponieważ 64 + 8 = 72. Teraz wiesz, dlaczego każda następna wartość musi być pomnożona przez 2.

Wartości są traktowane bitowo dodajemy je za pomocą operatorów bitowych, o których na razie nic nie mówiłem na blogu. Operator “|” akurat oznacza operacje lubczyli dodaje ona wartości.

Do typu wyliczeniowego “DniTygodnia” brakuje jeszcze dwóch wartości. Wartości, która opisywałaby wszystkie wybrane dni oraz brak wyboru.

[Flags]
enum DniTygodnia
{
    Niedziela = 1,
    Poniedziałek = 2,
    Wtorek = 4,
    Środa = 8,
    Czwartek = 16,
    Piątek = 32,
    Sobota = 64,

    Żaden = 0,
    Wszystkie = Niedziela | Poniedziałek | Wtorek | Środa |
        Czwartek | Piątek | Sobota
}

Domyślnie ten typ wyliczeniowy w klasie miałby wartość 0 dlatego powinien on definiować słowo opisujące tę wartość. Jest to nawet błąd z tego co wyczytałem na MSDN w przypadku nie opisania tej wartości na start ten typ wyliczeniowy będzie posiadał nielegalną wartość.

Zgodnie z dokumentacją w każdym typie wyliczeniowym wartość 0 powinna mieć etykietę “None”. Widzę ,że niepotrzebnie użyłem w tym przykładzie polskich nazw. Miało to ułatwić zrozumienie przykładu, ale w tym wypadku pogorszyłem sprawę. Angielskie nazwy jednak są lepsze ,a polskie wprowadzają niepotrzebne zamieszanie.

Dla ułatwienie wszystkim życia twój typ wyliczeniowym powinien mieć wartość opisującą wszystkie wybrane opcje. Zgodnie z dokumentacją ta etykieta powinna nazywać się “All”.

Chyba że z twój typ wyliczeniowy opisuję przeciwstawne stany, które nie mogą być wybrane jako wszystkie.

Porównywanie flag

Jak sprawdzić czy dana zmienna zawiera dany dzień. Czy ten sposób zadziała?

DniTygodnia dniSerialu = DniTygodnia.Wtorek | 
DniTygodnia.Poniedziałek;

if (dniSerialu == DniTygodnia.Wtorek)
{  }

Nie

“DniSerialu” zawiera nie tylko wartość “Wtorek” więc jego wartość nie równa się tylko wtorkowi.

Sposobów istnieje kilka. Oto przykład zastosowania operatora bitowego “AND” &

if ((dniSerialu & DniTygodnia.Wtorek) == DniTygodnia.Wtorek)
{  }

Operator bitowy AND zwróci wartość liczbową "Wtorek" jeśli dniSerialu ją zawiera. Jeśli tak nie jest operacja ta zwróci zero.

W .NET 4.0 pojawiła się nowa metoda w klasie Enum ,która spełnia ten sam cel. Kod ten jest jednak bardziej czytelny, ponieważ nie wymaga od czytelnika znajomości operatora &.

if (dniSerialu.HasFlag(DniTygodnia.Wtorek))
{  }

To wszystko.Chociaż czuje, że ta tematyka nie kończy się tutaj.