Как перебирать значения 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;
}

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

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.


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

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 внутри цикла.


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

Извините, не успел посмотреть этот проект несколько дней. Я вернусь к вам, когда лучше посмотрю на ваш код.

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. Вы могли бы пойти куда-нибудь с контуром.


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())
    {
        ...
    }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.