Reputation: 11177
I am trying to implement functionality in a way that it was specified here:
However, I'm trying to do it as generic method to be used as an extension:
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;
}
Problem is however that this line:
merged = merged | value;
Will not compile. Message I'm getting is:
Operator '|' cannot be applied to operands of type 'TEnum?' and 'TEnum'.
Is it possible to write this generic method that will convert array of enum values to flags enum?
Upvotes: 3
Views: 1279
Reputation: 3539
1 line in Linq, no need for a helper method:
var result = enumArray.Aggregate ((TestEnum)0, (a, b) => a | b);
But if you don't want to remember this code, an extension method is also possible:
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;
}
Proof of concept:
[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);
}
Output:
result = First | Second | Third | Bit40
result2 = First | Second | Third | Bit40
result .ToString ("X") -> "0000010000000007"
result2.ToString ("X") -> "0000010000000007"
Upvotes: 3
Reputation: 46490
I needed this ability and was glad to find Marc's answer. I took a stab at using some newer language features. Here's what I came up with:
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);
}
}
}
Upvotes: 1
Reputation: 1064114
There are a couple of issues here, but the biggest is that generics does not support operators - and |
is an operator. You can hack around it via object
, but then you have boxing. Here's what I would do - it generates some dynamic IL per-enum-type (once only), and uses that to do a direct "or" without boxing. Note that it also uses 0
for the default return (far more expected, IMO), and avoids an explicit Count()
, as that can be unpredictably expensive, and can break the enumerator (you can't guarantee that you can enumerate data more than once):
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 });
}
}
Edit: if you really must return null
for the "null or empty" case, then you could use the following tweak - but I emphasize: IMO this is an incorrect implementation - it would be more correct to simply return 0
(aka default(TEnum)
) for this scenario.
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;
}
}
What this does is:
Upvotes: 4