Jak porównać flagi w C #?

155

Poniżej mam wyliczenie flag.

[Flags]
public enum FlagTest
{
    None = 0x0,
    Flag1 = 0x1,
    Flag2 = 0x2,
    Flag3 = 0x4
}

Nie mogę sprawić, by stwierdzenie if było prawdziwe.

FlagTest testItem = FlagTest.Flag1 | FlagTest.Flag2;

if (testItem == FlagTest.Flag1)
{
    // Do something,
    // however This is never true.
}

Jak mogę to urzeczywistnić?

David Basarab
źródło
Popraw mnie, jeśli się mylę, czy 0 jest odpowiednie do użycia jako wartość flagi?
Roy Lee,
4
@Roylee: 0 jest akceptowalne i dobrym pomysłem jest posiadanie flagi „Brak” lub „Niezdefiniowana” w celu przetestowania braku ustawionych flag. W żadnym wypadku nie jest to wymagane, ale to dobra praktyka. Na ważną rzecz, o której należy pamiętać, zwraca uwagę Leonid w swojej odpowiedzi.
Andy,
5
@Roylee Firma Microsoft faktycznie zaleca podanie Noneflagi o wartości zerowej. Zobacz msdn.microsoft.com/en-us/library/vstudio/…
ThatMatthew
Wiele osób twierdzi również, że porównanie bitów jest zbyt trudne do odczytania, dlatego należy go unikać na rzecz zbioru flag, w którym można po prostu zrobić zbiór.
Zawiera
Byłaś całkiem blisko, z wyjątkiem trzeba odwrócić Ci logikę, trzeba bitowego &operatora dla porównania, |jest jak dodatek: 1|2=3, 5|2=7, 3&2=2, 7&2=2, 8&2=0. 0ocenia do false, wszystko inne do true.
Damian Vogel

Odpowiedzi:

321

W .NET 4 jest nowa metoda Enum.HasFlag . Dzięki temu możesz napisać:

if ( testItem.HasFlag( FlagTest.Flag1 ) )
{
    // Do Stuff
}

co jest dużo bardziej czytelne, IMO.

Źródło .NET wskazuje, że działa to tak samo, jak zaakceptowana odpowiedź:

public Boolean HasFlag(Enum flag) {
    if (!this.GetType().IsEquivalentTo(flag.GetType())) {
        throw new ArgumentException(
            Environment.GetResourceString(
                "Argument_EnumTypeDoesNotMatch", 
                flag.GetType(), 
                this.GetType()));
    }

    ulong uFlag = ToUInt64(flag.GetValue()); 
    ulong uThis = ToUInt64(GetValue());
    // test predicate
    return ((uThis & uFlag) == uFlag); 
}
Phil Devaney
źródło
23
Ach, w końcu coś po wyjęciu z pudełka. To świetnie, długo czekałem na tę stosunkowo prostą funkcję. Cieszę się, że zdecydowali się to wsunąć.
Rob van Groenewoud
9
Zwróć jednak uwagę na odpowiedź poniżej przedstawiającą problemy z wydajnością tej metody - może to być problem dla niektórych osób. Na szczęście nie dla mnie.
Andy Mortimer
2
Uwzględnienie wydajności w przypadku tej metody to boksowanie, ponieważ przyjmuje argumenty jako wystąpienie Enumklasy.
Adam Houldsworth,
1
Aby uzyskać informacje na temat problemu z wydajnością, spójrz na tę odpowiedź: stackoverflow.com/q/7368652/200443
Maxence
180
if ((testItem & FlagTest.Flag1) == FlagTest.Flag1)
{
     // Do something
}

(testItem & FlagTest.Flag1) jest operacją bitową AND.

FlagTest.Flag1jest równoważne 001z wyliczeniem OP. Teraz powiedzmy, że testItemma Flag1 i Flag2 (więc jest bitowa 101):

  001
 &101
 ----
  001 == FlagTest.Flag1
Scott Nichols
źródło
2
Jaka jest tu dokładnie logika? Dlaczego predykat musi być napisany w ten sposób?
Ian R. O'Brien,
4
@ IanR.O'Brien Flag1 | Flaga 2 jest tłumaczona na 001 lub 010, co jest tym samym co 011, teraz jeśli zrobisz równość 011 == Flaga1 lub przetłumaczona 011 == 001, to zawsze zwraca fałsz. Teraz, jeśli wykonasz bitowe AND z Flag1, to przekłada się to na 011 AND 001, co zwraca 001, teraz wykonanie równości zwraca prawdę, ponieważ 001 == 001
pqsk
Jest to najlepsze rozwiązanie, ponieważ HasFlags wymaga znacznie więcej zasobów. A zresztą wszystko co robi HasFlags, tutaj robi kompilator
Sebastian Xawery Wiśniowiecki
78

Dla tych, którzy mają problem z wizualizacją tego, co dzieje się z przyjętym rozwiązaniem (czyli tym),

if ((testItem & FlagTest.Flag1) == FlagTest.Flag1)
{
    // Do stuff.
}

testItem (zgodnie z pytaniem) definiuje się jako:

testItem 
 = flag1 | flag2  
 = 001 | 010  
 = 011

Następnie w instrukcji if lewa strona porównania to:

(testItem & flag1) 
 = (011 & 001) 
 = 001

A pełna instrukcja if (która flag1daje wartość true, jeśli jest ustawiona w testItem),

(testItem & flag1) == flag1
 = (001) == 001
 = true
Sekhat
źródło
25

@ phil-devaney

Zauważ, że z wyjątkiem najprostszych przypadków, Enum.HasFlag niesie znaczny spadek wydajności w porównaniu z ręcznym pisaniem kodu. Rozważ następujący kod:

[Flags]
public enum TestFlags
{
    One = 1,
    Two = 2,
    Three = 4,
    Four = 8,
    Five = 16,
    Six = 32,
    Seven = 64,
    Eight = 128,
    Nine = 256,
    Ten = 512
}


class Program
{
    static void Main(string[] args)
    {
        TestFlags f = TestFlags.Five; /* or any other enum */
        bool result = false;

        Stopwatch s = Stopwatch.StartNew();
        for (int i = 0; i < 10000000; i++)
        {
            result |= f.HasFlag(TestFlags.Three);
        }
        s.Stop();
        Console.WriteLine(s.ElapsedMilliseconds); // *4793 ms*

        s.Restart();
        for (int i = 0; i < 10000000; i++)
        {
            result |= (f & TestFlags.Three) != 0;
        }
        s.Stop();
        Console.WriteLine(s.ElapsedMilliseconds); // *27 ms*        

        Console.ReadLine();
    }
}

Ponad 10 milionów iteracji, metoda rozszerzenia HasFlags zajmuje aż 4793 ms, w porównaniu z 27 ms dla standardowej implementacji bitowej.

Chuck Dee
źródło
5
W rzeczy samej. Jeśli spojrzysz na implementację HasFlag, zobaczysz, że wykonuje ona „GetType ()” na obu operandach, co jest dość powolne. Następnie wykonuje "Enum.ToUInt64 (value.GetValue ());" na obu operandach przed sprawdzeniem bitowym.
user276648
1
Przeprowadziłem Twój test kilka razy i uzyskałem ~ 500 ms dla HasFlags i ~ 32 ms dla bitowego. Chociaż wciąż o rząd wielkości szybciej z bitową, HasFlags był o rząd wielkości niższy od twojego testu. (Przeprowadziłem test na 2,5 GHz Core i3 i .NET 4.5)
MarioVW,
1
@MarioVW Działając kilka razy na .NET 4, i7-3770 daje ~ 2400 ms w porównaniu z ~ 20 ms w trybie AnyCPU (64-bitowym) i ~ 3000 ms w porównaniu z ~ 20 ms w trybie 32-bitowym. .NET 4.5 mógł go nieco zoptymalizować. Zwróć także uwagę na różnicę w wydajności między wersjami 64-bitowymi i 32-bitowymi, co może wynikać z szybszej 64-bitowej arytmetyki (patrz pierwszy komentarz).
Bob,
1
(«flag var» & «flag value») != 0nie działa dla mnie. Warunek zawsze kończy się niepowodzeniem, a mój kompilator (Unity3D's Mono 2.6.5) zgłasza „ostrzeżenie CS0162: wykryto nieosiągalny kod”, gdy jest używany w pliku if (…).
Slipp D. Thompson
1
@ wraith808: Zdałem sobie sprawę, że błąd polegał na popełnianiu mojego testu, że masz poprawny w swoim - potęga 2 … = 1, … = 2, … = 4 na wartościach wyliczenia jest krytycznie ważna podczas używania [Flags]. 1Zakładałem , że rozpocznie on wpisy o godzinie i przejdzie automatycznie o Po2. Zachowanie wygląda spójnie w MS .NET i przestarzałym Mono. Przepraszam, że się tym zajmę.
Slipp D. Thompson
21

Ustawiłem metodę rozszerzenia, aby to zrobić: pytanie powiązane .

Gruntownie:

public static bool IsSet( this Enum input, Enum matchTo )
{
    return ( Convert.ToUInt32( input ) & Convert.ToUInt32( matchTo ) ) != 0;
}

Następnie możesz:

FlagTests testItem = FlagTests.Flag1 | FlagTests.Flag2;

if( testItem.IsSet ( FlagTests.Flag1 ) )
    //Flag1 is set

Nawiasem mówiąc, konwencja, której używam dla wyliczeń, jest pojedyncza dla standardu, liczba mnoga dla flag. W ten sposób na podstawie nazwy wyliczenia wiesz, czy może on zawierać wiele wartości.

Keith
źródło
To powinien być komentarz, ale ponieważ jestem nowym użytkownikiem, wygląda na to, że nie mogę jeszcze dodawać komentarzy ... public static bool IsSet (this Enum input, Enum matchTo) {return (Convert.ToUInt32 (input) & Convert .ToUInt32 (matchTo))! = 0; } Czy istnieje sposób na uzyskanie zgodności z jakimkolwiek rodzajem wyliczenia (ponieważ tutaj nie zadziała, jeśli wyliczenie jest typu UInt64 lub może mieć wartości ujemne)?
user276648,
Jest to dość zbędne w przypadku Enum.HasFlag (Enum) (dostępne w .net 4.0)
PPC
1
@PPC Nie powiedziałbym, że jest zbędny - wiele osób pisze na starszych wersjach frameworka. Masz rację, użytkownicy .Net 4 powinni HasFlagzamiast tego używać rozszerzenia.
Keith
4
@Keith: Istnieje również zauważalna różnica: ((FlagTest) 0x1) .HasFlag (0x0) zwróci wartość true, co może być pożądanym zachowaniem lub nie
PPC
19

Jeszcze jedna rada ... Nigdy nie wykonuj standardowego testu binarnego z flagą, której wartość wynosi "0". Twoje sprawdzenie tej flagi zawsze będzie prawdziwe.

[Flags]
public enum LevelOfDetail
{
    [EnumMember(Value = "FullInfo")]
    FullInfo=0,
    [EnumMember(Value = "BusinessData")]
    BusinessData=1
}

Jeśli binarnie sprawdzisz parametr wejściowy względem FullInfo - otrzymasz:

detailLevel = LevelOfDetail.BusinessData;
bool bPRez = (detailLevel & LevelOfDetail.FullInfo) == LevelOfDetail.FullInfo;

bPRez zawsze będzie prawdziwe jako WSZYSTKO i 0 zawsze == 0.


Zamiast tego powinieneś po prostu sprawdzić, czy wartość wejścia wynosi 0:

bool bPRez = (detailLevel == LevelOfDetail.FullInfo);
Leonid
źródło
Właśnie naprawiłem taki błąd z flagą 0. Myślę, że jest to błąd projektowy w .NET Framework (3.5), ponieważ przed przetestowaniem musisz wiedzieć, która z wartości flag wynosi 0.
thersch
7
if((testItem & FlagTest.Flag1) == FlagTest.Flag1) 
{
...
}
Damian
źródło
5

W przypadku operacji bitowych należy używać operatorów bitowych.

To powinno załatwić sprawę:

if ((testItem & FlagTest.Flag1) == FlagTest.Flag1)
{
    // Do something,
    // however This is never true.
}

Edycja: Naprawiono mój czek if - wróciłem do moich sposobów C / C ++ (dzięki Ryanowi Farleyowi za wskazanie tego)

17 z 26
źródło
5

Odnośnie edycji. Nie możesz tego zrobić. Sugeruję zawinięcie tego, co chcesz, do innej klasy (lub metody rozszerzenia), aby zbliżyć się do potrzebnej składni.

to znaczy

public class FlagTestCompare
{
    public static bool Compare(this FlagTest myFlag, FlagTest condition)
    {
         return ((myFlag & condition) == condition);
    }
}
Martin Clarke
źródło
4

Spróbuj tego:


if ((testItem & FlagTest.Flag1) == FlagTest.Flag1)
{
    // do something
}
Zasadniczo twój kod pyta, czy ustawienie obu flag jest tym samym, co ustawienie jednej flagi, co jest oczywiście fałszywe. Powyższy kod pozostawi ustawiony tylko bit Flag1, jeśli w ogóle jest ustawiony, a następnie porównuje ten wynik z Flag1.

OwenP
źródło
1

nawet bez [Flags] możesz użyć czegoś takiego

if((testItem & (FlagTest.Flag1 | FlagTest.Flag2 ))!=0){
//..
}

lub jeśli masz wyliczenie o wartości zero

if((testItem & (FlagTest.Flag1 | FlagTest.Flag2 ))!=FlagTest.None){
//..
}
Waleed AK
źródło