Перебирать значения в Flags Enum?
Если у меня есть переменная, содержащая перечисление flags, могу ли я как-то перебрать значения битов в этой конкретной переменной? Или мне нужно использовать Enum.GetValues для перебора всего перечисления и проверки, какие из них установлены?
19 ответов
Возвращаясь к этому несколько лет спустя, имея немного больше опыта, мой окончательный ответ только для одноразрядных значений, переход от младшего к старшему, является небольшим вариантом внутренней процедуры Джеффа Меркадо:
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, насколько я знаю... я не проверял.)
static IEnumerable<Enum> GetFlags(Enum input)
{
foreach (Enum value in Enum.GetValues(input.GetType()))
if (input.HasFlag(value))
yield return value;
}
Вот решение проблемы Linq.
public static IEnumerable<Enum> GetFlags(this Enum e)
{
return Enum.GetValues(e.GetType()).Cast<Enum>().Where(e.HasFlag);
}
Там нет никаких методов AFAIK, чтобы получить каждый компонент. Вот один из способов получить их:
[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
Исходя из метода @ Грега, но добавив новую функцию из 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
,
+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;
}
}
}
Метод расширения с использованием нового ограничения 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();
}
}
Продолжая делать код короче, это моя последняя версия процедуры. (Я ОП... длинная история.) Как обсуждалось ранее, это игнорирует None и многобитовые значения.
Обратите внимание, что здесь используется ограничение Enum и шаблон var, поэтому потребуется как минимум 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;
}
}
}
Вот еще одно решение C# 7.3 с использованием Linq
public static class FlagEnumExtensions
{
public static IEnumerable<T> GetFlags<T>(this T en) where T : struct, Enum
{
return Enum.GetValues<T>().Where(member => en.HasFlag(member));
}
}
Вам не нужно повторять все значения. просто проверьте ваши конкретные флаги, например, так:
if((myVar & FlagsEnum.Flag1) == FlagsEnum.Flag1)
{
//do something...
}
или (как сказал pstrjds в комментариях) вы можете проверить его использование следующим образом:
if(myVar.HasFlag(FlagsEnum.Flag1))
{
//do something...
}
Не был удовлетворен ответами выше, хотя они были началом.
После объединения нескольких различных источников здесь:
Предыдущий постер в этой теме SO QnA
Code Project Enum Flags Check Post
Great Enum
Я создал это, так что дайте мне знать, что вы думаете.
Параметры:bool checkZero
: говорит, что разрешить 0
в качестве значения флага. По умолчанию input = 0
возвращается пустым.bool checkFlags
: говорит, чтобы проверить, является ли Enum
оформлен с [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;
}
}
}
Это позволяет использовать перечисленные здесь вспомогательные классы Enumyield 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;
}
Когда дело доходит до производительности, которая важна при разработке игр, все приведенные решения ужасны из-за выделения мусора и низкой скорости. Это решение без мусора, более чем в 100 раз быстрее, чем принятый ответ.
[Flags]
public enum PersonalTraits : short
{
None = 1 << 0,
Strength = 1 << 1,
Agility = 1 << 2,
Attack = 1 << 3,
Defence = 1 << 4,
Vitality = 1 << 5,
Stamina = 1 << 6,
Accuracy = 1 << 7,
Perception = 1 << 8,
Charisma = 1 << 9,
}
PersonalTraits athlete = PersonalTraits.Stamina | PersonalTraits.Strength;
for (short i = 0, value = 0; value <= (short)PersonalTraits.Charisma; i++, value = (short)(1 << i))
if (((short)athlete & value) != 0)
yield return (PersonalTraits)value;
В .NET 6
Enum.GetValues<yourenum>().Where(e => input.HasFlag(e));
Я изменил свой подход, вместо того, чтобы вводить входной параметр метода в качестве enum
типа, я набрал его в виде массива enum
тип (MyEnum[] myEnums
), таким образом, я просто перебираю массив с оператором switch внутри цикла.
Все ответы хорошо работают с простыми флагами, вы, вероятно, столкнетесь с проблемами при объединении флагов.
[Flags]
enum Food
{
None=0
Bread=1,
Pasta=2,
Apples=4,
Banana=8,
WithGluten=Bread|Pasta,
Fruits = Apples | Banana,
}
вероятно, нужно добавить проверку, чтобы проверить, является ли само значение enum комбинацией. Вам, вероятно, понадобится что-то подобное, опубликованное здесь Хенком ван Бойеном, чтобы удовлетворить ваши требования (вам нужно немного прокрутить вниз)
Основываясь на ответе Грега выше, это также учитывает случай, когда у вас есть значение 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 | ...
Вы можете использовать Итератор из 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. Вы могли бы быть в состоянии пойти куда-нибудь с планом.
Это может быть так же, как следующий код:
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(',');
}
Он входит внутрь, только если в значении содержится значение enum
Вы можете сделать это напрямую, конвертировав в 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())
{
...
}