Jak iterować po wartościach wyliczenia mającego flagi?


134

Jeśli mam zmienną zawierającą wyliczenie flag, czy mogę w jakiś sposób iterować po wartościach jednobitowych w tej określonej zmiennej? Czy muszę użyć Enum.GetValues ​​do iteracji po całym wyliczeniu i sprawdzić, które z nich są ustawione?


Jeśli masz kontrolę nad swoim API, unikaj używania flag bitowych. Rzadko są użyteczną optymalizacją. Używanie struktury z kilkoma publicznymi polami typu „bool” jest semantycznie równoważne, ale kod jest znacznie prostszy. A jeśli zajdzie taka potrzeba, możesz zmienić pola na właściwości, które wewnętrznie manipulują polami bitowymi, hermetyzując optymalizację.
Jay Bazuzi

2
Rozumiem, co mówisz, iw wielu przypadkach miałoby to sens, ale w tym przypadku miałbym ten sam problem co w przypadku sugestii If ... Zamiast tego musiałbym napisać instrukcje If dla kilkunastu różnych wartości logicznych używania prostej pętli foreach na tablicy. (A ponieważ jest to część publicznej biblioteki DLL, nie mogę robić tajemniczych rzeczy, takich jak po prostu posiadanie tablicy bools lub tego, co masz.)

12
„Twój kod jest znacznie prostszy” - zupełnie odwrotnie jest prawdą, jeśli robisz cokolwiek innego niż tylko testowanie pojedynczych bitów ... pętle i operacje na zbiorach stają się praktycznie niemożliwe, ponieważ pola i właściwości nie są jednostkami pierwszej klasy.
Jim Balter,

3
@nawfal "trochę" Widzę, co tam zrobiłeś
stannius,

Odpowiedzi:


186
static IEnumerable<Enum> GetFlags(Enum input)
{
    foreach (Enum value in Enum.GetValues(input.GetType()))
        if (input.HasFlag(value))
            yield return value;
}

7
Należy pamiętać, że HasFlagjest dostępny od .NET 4 i nowszych.
Andreas Grech

3
This is great! But You can make it even simpler and easy to use. Just stick this as an extension method: Enum.GetValues(input.GetType()).Cast<Enum>().Where(input.HasFlag); Then just: myEnum.GetFLags() :)
joshcomley

3
Great one-liner, josh, but it still suffers the problem of picking up multi-flag values (Boo) instead of only single-flag values (Bar, Baz), as in Jeff's answer, above.

10
Fajnie - uważaj jednak na None's - np. Przedmioty. Żadna z odpowiedzi Jeffa nie będzie zawsze uwzględniona
Ilan

1
Podpis metody powinien brzmiećstatic IEnumerable<Enum> GetFlags(this Enum input)
Erwin Rooijakkers

51

Oto rozwiązanie tego problemu firmy Linq.

public static IEnumerable<Enum> GetFlags(this Enum e)
{
      return Enum.GetValues(e.GetType()).Cast<Enum>().Where(e.HasFlag);
}

7
Dlaczego to nie jest na górze? :) Użyj, .Where(v => !Equals((int)(object)v, 0) && e.HasFlag(v));jeśli masz wartość zero do przedstawieniaNone
georgiosd

Super czysty. Najlepsze moim zdaniem rozwiązanie.
Ryan Fiorini

@georgiosd Myślę, że wydajność nie jest tak dobra. (Ale powinno wystarczyć do większości zadań)
AntiHeadshot

43

O ile wiem, nie ma żadnych wbudowanych metod pobierania każdego komponentu. Ale oto jeden sposób, w jaki możesz je zdobyć:

[Flags]
enum Items
{
    None = 0x0,
    Foo  = 0x1,
    Bar  = 0x2,
    Baz  = 0x4,
    Boo  = 0x6,
}

var value = Items.Foo | Items.Bar;
var values = value.ToString()
                  .Split(new[] { ", " }, StringSplitOptions.None)
                  .Select(v => (Items)Enum.Parse(typeof(Items), v));

// This method will always end up with the most applicable values
value = Items.Bar | Items.Baz;
values = value.ToString()
              .Split(new[] { ", " }, StringSplitOptions.None)
              .Select(v => (Items)Enum.Parse(typeof(Items), v)); // Boo

Dostosowałem to, co Enumrobi wewnętrznie, aby wygenerować ciąg zamiast zwracać flagi. Możesz spojrzeć na kod w reflektorze i powinien być mniej więcej równoważny. Działa dobrze w ogólnych przypadkach użycia, w których istnieją wartości zawierające wiele bitów.

static class EnumExtensions
{
    public static IEnumerable<Enum> GetFlags(this Enum value)
    {
        return GetFlags(value, Enum.GetValues(value.GetType()).Cast<Enum>().ToArray());
    }

    public static IEnumerable<Enum> GetIndividualFlags(this Enum value)
    {
        return GetFlags(value, GetFlagValues(value.GetType()).ToArray());
    }

    private static IEnumerable<Enum> GetFlags(Enum value, Enum[] values)
    {
        ulong bits = Convert.ToUInt64(value);
        List<Enum> results = new List<Enum>();
        for (int i = values.Length - 1; i >= 0; i--)
        {
            ulong mask = Convert.ToUInt64(values[i]);
            if (i == 0 && mask == 0L)
                break;
            if ((bits & mask) == mask)
            {
                results.Add(values[i]);
                bits -= mask;
            }
        }
        if (bits != 0L)
            return Enumerable.Empty<Enum>();
        if (Convert.ToUInt64(value) != 0L)
            return results.Reverse<Enum>();
        if (bits == Convert.ToUInt64(value) && values.Length > 0 && Convert.ToUInt64(values[0]) == 0L)
            return values.Take(1);
        return Enumerable.Empty<Enum>();
    }

    private static IEnumerable<Enum> GetFlagValues(Type enumType)
    {
        ulong flag = 0x1;
        foreach (var value in Enum.GetValues(enumType).Cast<Enum>())
        {
            ulong bits = Convert.ToUInt64(value);
            if (bits == 0L)
                //yield return value;
                continue; // skip the zero value
            while (flag < bits) flag <<= 1;
            if (flag == bits)
                yield return value;
        }
    }
}

Metoda rozszerzenia GetIndividualFlags()pobiera wszystkie indywidualne flagi dla typu. Tak więc wartości zawierające wiele bitów są pomijane.

var value = Items.Bar | Items.Baz;
value.GetFlags();           // Boo
value.GetIndividualFlags(); // Bar, Baz

Rozważałem wykonanie podziału na ciąg, ale to prawdopodobnie dużo więcej narzutów niż zwykłe iterowanie wartości bitowych całego wyliczenia.

Niestety, robiąc to, musiałbyś przetestować zbędne wartości (jeśli nie chciałbyś ich). Zobacz mój drugi przykład, to ustąpi Bar, Baza Boozamiast po prostu Boo.
Jeff Mercado

Ciekawe, że jesteś w stanie wyciągnąć z tego Boo, chociaż ta część jest niepotrzebna (a właściwie to naprawdę zły pomysł :)) do tego, co robię.

Wygląda to interesująco, ale jeśli się nie mylę, zwróci to Boo dla powyższego wyliczenia, gdzie chciałbym wyliczyć tylko wersje, które nie są kombinacjami innych wartości (tj. Te, które są potęgami dwóch) . Czy można to zrobić łatwo? Próbowałem i nie mogę wymyślić prostego sposobu, aby to zidentyfikować bez uciekania się do matematyki FP.

@Robin: Masz rację, oryginał zwróciłby się Boo(wartość zwrócona za pomocą ToString()). Poprawiłem go, aby zezwalał tylko na pojedyncze flagi. Więc w moim przykładzie możesz uzyskać Bari Bazzamiast Boo.
Jeff Mercado,

28

Wracając do tego kilka lat później, z nieco większym doświadczeniem, moją ostateczną odpowiedzią tylko dla wartości jednobitowych, przechodząc od najniższego do najwyższego bitu, jest niewielki wariant wewnętrznej rutyny Jeffa Mercado:

public static IEnumerable<Enum> GetUniqueFlags(this Enum flags)
{
    ulong flag = 1;
    foreach (var value in Enum.GetValues(flags.GetType()).Cast<Enum>())
    {
        ulong bits = Convert.ToUInt64(value);
        while (flag < bits)
        {
            flag <<= 1;
        }

        if (flag == bits && flags.HasFlag(value))
        {
            yield return value;
        }
    }
}

Wydaje się, że działa i pomimo moich zastrzeżeń sprzed kilku lat używam tutaj HasFlag, ponieważ jest on znacznie bardziej czytelny niż przy użyciu porównań bitowych, a różnica prędkości jest nieistotna dla wszystkiego, co będę robił. (Jest całkiem możliwe, że i tak poprawili prędkość HasFlags od tego czasu, z tego, co wiem ... nie testowałem.)


Tylko flaga uwaga jest inicjowany do int powinno być Ulong takich jak bitów, powinny być inicjowane jako 1UL
forcewill

Dzięki, naprawię to! (Właśnie poszedłem i sprawdziłem mój rzeczywisty kod i już naprawiłem go na odwrót, deklarując go jako ulong.)

2
Jest to jedyne rozwiązanie, które znalazłem, które wydaje się również nie cierpieć z powodu faktu, że jeśli masz flagę o wartości zero, która powinna reprezentować „None”, inne odpowiedzi metody GetFlag () zwrócą YourEnum.None jako jedną z flag nawet jeśli tak naprawdę nie jest w wyliczeniu, na którym uruchamiasz metodę! Otrzymywałem dziwne zduplikowane wpisy dziennika, ponieważ metody działały więcej razy, niż się spodziewałem, gdy miały tylko jedną niezerową flagę wyliczenia. Dziękujemy za poświęcenie czasu na aktualizację i dodanie tego wspaniałego rozwiązania!
BrianH

yield return bits;?
Jaider

1
Chciałem mieć zmienną wyliczeniową „All”, do której przypisałem ulong.MaxValue, więc wszystkie bity są ustawione na „1”. Ale twój kod trafia w nieskończoną pętlę, ponieważ flag <bits nigdy nie daje wartości true (flaga zapętla się na wartość ujemną, a następnie utknie na 0).
Haighstrom

17

Wyjście z metody @ Grega, ale dodanie nowej funkcji z C # 7.3, Enumograniczenie:

public static IEnumerable<T> GetUniqueFlags<T>(this Enum flags)
    where T : Enum    // New constraint for C# 7.3
{
    foreach (Enum value in Enum.GetValues(flags.GetType()))
        if (flags.HasFlag(value))
            yield return (T)value;
}

Nowe ograniczenie pozwala na to, aby była to metoda rozszerzająca, bez konieczności rzutowania (int)(object)e, i mogę używać tej HasFlagmetody i rzutować bezpośrednio Tz value.

C # 7,3 dodał również ograniczenia dla delagates i unmanaged.


6
Prawdopodobnie chciałeś, aby flagsparametr był również typu ogólnego T, w przeciwnym razie musiałbyś jawnie określać typ wyliczenia za każdym razem, gdy go wywołujesz.
Ray

11

+1 za odpowiedź udzieloną przez @ RobinHood70. Odkryłem, że ogólna wersja metody jest dla mnie wygodna.

public static IEnumerable<T> GetUniqueFlags<T>(this Enum flags)
{
    if (!typeof(T).IsEnum)
        throw new ArgumentException("The generic type parameter must be an Enum.");

    if (flags.GetType() != typeof(T))
        throw new ArgumentException("The generic type parameter does not match the target type.");

    ulong flag = 1;
    foreach (var value in Enum.GetValues(flags.GetType()).Cast<T>())
    {
        ulong bits = Convert.ToUInt64(value);
        while (flag < bits)
        {
            flag <<= 1;
        }

        if (flag == bits && flags.HasFlag(value as Enum))
        {
            yield return value;
        }
    }
}

EDYCJA I +1 dla @AustinWBryan za wprowadzenie języka C # 7.3 do przestrzeni rozwiązań.

public static IEnumerable<T> GetUniqueFlags<T>(this T flags) where T : Enum
{
    ulong flag = 1;
    foreach (var value in Enum.GetValues(flags.GetType()).Cast<T>())
    {
        ulong bits = Convert.ToUInt64(value);
        while (flag < bits)
        {
            flag <<= 1;
        }

        if (flag == bits && flags.HasFlag(value as Enum))
        {
            yield return value;
        }
    }
}


Twój edytowany kod był dokładnie tym, czego używałem do dzisiaj. Po raz kolejny poprawiłem kod, używając różnych sztuczek, których nauczyłem się w ciągu ostatnich kilku lat. Zobacz moją najnowszą odpowiedź: stackoverflow.com/a/63760259/14160657
RobinHood70

3

Nie musisz iterować wszystkich wartości. po prostu sprawdź swoje określone flagi w ten sposób:

if((myVar & FlagsEnum.Flag1) == FlagsEnum.Flag1) 
{
   //do something...
}

lub (jak powiedział pstrjds w komentarzach) możesz sprawdzić, czy go używasz, jak:

if(myVar.HasFlag(FlagsEnum.Flag1))
{
   //do something...
}

5
Jeśli używasz .Net 4.0, istnieje metoda rozszerzenia HasFlag, której możesz użyć do zrobienia tego samego: myVar.HasFlag (FlagsEnum.Flag1)
pstrjds

1
Jeśli programista nie może zrozumieć operacji bitowych ORAZ, powinien to spakować i znaleźć nową karierę.
Ed S.

2
@Ed: prawda, ale HasFlag jest lepszy, gdy ponownie czytasz kod ... (może po kilku miesiącach lub latach)
Dr TJ

4
@Ed Swangren: Tak naprawdę chodzi o uczynienie kodu bardziej czytelnym i mniej szczegółowym, niekoniecznie dlatego, że używanie operacji bitowych jest „trudne”.
Jeff Mercado

2
HasFlag jest niesamowicie powolny. Wypróbuj dużą pętlę za pomocą HasFlag vs. maskowanie bitów, a zauważysz ogromną różnicę.

3

Zmieniłem swoje podejście, zamiast wpisywać parametr wejściowy metody jako enumtyp, wpisałem go jako tablicę enumtypu type ( MyEnum[] myEnums), w ten sposób po prostu iteruję przez tablicę z instrukcją switch wewnątrz pętli.


2

Nie był zadowolony z odpowiedzi powyżej, chociaż były one początkiem.

Po zebraniu razem kilku różnych źródeł tutaj:
Poprzedni plakat w tym wątku SO QnA
Code Project Enum Flags Check Post
Great Enum <T> Utility

Stworzyłem to, więc daj mi znać, co myślisz.
Parametry::
bool checkZeromówi mu, aby zezwolić 0jako wartość flagi. Domyślnie input = 0zwraca puste.
bool checkFlags: każe mu sprawdzić, czy Enumjest ozdobiony [Flags]atrybutem.
PS. Nie mam teraz czasu, aby wymyślić checkCombinators = falsealgorytm, który zmusi go do zignorowania wartości wyliczenia, które są kombinacjami bitów.

    public static IEnumerable<TEnum> GetFlags<TEnum>(this TEnum input, bool checkZero = false, bool checkFlags = true, bool checkCombinators = true)
    {
        Type enumType = typeof(TEnum);
        if (!enumType.IsEnum)
            yield break;

        ulong setBits = Convert.ToUInt64(input);
        // if no flags are set, return empty
        if (!checkZero && (0 == setBits))
            yield break;

        // if it's not a flag enum, return empty
        if (checkFlags && !input.GetType().IsDefined(typeof(FlagsAttribute), false))
            yield break;

        if (checkCombinators)
        {
            // check each enum value mask if it is in input bits
            foreach (TEnum value in Enum<TEnum>.GetValues())
            {
                ulong valMask = Convert.ToUInt64(value);

                if ((setBits & valMask) == valMask)
                    yield return value;
            }
        }
        else
        {
            // check each enum value mask if it is in input bits
            foreach (TEnum value in Enum <TEnum>.GetValues())
            {
                ulong valMask = Convert.ToUInt64(value);

                if ((setBits & valMask) == valMask)
                    yield return value;
            }
        }

    }

To wykorzystuje pomocniczą klasę Enum <T> znalezioną tutaj , którą zaktualizowałem do użycia yield returndla GetValues:

public static class Enum<TEnum>
{
    public static TEnum Parse(string value)
    {
        return (TEnum)Enum.Parse(typeof(TEnum), value);
    }

    public static IEnumerable<TEnum> GetValues()   
    {
        foreach (object value in Enum.GetValues(typeof(TEnum)))
            yield return ((TEnum)value);
    }
}  

Na koniec, oto przykład jego użycia:

    private List<CountType> GetCountTypes(CountType countTypes)
    {
        List<CountType> cts = new List<CountType>();

        foreach (var ct in countTypes.GetFlags())
            cts.Add(ct);

        return cts;
    }

Przepraszamy, nie miałem czasu na obejrzenie tego projektu od kilku dni. Wrócę do Ciebie, gdy przyjrzę się dokładniej Twojemu kodowi.

4
Tylko ostrzeżenie, że w tym kodzie jest błąd. Kod w obu gałęziach instrukcji if (checkCombinators) jest identyczny. Być może nie jest to również błąd, ale nieoczekiwany, ponieważ jeśli masz zadeklarowaną wartość wyliczenia jako 0, zawsze zostanie ona zwrócona w kolekcji. Wydaje się, że powinno być zwracane tylko wtedy, gdy checkZero ma wartość true i nie ma innych ustawionych flag.
dhochee

@dhochee. Zgadzam się. Albo kod jest całkiem niezły, ale argumenty są mylące.
AFract

2

Opierając się na powyższej odpowiedzi Grega, rozwiązuje to również przypadek, w którym masz wartość 0 w swoim wyliczeniu, na przykład None = 0. W takim przypadku nie powinno iterować po tej wartości.

public static IEnumerable<Enum> ToEnumerable(this Enum input)
{
    foreach (Enum value in Enum.GetValues(input.GetType()))
        if (input.HasFlag(value) && Convert.ToInt64(value) != 0)
            yield return value;
}

Czy ktoś wiedziałby, jak to jeszcze bardziej ulepszyć, aby poradził sobie z przypadkiem, w którym wszystkie flagi w wyliczeniu są ustawione w super inteligentny sposób, który mógłby obsłużyć wszystkie podstawowe typy wyliczeń i przypadek All = ~ 0 i All = EnumValue1 | EnumValue2 | EnumValue3 | ...


1

Możesz użyć Iteratora z Enum. Począwszy od kodu MSDN:

public class DaysOfTheWeek : System.Collections.IEnumerable
{
    int[] dayflag = { 1, 2, 4, 8, 16, 32, 64 };
    string[] days = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
    public string value { get; set; }

    public System.Collections.IEnumerator GetEnumerator()
    {
        for (int i = 0; i < days.Length; i++)
        {
            if value >> i & 1 == dayflag[i] {
                yield return days[i];
            }
        }
    }
}

Nie jest testowany, więc jeśli popełniłem błąd, zadzwoń do mnie. (oczywiście nie jest to ponowne wejście). Musiałbyś wcześniej przypisać wartość lub podzielić ją na inną funkcję, która używa enum.dayflag i enum.days. Możesz być w stanie pójść gdzieś z konspektem.


1

Metoda rozszerzenia przy użyciu nowego ograniczenia wyliczenia i typów ogólnych, aby zapobiec rzutowaniu:

public static class EnumExtensions
{
    public static T[] GetFlags<T>(this T flagsEnumValue) where T : Enum
    {
        return Enum
            .GetValues(typeof(T))
            .Cast<T>()
            .Where(e => flagsEnumValue.HasFlag(e))
            .ToArray();
    }
}

0

Może to być również następujący kod:

public static string GetEnumString(MyEnum inEnumValue)
{
    StringBuilder sb = new StringBuilder();

    foreach (MyEnum e in Enum.GetValues(typeof(MyEnum )))
    {
        if ((e & inEnumValue) != 0)
        {
           sb.Append(e.ToString());
           sb.Append(", ");
        }
    }

   return sb.ToString().Trim().TrimEnd(',');
}

Wchodzi do środka tylko wtedy, gdy wartość wyliczenia jest zawarta w wartości


0

Wszystkie odpowiedzi działają dobrze z prostymi flagami, prawdopodobnie będziesz mieć problemy z połączeniem flag.

[Flags]
enum Food
{
  None=0
  Bread=1,
  Pasta=2,
  Apples=4,
  Banana=8,
  WithGluten=Bread|Pasta,
  Fruits = Apples | Banana,
}

prawdopodobnie trzeba będzie dodać sprawdzenie, aby sprawdzić, czy wartość wyliczenia jest kombinacją. Prawdopodobnie potrzebowałbyś czegoś takiego, jak opublikował tutaj Henk van Boeijen, aby spełnić swoje wymagania (musisz trochę przewinąć)


W kilku odpowiedziach rozwiązano ten problem na różne sposoby. Moim pierwotnym zamiarem było uzyskanie tylko wartości jednobitowych, nawet jeśli obecne były wartości Brak / wielobitowe, ale zapomniałem określić to w OP.
RobinHood70

0

Kontynuując moje wysiłki, aby skrócić kod, jest to moja najnowsza wersja procedury. (Jestem OP ... długa historia). Jak wspomniano wcześniej, ignoruje to wartości None i wielobitowe.

Zauważ, że używa to ograniczenia Enum i wzorca var, więc będzie wymagało co najmniej C # 7,3.

public static IEnumerable<T> GetUniqueFlags<T>(this T value)
    where T : Enum
{
    var valueLong = Convert.ToUInt64(value, CultureInfo.InvariantCulture);
    foreach (var enumValue in value.GetType().GetEnumValues())
    {
        if (
            enumValue is T flag // cast enumValue to T
            && Convert.ToUInt64(flag, CultureInfo.InvariantCulture) is var bitValue // convert flag to ulong
            && (bitValue & (bitValue - 1)) == 0 // is this a single-bit value?
            && (valueLong & bitValue) != 0 // is the bit set?
           )
        {
            yield return flag;
        }
    }
}

Aby uzyskać znaczny wzrost wydajności, można rzutować wynik GetEnumValues()na T[]i wyeliminować enumValue is T flagwarunek, ale polega to na tym, że GetEnumValues ​​zawsze zwraca tablicę typu T, co nie jest wymagane w umowie.
RobinHood70

-1

Możesz to zrobić bezpośrednio, konwertując na int, ale stracisz sprawdzanie typów. Myślę, że najlepszym sposobem jest użycie czegoś podobnego do mojej propozycji. Zachowuje właściwy typ przez cały czas. Nie jest wymagana konwersja. Nie jest doskonały ze względu na boks, który doda trochę uderzenia w wydajność.

Nie doskonały (boks), ale spełnia swoje zadanie bez ostrzeżenia ...

/// <summary>
/// Return an enumerators of input flag(s)
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static IEnumerable<T> GetFlags<T>(this T input)
{
    foreach (Enum value in Enum.GetValues(input.GetType()))
    {
        if ((int) (object) value != 0) // Just in case somebody has defined an enum with 0.
        {
            if (((Enum) (object) input).HasFlag(value))
                yield return (T) (object) value;
        }
    }
}

Stosowanie:

    FileAttributes att = FileAttributes.Normal | FileAttributes.Compressed;
    foreach (FileAttributes fa in att.GetFlags())
    {
        ...
    }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.