繁体   English   中英

如何获得枚举的所有可能组合(标志)

[英]How do I get all possible combinations of an enum ( Flags )

[Flags]
public enum MyEnum
{
    None = 0,
    Setting1 = (1 << 1),
    Setting2 = (1 << 2),
    Setting3 = (1 << 3),
    Setting4 = (1 << 4),
}

我需要能够以某种方式遍历每个可能的设置并将设置组合传递给 function。 可悲的是,我一直无法弄清楚如何做到这一点

未经测试,使用风险自负,但应该足够通用地解决问题。 System.Enum不是有效的限制,因为从技术上讲 C# 只允许 inheritance 在/与class中/与 class 一起绕过EnumValueType 对丑陋的演员阵容感到抱歉。 它也不是非常有效,但除非您针对动态生成的类型运行它,否则每次执行只需要执行一次(或者如果保存一次)。

public static List<T> GetAllEnums<T>()
    where T : struct
    // With C# 7.3 where T : Enum works
{
    // Unneeded if you add T : Enum
    if (typeof(T).BaseType != typeof(Enum)) throw new ArgumentException("T must be an Enum type");

    // The return type of Enum.GetValues is Array but it is effectively int[] per docs
    // This bit converts to int[]
    var values = Enum.GetValues(typeof(T)).Cast<int>().ToArray();

    if (!typeof(T).GetCustomAttributes(typeof(FlagsAttribute), false).Any())
    {
        // We don't have flags so just return the result of GetValues
        return values;
    }

    var valuesInverted = values.Select(v => ~v).ToArray();
    int max = 0;
    for (int i = 0; i < values.Length; i++)
    {
        max |= values[i];
    }

    var result = new List<T>();
    for (int i = 0; i <= max; i++)
    {
        int unaccountedBits = i;
        for (int j = 0; j < valuesInverted.Length; j++)
        {
            // This step removes each flag that is set in one of the Enums thus ensuring that an Enum with missing bits won't be passed an int that has those bits set
            unaccountedBits &= valuesInverted[j];
            if (unaccountedBits == 0)
            {
                result.Add((T)(object)i);
                break;
            }
        }
    }

    //Check for zero
    try
    {
        if (string.IsNullOrEmpty(Enum.GetName(typeof(T), (T)(object)0)))
        {
            result.Remove((T)(object)0);
        }
    }
    catch
    {
        result.Remove((T)(object)0);
    }

    return result;
}

这通过获取所有值并将它们 ORing 在一起,而不是求和,以防包含合数。 然后将每个 integer 取到最大值,并用每个 Flag 的反转来屏蔽它们,这导致有效位变为 0,从而使我们能够识别那些不可能的位。

最后的检查是从枚举中丢失零。 如果您可以始终在结果中包含零枚举,则可以将其删除。

当给定一个包含 2,4,6,32,34,16384 的枚举时,给出 15 的预期结果。

    public IEnumerable<TEnum> AllCombinations<TEnum>() where TEnum : struct
    {
        Type enumType = typeof (TEnum);
        if (!enumType.IsEnum)
            throw new ArgumentException(string.Format("The type {0} does not represent an enumeration.", enumType), "TEnum");

        if (enumType.GetCustomAttributes(typeof (FlagsAttribute), true).Length > 0) //Has Flags attribute
        {
            var allCombinations = new HashSet<TEnum>();

            var underlyingType = Enum.GetUnderlyingType(enumType);
            if (underlyingType == typeof (sbyte) || underlyingType == typeof (short) || underlyingType == typeof (int) || underlyingType == typeof (long))
            {
                long[] enumValues = Array.ConvertAll((TEnum[]) Enum.GetValues(enumType), value => Convert.ToInt64(value));
                for (int i = 0; i < enumValues.Length; i++)
                    FillCombinationsRecursive(enumValues[i], i + 1, enumValues, allCombinations);
            }
            else if (underlyingType == typeof (byte) || underlyingType == typeof (ushort) || underlyingType == typeof (uint) || underlyingType == typeof (ulong))
            {
                ulong[] enumValues = Array.ConvertAll((TEnum[]) Enum.GetValues(enumType), value => Convert.ToUInt64(value));
                for (int i = 0; i < enumValues.Length; i++)
                    FillCombinationsRecursive(enumValues[i], i + 1, enumValues, allCombinations);
            }
            return allCombinations;
        }
        //No Flags attribute
        return (TEnum[]) Enum.GetValues(enumType);
    }

    private void FillCombinationsRecursive<TEnum>(long combination, int start, long[] initialValues, HashSet<TEnum> combinations) where TEnum : struct
    {
        combinations.Add((TEnum)Enum.ToObject(typeof(TEnum), combination));
        if (combination == 0)
            return;

        for (int i = start; i < initialValues.Length; i++)
        {
            var nextCombination = combination | initialValues[i];
            FillCombinationsRecursive(nextCombination, i + 1, initialValues, combinations);
        }
    }

    private void FillCombinationsRecursive<TEnum>(ulong combination, int start, ulong[] initialValues, HashSet<TEnum> combinations) where TEnum : struct
    {
        combinations.Add((TEnum)Enum.ToObject(typeof(TEnum), combination));
        if (combination == 0)
            return;

        for (int i = start; i < initialValues.Length; i++)
        {
            var nextCombination = combination | initialValues[i];
            FillCombinationsRecursive(nextCombination, i + 1, initialValues, combinations);
        }
    }

这是您的代码示例特有的解决方案,使用简单的 for 循环(不要使用,请参阅下面的更新)

int max = (int)(MyEnum.Setting1 | MyEnum.Setting2 | MyEnum.Setting3 | MyEnum.Setting4);
for (int i = 0; i <= max; i++)
{
    var value = (MyEnum)i;
    SomeOtherFunction(value);
}

更新:这是一个通用方法,它将返回所有可能的组合。 并感谢@David Yaw提出使用队列来构建每个组合的想法。

IEnumerable<T> AllCombinations<T>() where T : struct
{
    // Constuct a function for OR-ing together two enums
    Type type = typeof(T);
    var param1 = Expression.Parameter(type);
    var param2 = Expression.Parameter(type);
    var orFunction = Expression.Lambda<Func<T, T, T>>(
        Expression.Convert(
            Expression.Or(
                Expression.Convert(param1, type.GetEnumUnderlyingType()),
                Expression.Convert(param2, type.GetEnumUnderlyingType())),
            type), param1, param2).Compile();

    var initalValues = (T[])Enum.GetValues(type);
    var discoveredCombinations = new HashSet<T>(initalValues);
    var queue = new Queue<T>(initalValues);

    // Try OR-ing every inital value to each value in the queue
    while (queue.Count > 0)
    {
        T a = queue.Dequeue();
        foreach (T b in initalValues)
        {
            T combo = orFunction(a, b);
            if (discoveredCombinations.Add(combo))
                queue.Enqueue(combo);
        }
    }

    return discoveredCombinations;
}

既然它是一个标记的枚举,为什么不简单地:

  1. 获取枚举的最高值。
  2. 计算组合的数量,即上限。
  3. 迭代每个组合,即从 0 循环到上限。

一个例子看起来像这样

var highestEnum = Enum.GetValues(typeof(MyEnum)).Cast<int>().Max();
var upperBound = highestEnum * 2;    
for (int i = 0; i < upperBound; i++)
{
    Console.WriteLine(((MyEnum)i).ToString());
}

首先,获取所有单个值的列表。 由于您有 5 个值,即(1 << 5) = 32 个组合,因此从 1 迭代到 31。(不要从零开始,这意味着不包含任何枚举值。)迭代时,检查数字中的位,迭代变量中的每一位都意味着包含该枚举值。 将结果放入 HashSet 中,这样就不会有重复项,因为包含“None”值不会更改结果枚举。

List<MyEnum> allValues = new List<MyEnum>(Enum.Getvalues(typeof(MyEnum)));
HashSet<MyEnum> allCombos = new Hashset<MyEnum>();

for(int i = 1; i < (1<<allValues.Count); i++)
{
    MyEnum working = (MyEnum)0;
    int index = 0;
    int checker = i;
    while(checker != 0)
    {
        if(checker & 0x01 == 0x01) working |= allValues[index];
        checker = checker >> 1;
        index++;
    }
    allCombos.Add(working);
}

我可能有点晚了,我想离开我的解决方案,其中还包括值加上组合的可能文本(“V1 | V2”,“V1 | V2 | V3”等)。

我采用了上述解决方案的某些方面,因此感谢所有发布先前答案的人:D。

注意:仅适用于设置为基数 2 组合的枚举。

public static Dictionary<int,string> GetCombinations( this Enum enu)
    {
        var fields = enu.GetType()
                        .GetFields()
                        .Where(f => f.Name != "value__")
                        .DistinctBy(f=> Convert.ToInt32(f.GetRawConstantValue()));

        var result = fields.ToDictionary(f=>Convert.ToInt32(f.GetRawConstantValue()), f => f.Name);

        int max = Enum.GetValues(enu.GetType()).Cast<int>().Max();
        int upperBound = max * 2;

        for (int i = 0 ; i <= upperBound ; i += 2)
        {
            string s = Convert.ToString(i, 2).PadLeft(Math.Abs(i-max),'0');
            Boolean[] bits = s.Select(chs => chs == '1' ? true : false)
                             .Reverse()
                             .ToArray();

            if (!result.ContainsKey(i))
            {
                var newComb = string.Empty;
                for (int j = 1; j < bits.Count(); j++)
                {
                    var idx = 1 << j;
                    if (bits[j] && result.ContainsKey(idx))
                    {
                        newComb = newComb + result[idx] + " | ";
                    }
                }                                      
                newComb = newComb.Trim(new char[] { ' ', '|' });
                if (!result.ContainsValue(newComb) && !string.IsNullOrEmpty(newComb))
                {
                    result.Add(i, newComb);
                }                    
            }
        }
        return result;
    }

与此版本结束:

没有检查:

public IEnumerable<T> AllCombinations<T>() where T : struct
{
    var type = typeof(T);
    for (var combination = 0; combination < Enum.GetValues(type).Cast<int>().Max()*2; combination++)
    {
        yield return (T)Enum.ToObject(type, combination);
    }
}

通过一些检查

public IEnumerable<T> AllCombinations<T>() where T : struct
{
    var type = typeof(T);
    if (!type.IsEnum)
    {
        throw new ArgumentException($"Type parameter '{nameof(T)}' must be an Enum type.");
    }

    for (var combination = 0; combination < Enum.GetValues(type).Cast<int>().Max()*2; combination++)
    {
        var result = (T)Enum.ToObject(type, combination);

        // Optional check for legal combination.
        // (and is not necessary if all flag a ascending exponent of 2 like 2, 4, 8...
        if (result.ToString() == combination.ToString() && combination != 0)
        {
            continue;
        }

        yield return result;
    }
}

当我向枚举添加新成员时,我通常不想更新代表枚举最大值的每个变量。 例如,我不喜欢 Greg 提出的声明:

int max = (int)(MyEnum.Setting1 | MyEnum.Setting2 | ... | MyEnum.SettingN);

考虑一下,当您的解决方案中散布着这些变量中的几个并且您决定修改您的枚举时。 这肯定不是一个理想的情况。

我会提前承认我的代码速度较慢,但是在修改枚举后它会自动正确,并且我努力以这种健壮的方式进行编码。 我愿意为此付出一些计算上的代价,C# 无论如何都是这样的。 我提议:

public static IEnumerable<T> GetAllValues<T>() where T : struct
{
    if (!typeof(T).IsEnum) throw new ArgumentException("Generic argument is not an enumeration type");
    int maxEnumValue = (1 << Enum.GetValues(typeof(T)).Length) - 1;
    return Enumerable.Range(0, maxEnumValue).Cast<T>();
}

这假设枚举包含 2 到某个幂(包括 0)的所有幂的成员,就像通常使用的标志枚举一样。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM