Enumy jako flagi i wykorzystanie operatorów bitowych (XMASS Edition)

By | December 24, 2016

Niektórzy w tej chwili lepią pierogi, próbują nie wrócić do domu z pangą, latają za zapomnianym prezentem niczym Arnold za Turbo-Manem lub w ciepłym, przytulnym domku wieszają na choince bombki. Ja z kolei mam cały dzień dla siebie i jego część postanowiłem na napisanie krótkiego posta o tym jak możecie wykorzystać enumy jako flagi i obwiesić nimi swoje obiekty tak bardzo jak tylko chcecie, a żeby zachować choć pozory świątecznej atmosfery zrobię to z małym świątecznym akcentem (takim tycim). 

Zacznijmy od wyjaśnienia do czego możemy wykorzystać flagi. Pierwszą rzeczą jaką przychodzi mi do głowy jest oznaczenie uprawnień, do jakiegoś zasobu dla danego użytkownika lub grupy. Możemy to zrobić w prostej formie składającej się z trzech bitów (np. 011), gdzie każdy z nich oznacza kolejno: Usunięcie, Edycję i Odczyt, czyli wartości, które spokojnie moglibyśmy zamknąć w enumie. I przy wykorzystaniu atrybutu [Flags] oraz operatorów bitowych (BIT-AND i BIT-OR) jesteśmy w stanie to osiągnąć bardzo szybko i stosunkowo łatwo. Konstrukcja enuma jaka nas interesuje i obiektu, do którego będziemy przypinać nasze flagi.

public class SomeObject
{
    public EnumFlags Flags;

    public SomeObject(EnumFlags flags)
    {
        Flags = flags;
    }
}

[Flags]
public enum EnumFlags
{
    Grinch = 0, // 00000000
    Merry = 1,  // 00000001
    Xmass = 2,  // 00000010
    And = 4,    // 00000100
    Happy = 8,  // 00001000
    New = 16,   // 00010000
    Year = 32   // 00100000
}

W enumie warto zwrócić uwagę na wspomniany wcześniej atrybut Flags oraz na przypisane wartości, które muszą zostać określone i muszą być przypisywane dokładnie jako kolejne liczby będące potęgą liczby 2? Dlaczego? Spójrz na podane w komentarzu wartości binarne dla każdej z przypisanych wartości liczbowych, takie ich określenie sprawia, że możemy dowolnie włączać i wyłączać poszczególne flagi i nie będą się one nadpisywać. Enuma można zapisać jeszcze w nieco inny sposób, ale tym zajmiemy się nieco później. Teraz stwórzmy instancję obiektu i wyplujmy coś do konsoli.

var x = new SomeObject(EnumFlags.Merry | EnumFlags.Xmass);

Console.WriteLine(x.Flags.ToString());
Console.WriteLine((int)x.Flags);
Console.WriteLine(Convert.ToString((int)x.Flags, 2)); // Convert 'Flags' as int to binary

W konstruktorze przekazaliśmy dwie wartości wyliczenia (flagi) oddzielone symbolem ‘|’, który w C# jest operatorem BIT-OR. Co dzieje się po jego wykorzystaniu? Wartości na konkretnych bitach są ustawiane na 1 jeśli występują w którymkolwiek z elementów, które potraktujemy tym operatorem. Dzięki czemu wynik, który zobaczymy w konsoli będzie prezentował się następująco:

Merry, Xmass
3
11

Nasz obiekt zawiera w tej chwili dwie flagi. Co możemy z nimi zrobić? Przede wszystkim powinniśmy mieć możliwość sprawdzenia czy taką flagę posiadamy. Oczywiście możemy napisać wielkiego switcha, w którym będziemy sprawdzać wszystkie możliwe wartości, ale nie ma to zbyt wielkiego sensu, bo są na to dwa sposoby, które sprawują się o wiele lepiej.

Console.WriteLine(x.Flags.HasFlag(EnumFlags.Merry)); //True
Console.WriteLine(x.Flags.HasFlag(EnumFlags.And)); //False
Console.WriteLine((x.Flags & EnumFlags.Merry) != 0); //True
Console.WriteLine((x.Flags & EnumFlags.Year) != 0); //False

O ile metoda rozszerzająca jest naprawdę bardzo fajna i sprawdza się świetnie, to dobrze byłoby wiedzieć i rozumieć co dzieje się w momencie zastosowania operatora BIT-AND (‘&’) i dlaczego porównanie wygląda tak nietypowo. Otóż porównuje on obie strony wyrażenia (jak to operator) i ustawia na 1 tylko i wyłącznie te bity, które po obu stronach mają wartość 1. Jeśli więc wynikiem takiego porównania będzie ciąg zer, wynikiem będzie false. Aby to dobrze zobrazować wyprintujmy binarny wynik porównania do konsoli.

Console.WriteLine(Convert.ToString((int)(x.Flags & EnumFlags.Xmass), 2)); // 10
Console.WriteLine(Convert.ToString((int)(x.Flags & EnumFlags.Year), 2)); // 0

Co jeszcze możemy zrobić z flagami? Przy wykorzystaniu operatorów bitowych możemy je dodawać i usuwać.

x.Flags &= ~ EnumFlags.Xmass; //Remove EnumFlags.Xmass with BIT-NOT operator '~' and BIT-AND
x.Flags |= EnumFlags.And; //Add EnumFlags.And to Flags with BIT-OR

Usuwanie odbywa się przy użyciu omówionego wcześniej operatora BIT-AND i negacji, która odwraca wszystkie bity i w efekcie używamy bit and na dotychczasowej wartości flag i odwróconej wartości EnumFlags.Xmass (a więc 11111101), co w połączeniu z operatorem BIT-AND zmienia ten jeden bit na 0 (o ile miał wcześniej wartość 1), nie zmienia wartości innych jedynek (bo powtarzają się po obu stronach operatora) i pozostawia w spokoju zera w zmiennej x.Flags (bo są różne po obu stronach BIT-AND).

Dodawanie flagi przy użyciu operatora BIT-OR jest już o wiele prostsze i odbywa się na takiej samej zasadzie jak w konstruktorze.

Kolejnym operatorem, który możemy wykorzystać jest BIT-XOR czyli alternatywa. Pozwala nam w bardzo prosty sposób przełączać stan obiektu na przeciwny, a więc zmienić 1 na 0 i na odwrót.

x.Flags ^= EnumFlags.Xmass;

Jak wspomniałem wcześniej samego enuma można zapisać nieco inaczej. Nie będę omawiał tych sposobów dokładniej, bo i tak wyszedł z tego nieco przydługi post jak na lekki świąteczny wpisik, którym miał być, ale byłoby naprawdę dużym niedopowiedzeniem, gdybym je pominął. Pierwszym z nich jest zapis w hexach: 

[Flags]
    public enum EnumFlags
    {
        Grinch = 0x00, // 00000000
        Merry = 0x01,  // 00000001
        Xmass = 0x02,  // 00000010
        And = 0x04,    // 00000100
        Happy = 0x08,  // 00001000
        New = 0x10,    // 00010000   <== REMEMBER HEX10 == DEC16
        Year = 0x20    // 00100000   <== REMEMBER HEX20 == DEC32
    }

Drugim z kolei (i chyba najlepiej wyglądającym) jest zapis używający przesunięcia bitowego:

[Flags]
public enum EnumFlags
{
    Grinch = 0x00, // 00000000
    Merry = 1 << 0,// 00000001
    Xmass = 1 << 1,// 00000010
    And =   1 << 2,// 00000100
    Happy = 1 << 3,// 00001000
    New =   1 << 4,// 00010000
    Year =  1 << 5 // 00100000
}

Z kolei ostatni zapis podaje jako swego rodzaju ciekawostkę. W enumie możemy deklarować flagi, które są wartościami, które mają z góry ustawione kilka poprzednich flag używając operatora BIT-OR w ten sposób:

[Flags]
    public enum EnumFlags
    {
        Grinch = 0x00, // 00000000
        Merry = 1 << 0,// 00000001
        Xmass = 1 << 1,// 00000010
        And =   1 << 2,// 00000100
        Happy = 1 << 3,// 00001000
        New =   1 << 4,// 00010000
        Year =  1 << 5,// 00100000
        MerryXmass = EnumFlags.Merry | EnumFlags.Xmass //00000011
    }

Podsumujmy więc dzisiejszego posta i wykonajmy ten kawałek kodu:

var x = new SomeObject(EnumFlags.MerryXmass);
x.Flags |= EnumFlags.And | EnumFlags.Happy;
x.Flags ^= EnumFlags.New | EnumFlags.Year;

Console.WriteLine(x.Flags);
Console.WriteLine((int)x.Flags);
Console.WriteLine(Convert.ToString((int)x.Flags, 2));

By uzyskać w konsoli wynik: 

MerryXmass, And, Happy, New, Year
63
111111

Czego Wam i sobie życzę!