Как перебирать значения Enum с флагами?

131

Если у меня есть переменная, содержащая перечисление флагов, могу ли я как-то перебрать битовые значения в этой конкретной переменной? Или мне нужно использовать Enum.GetValues ​​для перебора всего перечисления и проверки того, какие из них установлены?

Оливье Рожье
источник
Если у вас есть контроль над своим API, избегайте использования битовых флагов. Это редко бывает полезной оптимизацией. Использование структуры с несколькими общедоступными полями типа «bool» семантически эквивалентно, но ваш код значительно проще. И если вам понадобится позже, вы можете изменить поля на свойства, которые управляют битовыми полями внутри, инкапсулируя оптимизацию.
Jay Bazuzi
2
Я понимаю, о чем вы говорите, и во многих случаях это имеет смысл, но в этом случае у меня будет та же проблема, что и с предложением If ... Вместо этого мне пришлось бы написать операторы If для дюжины разных bool использования простого цикла foreach над массивом. (И поскольку это часть общедоступной DLL, я не могу делать загадочные вещи, например, просто иметь массив bools или что-то еще.)
10
«ваш код значительно проще» - совершенно противоположное верно, если вы делаете что-либо, кроме тестирования отдельных битов ... циклы и операции с наборами становятся практически невозможными, потому что поля и свойства не являются объектами первого класса.
Джим Балтер
2
@nawfal "немного" Я вижу, что вы там делали
станниус

Ответы:

180
static IEnumerable<Enum> GetFlags(Enum input)
{
    foreach (Enum value in Enum.GetValues(input.GetType()))
        if (input.HasFlag(value))
            yield return value;
}
Greg
источник
7
Обратите внимание, что HasFlagэто доступно начиная с .NET 4.
Андреас Греч
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);
}
agritton
источник
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, насколько я знаю ... Я не тестировал.)


источник
Просто флаг примечания инициализируется как int, должен быть ulong, например биты, должен быть инициализирован как 1ul
forcewill
Спасибо, я исправлю! (Я просто пошел и проверил свой реальный код, и я уже исправил его наоборот, специально объявив его ulong.)
2
Это единственное найденное мной решение, которое, похоже, также не страдает от того факта, что если у вас есть флаг с нулевым значением, который должен представлять «None», другие методы ответов GetFlag () вернут YourEnum.None как один из флагов даже если на самом деле его нет в перечислении, в котором вы запускаете метод! Я получал странные повторяющиеся записи журнала, потому что методы выполнялись больше раз, чем я ожидал, когда у них был установлен только один ненулевой флаг перечисления. Спасибо, что нашли время обновить и добавить это отличное решение!
BrianH
yield return bits;?
Jaider 05
1
Мне нужна переменная перечисления «All», для которой я назначил ulong.MaxValue, поэтому все биты установлены в «1». Но ваш код попадает в бесконечный цикл, потому что flag <bits никогда не оценивается как истина (цикл флагов становится отрицательным, а затем застревает на 0).
Haighstrom
16

Уходя от метода @Greg, но добавляя новую функцию из 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метод и преобразовывать его напрямую в Tfrom value.

В C # 7.3 также добавлены ограничения для delagates и unmanaged.

AustinWBryan
источник
5
Вероятно, вы хотели, чтобы flagsпараметр Tтоже был универсального типа , иначе вам пришлось бы явно указывать тип перечисления каждый раз, когда вы его вызываете.
Рэй
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...
}
Д-р TJ
источник
5
Если вы используете .Net 4.0, существует метод расширения HasFlag, который вы можете использовать для того же: myVar.HasFlag (FlagsEnum.Flag1)
pstrjds
1
Если программист не понимает побитовой операции И, он должен упаковать ее и найти новую карьеру.
Эд С.
2
@Ed: верно, но HasFlag лучше, когда вы снова читаете код ... (может быть, через несколько месяцев или лет)
Dr TJ
4
@Ed Swangren: На самом деле речь идет о том, чтобы сделать код более читабельным и менее подробным, не обязательно потому, что использование побитовых операций «сложно».
Джефф Меркадо,
2
HasFlag работает очень медленно. Попробуйте использовать большой цикл с использованием HasFlag и побитовой маскировки, и вы заметите огромную разницу.
3

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

Ragin'Geek
источник
2

Не был удовлетворен приведенными выше ответами, хотя они были началом.

После объединения нескольких различных источников здесь:
Предыдущий постер в этой теме
Флаги перечисления проекта кода QnA SO Проверить сообщение
Утилита Great Enum <T>

Я создал это, так что дайте мне знать, что вы думаете.
Параметры::
bool checkZeroуказывает разрешить 0как значение флага. По умолчанию input = 0возвращает пустое значение.
bool checkFlags: указывает ему проверить, Enumукрашен ли [Flags]атрибут атрибутом.
PS. У меня сейчас нет времени выяснять checkCombinators = falseалгоритм, который заставит его игнорировать любые значения перечисления, которые являются комбинациями битов.

    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 <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;
    }
eudaimos
источник
Извините, не успел посмотреть этот проект несколько дней. Я вернусь к вам, когда лучше посмотрю на ваш код.
4
Просто предупреждаю, что в этом коде есть ошибка. Код в обеих ветвях оператора if (checkCombinators) идентичен. Кроме того, возможно, это не ошибка, но неожиданно, если у вас есть значение перечисления, объявленное для 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. Вы могли бы пойти куда-нибудь с контуром.

SilverbackNet
источник
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,
}

вероятно, потребуется добавить проверку, чтобы проверить, является ли само значение перечисления комбинацией. Вам, вероятно, понадобится что-то вроде написанного здесь Хенком ван Бойеном, чтобы удовлетворить ваши требования (вам нужно немного прокрутить вниз)

Уолтер Веховен
источник
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())
    {
        ...
    }
Эрик Уэлле
источник