将枚举值的数组转换为标志枚举

本文关键字:枚举 标志 转换 数组 | 更新日期: 2023-09-27 18:08:24

我正在尝试以这里指定的方式实现功能:

具体的解决方案

然而,我试图做它作为一个通用的方法被用作扩展:

    public static TEnum? Merge<TEnum>(this IEnumerable<TEnum> values)
        where TEnum : struct, IConvertible, IComparable, IFormattable
    {
        Nullable<TEnum> merged = null;
        if (values == null || values.Count() == 0)
            return null;
        foreach(TEnum value in values)
        {
            if (merged == null)
                merged = value;
            else
            {
                merged = merged | value;
            }
        }
        return merged;
    }

问题是这一行:

merged = merged | value;

将不编译。我收到的消息是:

操作符'|'不能应用于'TEnum? '类型的操作数

是否有可能编写这个泛型方法,将数组的枚举值转换为标志枚举?

将枚举值的数组转换为标志枚举

这里有几个问题,但最大的问题是泛型不支持操作符—而|是一个操作符。你可以通过object绕过它,然后你就有了拳击。下面是我要做的——它生成一些动态的逐枚举类型IL(仅一次),并使用它来执行直接的"或"操作,而不需要装箱。请注意,它还使用0作为默认返回值(我认为这是意料之外的),并避免使用显式的Count(),因为这可能会不可预测地昂贵,并且可能破坏枚举器(您不能保证可以枚举数据多次):

using System;
using System.Collections.Generic;
using System.Reflection.Emit;
public static class EnumUtils
{
    public static TEnum Merge<TEnum>(this IEnumerable<TEnum> values)
        where TEnum : struct
    {
        TEnum merged = default(TEnum);
        if (values != null)
        {
            var or = Operator<TEnum>.Or;
            foreach (var value in values)
            {
                merged = or(merged, value);
            }
        }
        return (TEnum)(object)merged;
    }
    static class Operator<T>
    {
        public static readonly Func<T, T, T> Or;
        static Operator()
        {
            var dn = new DynamicMethod("or", typeof(T),
                new[] { typeof(T), typeof(T) }, typeof(EnumUtils));
            var il = dn.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Or);
            il.Emit(OpCodes.Ret);
            Or = (Func<T, T, T>)dn.CreateDelegate(typeof(Func<T, T, T>));
        }
    }
}
static class Program {
    [Flags]
    public enum Foo
    {
        None = 0, A = 1,  B =2, C = 4
    }
    static unsafe void Main()
    {
        var merged = EnumUtils.Merge(new[] { Foo.A, Foo.C });
    }
}

编辑:如果你真的必须返回null"null或empty"的情况下,那么你可以使用以下调整-但我强调:在我看来,这是一个不正确的实现-对于这种情况,简单返回0(又名default(TEnum))会更正确。

public static TEnum? Merge<TEnum>(this IEnumerable<TEnum> values)
    where TEnum : struct
{
    if (values == null) return null;
    using (var iter = values.GetEnumerator())
    {
        if (!iter.MoveNext()) return null;
        TEnum merged = iter.Current;
        var or = Operator<TEnum>.Or;
        while(iter.MoveNext())
        {
            merged = or(merged, iter.Current);
        }
        return merged;
    }
}

  • 检查空序、短路
  • 获取序列迭代器,如果没有,则尝试读取值-短路
  • 使用当前(第一个)值作为种子,并获得操作符
  • 迭代序列,依次应用操作符
  • 返回组合值

Linq中的1行,不需要辅助方法:

var result = enumArray.Aggregate ((TestEnum)0, (a, b) => a | b);

但是如果您不想记住这段代码,也可以使用扩展方法:

private static T Merge<T> (this T[] i_enumValues)
// - or -
// private static T Merge<T> (params T[] i_enumValues)
  where T : Enum
{
  var type = typeof (T);
  if (!type.IsDefined (typeof (FlagsAttribute), false))
    throw new ArgumentException ($"The given enum type '{type}' does not have the {nameof (FlagsAttribute)}.");
  var zero = (T)Convert.ChangeType (0, Enum.GetUnderlyingType (type));
  var result = i_enumValues.Aggregate (zero, (a, b) => (dynamic)a | (dynamic)b);
  return result;
}

概念证明:

[Flags]
public enum TestEnum : ulong
{
  First  = 1   << 0,
  Second = 1   << 1,
  Third  = 1   << 2,
  Fourth = 1   << 3,
  Fifth  = 1   << 4,
  Bit40  = 1ul << 40
}
private static void Main (string[] args)
{
  var enumArray = new[] { TestEnum.First, TestEnum.Second, TestEnum.Third, TestEnum.Bit40 };
  var result    = enumArray.Aggregate ((TestEnum)0, (a, b) => a | b);
  var result2   = Merge (enumArray);
}
输出:

result  = First | Second | Third | Bit40
result2 = First | Second | Third | Bit40
result .ToString ("X") -> "0000010000000007"
result2.ToString ("X") -> "0000010000000007"

我需要这种能力,很高兴找到Marc的答案。我尝试使用一些较新的语言特性。这是我想到的:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
public static class Extensions
{
    /// <summary>
    /// Intended for merging an enumeration of flags enum into a single value.
    /// </summary>
    public static TEnum Merge<TEnum>(this IEnumerable<TEnum> values)
        where TEnum : struct, IConvertible
    {
        var type = typeof(TEnum);
        if (!type.IsEnum)
            throw new InvalidOperationException($"{type} is not an enum type.");
        return values.DefaultIfEmpty(default(TEnum)).Aggregate(Operator<TEnum>.Or);
    }
    static class Operator<T>
    {
        private static readonly Lazy<Func<T, T, T>> LazyOr;
        private static readonly Lazy<Func<T, T, T>> LazyAnd;
        public static Func<T, T, T> Or => LazyOr.Value;
        public static Func<T, T, T> And => LazyAnd.Value;
        static Operator()
        {
            var enumType = typeof(T);
            var underType = enumType.GetEnumUnderlyingType();
            var leftParam = Expression.Parameter(enumType, "left");
            var rightParam = Expression.Parameter(enumType, "right");
            var leftCast = Expression.ConvertChecked(leftParam, underType);
            var rightCast = Expression.ConvertChecked(rightParam, underType);
            Lazy<Func<T, T, T>> CreateLazyOp(Func<Expression, Expression, BinaryExpression> opFunc) =>
            new Lazy<Func<T, T, T>>(() =>
            {
                var op = opFunc(leftCast, rightCast);
                var resultCast = Expression.ConvertChecked(op, enumType);
                var l = Expression.Lambda<Func<T, T, T>>(resultCast, leftParam, rightParam);
                return l.Compile();
            });
            LazyOr = CreateLazyOp(Expression.Or);
            LazyAnd = CreateLazyOp(Expression.And);
        }
    }
}