如何迭代具有标志的枚举值?

164

如果我有一个变量持有一个标志枚举,我是否可以以某种方式迭代该特定变量中的单个位值?或者我必须使用Enum.GetValues遍历整个枚举并检查哪些被设置了?

19个回答

209
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
5
太好了!但是您可以使它更简单易用。只需将以下内容作为扩展方法添加:Enum.GetValues(input.GetType()).Cast<Enum>().Where(input.HasFlag);然后只需要使用: myEnum.GetFLags() :) - joshcomley
3
非常好的一句话,乔许,但它仍然存在一个问题,就是捕获多标志值(Boo),而不仅仅是单标志值(Bar,Baz),就像杰夫在上面的答案中所说的那样。 - user502255
12
好的,我会尽力进行翻译。请注意,"Nice - watch out for None's though - e.g. Items.None from Jeff's answer will always be included" 这句话的意思是“不错,但要注意None的情况 - 例如,Jeff的答案中的 Items.None 将始终包含在内”。 - Ilan
2
方法签名应为 static IEnumerable<Enum> GetFlags(this Enum input) - user2609980
显示剩余2条评论

72

这里是一个解决问题的 Linq 解决方案。

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

13
如果您有一个零值来表示“无”,请使用".Where(v => !Equals((int)(object)v, 0) && e.HasFlag(v));",为什么这不在顶部呢? :) - georgiosd
@georgiosd 我猜性能不是很好。(但对于大多数任务来说应该足够好了) - AntiHeadshot
1
@georgiosd:如果底层类型不是“int”,这将失败。您应该使用var zero = (T)Convert.ChangeType(0, Enum.GetUnderlyingType(typeof(T)));并检查.Where(enumValue => !Equals(enumValue, zero)) - Tobias Knauss

40

据我所知,没有内置方法可以获得每个组件。但是这里有一种方法可以获取它们:

[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”。 - Jeff Mercado
@Robin:你说得对,原始代码会返回Boo(使用ToString()返回的值)。我已经进行了调整,只允许单个标志。所以在我的示例中,你可以获得BarBaz而不是Boo - Jeff Mercado
请注意,如果您的枚举具有负值(例如使用Visual Studio可扩展性框架Microsoft.VisualStudio.Shell.Interop._VSRDTFLAGS),则在此处使用ulong和ToUInt64将会导致问题。如果您替换为long和ToInt64,则可以处理具有负值的枚举。 - Nerdtron
自从.NET 6中引入了BitOperations.IsPow2()函数,可以将GetFlagValues简化为一行代码。 - undefined

32

几年后回来,经验更加丰富,对于仅限单个比特值的情况,从最低位到最高位移动,我的最终答案是Jeff 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;
        }
    }
}

看起来它能工作,尽管我几年前曾反对使用HasFlag,但在这里我使用它,因为它比使用位运算符更易读,并且速度差异对我所做的任何事情都是微不足道的。(他们完全有可能自那时以来改进了HasFlags的速度,我并没有测试。)


2
这是我找到的唯一解决方案,似乎也不会受到一个值为零的标志的影响,该标志应该表示“无”,其他 GetFlag() 方法将返回 YourEnum.None 作为其中一个标志,即使它实际上不在你运行方法的枚举中!当只有一个非零枚举标志设置时,方法运行的次数比我预期的要多,导致出现奇怪的重复日志条目。感谢您抽出时间更新并添加这个很棒的解决方案! - BrianH
这是一个不错的解决方案,但有一个小问题 - 如果您使用带符号的int64值,则无法找到最高有效位。 - TehGM

26

参考@Greg的方法,但加入了C# 7.3的新功能,即Enum约束:

public static IEnumerable<T> GetUniqueFlags<T>(this T 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方法并直接从value转换为T
C# 7.3 还为委托和unmanaged添加了约束。

15

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

编辑 并且点赞@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;
        }
    }
}


7

使用新的枚举约束和泛型编写扩展方法,以避免强制类型转换:

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

4

这是另一种使用Linq的C# 7.3解决方案。

using System;
using System.Collections.Generic;
using System.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)).ToArray();
    }
}

编辑:添加 .ToArray() 以防止多次枚举。


4

为了让代码更短,这是我的最新版本。 (我是OP...长故事。)如之前所讨论的,它会忽略None和多位值。

请注意,此版本使用了枚举约束和变量模式,因此至少需要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;
        }
    }
}

为了显著提高性能,您可以将GetEnumValues()的结果转换为T[]并消除enumValue is T flag条件,但这取决于GetEnumValues始终返回类型为T的数组,而这并非契约上要求的。 - RobinHood70

3

在游戏开发中,性能至关重要。由于垃圾分配和速度缓慢,所有给出的解决方案都很糟糕。但这是一个无垃圾分配、速度比被接受的答案快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;

你的解决方案只适用于一个特定的枚举。 - Mike Christiansen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接