Як перебрати значення Enum із прапорами?


131

Якщо у мене є змінна, що містить перелік прапорів, чи можу я якось повторити значення бітів у цій конкретній змінній? Або мені потрібно використовувати Enum.GetValues, щоб перебрати весь перелік і перевірити, які з них встановлені?


Якщо ви маєте контроль над своїм API, уникайте використання бітових прапорів. Вони рідко є корисною оптимізацією. Використання структури з кількома загальнодоступними полями 'bool' семантично еквівалентно, але ваш код значно простіший. І якщо вам потрібно пізніше, ви можете змінити поля на властивості, що маніпулюють бітовими полями внутрішньо, капсулюючи оптимізацію.
Джей Базузі

2
Я бачу, що ви говорите, і в багатьох випадках це мало б сенс, але в цьому випадку я мав би таку саму проблему, як і пропозиція If ... Я б повинен писати, якщо натомість заявки для десятка різних випусків використання простого циклу foreach над масивом. (Оскільки це є частиною публічної DLL, я не можу робити таємні речі, як-от просто мати масив bools або що у вас.)

10
"ваш код суттєво простіший" - абсолютно навпаки, якщо ви робите що-небудь, крім просто тестування окремих бітів ... циклів і операцій з набором стає практично неможливим, оскільки поля та властивості не є сутностями першого класу.
Джим Балтер

2
@nawfal "трохи" Я бачу, що ти там робив
stannius

Відповіді:


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

7
Зверніть увагу, що HasFlagце доступно від .NET 4 і далі.
Andreas Grech

3
Це чудово! Але ви можете зробити це ще простішим і легким у використанні. Просто дотримуйтесь цього як методу розширення: Enum.GetValues(input.GetType()).Cast<Enum>().Where(input.HasFlag); Тоді просто: myEnum.GetFLags():)
joshcomley

3
Чудовий однолінійний Джош, але він все ще страждає від проблеми з вибором значень мульти-прапора (Boo) замість лише однозначних значень (Bar, Baz), як у відповіді Джеффа, вище.

10
Приємно - слідкуйте за тим, що ніхто не містить - напр., Пункти. Ніхто з відповіді Джеффа завжди буде включений
Ілан

1
Підпис методу має бутиstatic IEnumerable<Enum> GetFlags(this Enum input)
Ервін Ройджаккерс

48

Ось вирішення проблеми Linq.

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

7
Чому це не вгорі ?? :) Використовуйте, .Where(v => !Equals((int)(object)v, 0) && e.HasFlag(v));якщо у вас є нульове значення для представленняNone
georgiosd

Супер чисто. Найкраще рішення на мій погляд.
Райан

@georgiosd Я думаю, продуктивність не така вже й велика. (Але має бути досить гарним для більшості завдань)
AntiHeadshot

41

Наскільки я не знаю, немає вбудованих методів для отримання кожного компонента. Але ось один спосіб їх отримати:

[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

Я адаптував те, що Enumробить внутрішньо, щоб генерувати рядок, щоб замість цього повернути прапори. Ви можете подивитися на код у відбивачі і він повинен бути більш-менш еквівалентним. Добре працює для випадків загального використання, коли є значення, які містять кілька біт.

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;
        }
    }
}

Метод розширення GetIndividualFlags()отримує всі окремі прапори для типу. Тож значення, що містять декілька біт, не залишаються.

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

Я б подумав зробити розбиття рядків, але це, ймовірно, набагато більше накладних витрат, ніж просто повторення бітових значень всього переліку.

На жаль, виконуючи це, вам доведеться протестувати зайві значення (якщо цього не хотіли). Дивіться мій другий приклад, він дав би результат Bar, Bazа Booзамість просто Boo.
Джефф Меркадо

Цікаво, що ти можеш витягнути Бу з цього, хоча ця частина непотрібна (і насправді дуже погана ідея :)) для того, що я роблю.

Це виглядає цікаво, але якщо я не розумію, він поверне Boo для вищезгаданого перерахунку, де я хотів би перерахувати лише версії, які не є комбінаціями інших значень (тобто ті, що є повноваженнями двох) . Чи можна це зробити легко? Я намагався і не можу придумати простий спосіб визначити це, не вдаючись до математики FP.

@Robin: Ти маєш рацію, оригінал повернеться Boo(значення, повернене з використанням ToString()). Я налаштував це, щоб дозволити лише окремі прапори. Тож у моєму прикладі можна отримати Barі Bazзамість Boo.
Джефф Меркадо

26

Повернувшись до цього через кілька років, маючи трохи більше досвіду, моя остаточна відповідь лише для однобітних значень, рухаючись від найнижчого біта до найвищого біта, є незначним варіантом внутрішньої рутини Джеффа Меркадо:

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;
        }
    }
}

Це, здається, працює, і незважаючи на мої заперечення кілька років тому, я тут використовую HasFlag, оскільки це набагато розбірливіше, ніж використання побітових порівнянь, а різниця швидкостей незначна для всього, що я буду робити. (Цілком можливо, що з тих пір вони покращили швидкість HasFlags, бо все, що я знаю ... Я не перевіряв.)


Просто прапор примітка инициализируется до міжнар має бути до ULong , такі як біти, повинні бути ініційовані 1UL
forcewill

Дякую, я це виправлю! (Я щойно пішов і перевірив свій фактичний код, і я вже виправив його навпаки, конкретно оголосивши його про улонг.)

2
Це єдине рішення, яке я знайшов, але, здається, також не страждає від того, що якщо у вас є прапор зі значенням нульовим, який має означати "None", інші відповіді методами GetFlag () повернуть YourEnum.None як один із прапорів навіть якщо це насправді не в переліку, ви запускаєте метод! Я отримував дивні дублікати записів журналу, оскільки методи працювали більше разів, ніж я очікував, коли у них було встановлено лише один ненульовий прапор перерахунку. Дякуємо, що знайшли час для оновлення та додання цього чудового рішення!
БрайанH

yield return bits;?
Jaider

1
Я хотів змінну перерахунку "Усі", для якої я призначив ulong.MaxValue, тому всі біти встановлені на "1". Але ваш код потрапляє в нескінченний цикл, оскільки прапор <біт ніколи не оцінюється як істинний (прапор циклів на мінус і потім застрягає на 0).
Хайстром

15

Вимкнення методу @ Грега, але додавання нової функції від C # 7.3, Enumобмеження:

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;
}

Нове обмеження дозволяє це метод розширення, не потребуючи перенесення (int)(object)e, і я можу використовувати HasFlagметод і передати безпосередньо Tз value.

C # 7.3 також додає обмеження для делегатів та unmanaged.


4
Ви, ймовірно, хотіли, щоб flagsпараметр був також загальним типом T, інакше вам доведеться чітко вказувати тип enum кожного разу, коли ви його викликаєте.
Рей

11

+1 за відповідь, надану @ RobinHood70. Я виявив, що загальна версія методу була для мене зручною.

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;
        }
    }
}

EDIT І +1 для @AustinWBryan для введення C # 7.3 у простір рішення.

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;
        }
    }
}

3

Вам не потрібно повторювати всі значення. просто перевірте свої конкретні прапори так:

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

або (як сказано в коментарях pstrjds ), ви можете перевірити, чи не використовується так:

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

5
Якщо ви використовуєте .Net 4.0, є метод розширення HasFlag, який ви можете використовувати для того ж самого: myVar.HasFlag (FlagsEnum.Flag1)
pstrjds

1
Якщо програміст не може зрозуміти побіжно І операцію, він повинен упакувати його і знайти нову кар'єру.
Ред С.

2
@Ed: правда, але HasFlag краще, коли ви знову читаєте код ... (можливо, через кілька місяців або років)
Д-р TJ

4
@Ed Swangren: Дійсно про те, щоб зробити код більш читабельним і менш дослідним, не обов'язково, оскільки використання побітових операцій "важко".
Джефф Меркадо

2
HasFlag надзвичайно повільний. Спробуйте великий цикл, використовуючи HasFlag проти біт-маскування, і ви помітите величезну різницю.

3

Що я зробив, це змінити мій підхід, замість того, щоб вводити вхідний параметр методу як enumтип, я набрав його як масив enumтипу ( MyEnum[] myEnums), таким чином я просто повторюю масив із заявою перемикання всередині циклу.


2

Не були задоволені відповідями вище, хоча вони були початком.

Після зібрання сюди кількох різних джерел тут:
Попередній плакат у проекті поточного
коду SO QnA прапори Enum прапори Перевірити пост
Great Enum <T> Утиліта

Я створив це, тому дайте мені знати, що ви думаєте.
Параметри::
bool checkZeroвказує на те, щоб дозволити 0як значення прапора. За замовчуванням input = 0повертається порожнім.
bool checkFlags: каже, щоб перевірити, чи Enumоформлено w / [Flags]атрибут.
PS. Зараз я не checkCombinators = falseвстигаю розібратися на alg, який змусить його ігнорувати будь-які значення перерахунку, що є комбінацією бітів.

    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;
            }
        }

    }

Для цього використовується Enum <Hel> Class Helper <T>, який я оновив для використання yield returnдля 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);
    }
}  

Нарешті, ось приклад його використання:

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

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

        return cts;
    }

На жаль, не встигли переглянути цей проект протягом декількох днів. Я повернуся до вас, як тільки я краще ознайомлюсь із вашим кодом.

4
Просто підкресліть, що в цьому коді є помилка. Код в обох гілках оператора if (checkCombinators) ідентичний. Крім того, можливо, це не помилка, але несподівано, що якщо у вас значення enum оголошено за 0, воно завжди буде повернуто в колекцію. Здається, що повернути його слід лише в тому випадку, якщо checkZero є істинним і немає інших прапорів.
dhochee

@dhochee. Я згоден. Або код досить приємний, але аргументи заплутані.
AFract

2

Спираючись на відповідь Грега вище, це також стосується випадку, коли у вашому перерахунку є значення 0, наприклад None = 0. У цьому випадку він не повинен повторювати це значення.

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;
}

Хтось би знав, як покращити це ще більше, щоб він міг обробляти випадок, коли всі прапори в перерахунку встановлені надзвичайно розумним способом, який би міг обробляти всі базові типи перерахунків та випадок All = ~ 0 та All = EnumValue1 | EnumValue2 | EnumValue3 | ...


1

Ви можете використовувати ітератор від Enum. Починаючи з коду 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];
            }
        }
    }
}

Це не перевірено, тому, якщо я помилився, не соромтеся зателефонувати мені. (очевидно, це не повторно.) Вам слід заздалегідь призначити значення або розбити його на іншу функцію, яка використовує enum.dayflag та enum.days. Можливо, ви зможете кудись піти з контуром.


0

Це може бути також наступний код:

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(',');
}

Він переходить всередину, якщо тільки значення перерахунку міститься у значенні


0

Усі відповіді добре працюють із простими прапорами, ви, мабуть, зіткнетеся з проблемами, коли прапори поєднуються.

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

ймовірно, потрібно додати чек, щоб перевірити, чи є значення перерахунку значення "self" - комбінація. Вам, напевно, знадобиться щось на кшталт опублікованого тут Хенка ван Бойєна, щоб покрити свою вимогу (потрібно трохи прокрутити вниз)


0

Метод розширення з використанням нового обмеження та дженериків Enum для запобігання трансляції:

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();
    }
}

-1

Ви можете зробити це безпосередньо, перейшовши на int, але ви втратите перевірку типу. Я думаю, що найкращий спосіб - використовувати щось подібне до моєї пропозиції. Він підтримує належний тип до кінця. Конверсія не потрібна. Це не ідеально завдяки боксу, який додасть трохи ударів у виконанні.

Не ідеально (бокс), але це робить роботу без попередження ...

/// <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;
        }
    }
}

Використання:

    FileAttributes att = FileAttributes.Normal | FileAttributes.Compressed;
    foreach (FileAttributes fa in att.GetFlags())
    {
        ...
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.