7

I want to pass a IEnumerable<T> of enum values (enum has the Flags attribute) and return the aggregate value. The method below works but only if the enum uses the default Int32 type. If it uses byte or Int64 it won't work.

public static T ToCombined<T>(this IEnumerable<T> list) where T : struct { if (!typeof(T).IsEnum) throw new ArgumentException("The generic type parameter must be an Enum."); var values = list.Select(v => Convert.ToInt32(v)); var result = values.Aggregate((current, next) => current | next); return (T)(object)result; } 

I know I can get the underlying type:

Type enumType = typeof(T); Type underlyingType = Enum.GetUnderlyingType(enumType); 

but I don't see how to make use of it in the method. How can I make the extension method so it can handle a list of any enums with the flags attribute?

Better but might be a problem with really big UInts

public static T ToCombined<T>(this IEnumerable<T> list) where T : struct { if (!typeof(T).IsEnum) throw new ArgumentException("The generic type parameter must be an Enum."); var values = list.Select(v => Convert.ToInt64(v)); var result = values.Sum(); var underlyingType = Enum.GetUnderlyingType(typeof(T)); return (T)Convert.ChangeType(result, underlyingType); } 

Thanks Andrew

7
  • 1
    @RyanWilson No, since this is C# Generics, and not C++ Templates, you won't can to do the | (there is not operator |() defined for object),. Commented Dec 5, 2018 at 16:48
  • Too. It's also for structures. But Iam checking wth System.Enum, just a moment... Commented Dec 5, 2018 at 16:50
  • You could handle each individual case with if(underlyingType == typeof(byte)) {//convert to byte and aggregate } Commented Dec 5, 2018 at 17:36
  • public static T ToCombined<T>(this IEnumerable<T> list) where T : struct { if (!typeof(T).IsEnum) throw new ArgumentException("The generic type parameter must be an Enum."); var values = list.Select(v => Convert.ToInt64(v)); var result = values.Sum(); var underlyingType = Enum.GetUnderlyingType(typeof(T)); return (T)Convert.ChangeType(result, underlyingType); } Commented Dec 5, 2018 at 17:40
  • don't use sum, use bitwise or then it won't overflow. Commented Dec 5, 2018 at 17:45

4 Answers 4

13

This solution inlines the conversions to the underlying type and back to the enum type in an expression.

public static T ToCombined<T>(this IEnumerable<T> list) where T : Enum { Type underlyingType = Enum.GetUnderlyingType(typeof(T)); var currentParameter = Expression.Parameter(typeof(T), "current"); var nextParameter = Expression.Parameter(typeof(T), "next"); Func<T, T, T> aggregator = Expression.Lambda<Func<T, T, T>>( Expression.Convert( Expression.Or( Expression.Convert(currentParameter, underlyingType), Expression.Convert(nextParameter, underlyingType) ), typeof(T) ), currentParameter, nextParameter ).Compile(); return list.Aggregate(aggregator); } 

Note that I've used the C# 7.3 Enum type constraint. If you're not using C# 7.3, the struct constraint with the IsEnum check is still the way to go.

Sign up to request clarification or add additional context in comments.

Comments

6

@madreflection's answer is great, but it compiles the expression every time the method is called, which will give you a significant performance hit.

The advantage of compiling expressions, is, if you cache the resulting delegate, you end up with no performance penalty, when compared to reflection. It seemed a shame to miss out on this opportunity, so I made the following, based on his answer.

public class GenericBitwise<TFlagEnum> where TFlagEnum : Enum { private readonly Func<TFlagEnum, TFlagEnum, TFlagEnum> _and = null; private readonly Func<TFlagEnum, TFlagEnum> _not = null; private readonly Func<TFlagEnum, TFlagEnum, TFlagEnum> _or = null; private readonly Func<TFlagEnum, TFlagEnum, TFlagEnum> _xor = null; public GenericBitwise() { _and = And().Compile(); _not = Not().Compile(); _or = Or().Compile(); _xor = Xor().Compile(); } public TFlagEnum And(TFlagEnum value1, TFlagEnum value2) => _and(value1, value2); public TFlagEnum And(IEnumerable<TFlagEnum> list) => list.Aggregate(And); public TFlagEnum Not(TFlagEnum value) => _not(value); public TFlagEnum Or(TFlagEnum value1, TFlagEnum value2) => _or(value1, value2); public TFlagEnum Or(IEnumerable<TFlagEnum> list) => list.Aggregate(Or); public TFlagEnum Xor(TFlagEnum value1, TFlagEnum value2) => _xor(value1, value2); public TFlagEnum Xor(IEnumerable<TFlagEnum> list) => list.Aggregate(Xor); public TFlagEnum All() { var allFlags = Enum.GetValues(typeof(TFlagEnum)).Cast<TFlagEnum>(); return Or(allFlags); } private Expression<Func<TFlagEnum, TFlagEnum>> Not() { Type underlyingType = Enum.GetUnderlyingType(typeof(TFlagEnum)); var v1 = Expression.Parameter(typeof(TFlagEnum)); return Expression.Lambda<Func<TFlagEnum, TFlagEnum>>( Expression.Convert( Expression.Not( // ~ Expression.Convert(v1, underlyingType) ), typeof(TFlagEnum) // convert the result of the tilde back into the enum type ), v1 // the argument of the function ); } private Expression<Func<TFlagEnum, TFlagEnum, TFlagEnum>> And() { Type underlyingType = Enum.GetUnderlyingType(typeof(TFlagEnum)); var v1 = Expression.Parameter(typeof(TFlagEnum)); var v2 = Expression.Parameter(typeof(TFlagEnum)); return Expression.Lambda<Func<TFlagEnum, TFlagEnum, TFlagEnum>>( Expression.Convert( Expression.And( // combine the flags with an AND Expression.Convert(v1, underlyingType), // convert the values to a bit maskable type (i.e. the underlying numeric type of the enum) Expression.Convert(v2, underlyingType) ), typeof(TFlagEnum) // convert the result of the AND back into the enum type ), v1, // the first argument of the function v2 // the second argument of the function ); } private Expression<Func<TFlagEnum, TFlagEnum, TFlagEnum>> Or() { Type underlyingType = Enum.GetUnderlyingType(typeof(TFlagEnum)); var v1 = Expression.Parameter(typeof(TFlagEnum)); var v2 = Expression.Parameter(typeof(TFlagEnum)); return Expression.Lambda<Func<TFlagEnum, TFlagEnum, TFlagEnum>>( Expression.Convert( Expression.Or( // combine the flags with an OR Expression.Convert(v1, underlyingType), // convert the values to a bit maskable type (i.e. the underlying numeric type of the enum) Expression.Convert(v2, underlyingType) ), typeof(TFlagEnum) // convert the result of the OR back into the enum type ), v1, // the first argument of the function v2 // the second argument of the function ); } private Expression<Func<TFlagEnum, TFlagEnum, TFlagEnum>> Xor() { Type underlyingType = Enum.GetUnderlyingType(typeof(TFlagEnum)); var v1 = Expression.Parameter(typeof(TFlagEnum)); var v2 = Expression.Parameter(typeof(TFlagEnum)); return Expression.Lambda<Func<TFlagEnum, TFlagEnum, TFlagEnum>>( Expression.Convert( Expression.ExclusiveOr( // combine the flags with an XOR Expression.Convert(v1, underlyingType), // convert the values to a bit maskable type (i.e. the underlying numeric type of the enum) Expression.Convert(v2, underlyingType) ), typeof(TFlagEnum) // convert the result of the OR back into the enum type ), v1, // the first argument of the function v2 // the second argument of the function ); } } 

Your ToCombined method is then replaced by the following overloads:

var genericBitwise = new GenericBitwise<FlagType>(); var combinedAnd = genericBitwise.And(new[] { FlagType.First, FlagType.Second, FlagType.Fourth }); var combinedOr = genericBitwise.Or(new[] { FlagType.First, FlagType.Second, FlagType.Fourth }); 

As long as you hang onto the same instance of GenericBitwise, you won't incur the performance penalty of multiple compiles.

6 Comments

Just now seeing this. First, thanks. Second, no question, it could stand to be optimized. I was more focused on avoiding non-trivial type conversions while keeping it truly generic. Nice job taking it to the next level.
As for hanging on to the same instance, make it a static class and that becomes a non-issue. Then, if you move the initializations to the declarations -- instead of starting with null and then assigning them in the type initializer -- you get the beforefieldinit flag, which has performance benefits for calling type initializers.
Thanks, I considered that, but I prefer to avoid statics (due to our coding standards) to keep everything dependency injection friendly, and testable. My actual class implements an interface (IGenericBitwise), so it can easily be mocked. That makes anything that consumes this class testable, because we can mock the IGenericBitwise instead of using the concrete implementation of GenericBitwise. If you don't need unit tests, then extension methods in a static class would be much better.
After doing some testing, I discovered that Negate is actually the arithmetic negative, which not the correct operator; Not is the bitwise negative. I tried suggesting an edit but it was rejected with "This edit deviates from the original intent of the post..." which is absolute nonsense.
@madreflection, I was still in bed when the edit was suggested, otherwise I'd have approved it. Fortunately as post owner, I was able to approve it even after it was rejected. Thanks for spotting the error, and improving my post.
|
2

Because the underlying type is unknown, this converts them all to Int64.

public static class FlagsEnumExtensions { public static TEnum GetAggregate<TEnum>(this IEnumerable<TEnum> values) where TEnum : Enum { if (!typeof(TEnum).GetCustomAttributes<FlagsAttribute>().Any()) throw new ArgumentException($"{typeof(TEnum)} does not have the Flags attribute"); var flags = Enum.GetValues(typeof(TEnum)).Cast<object>().Select(Convert.ToInt64); var valuesAsLong = values.Select(v => Convert.ToInt64(v)); var aggregated = flags.Where(flag => valuesAsLong.Any(value => (value & flag) == flag)) .Aggregate<long, long>(0, (current, flag) => current | flag); return (TEnum)Enum.ToObject(typeof(TEnum), aggregated); } } [TestClass] public class EnumAggregateTests { [TestMethod] public void AggregatesByteEnum() { var values = new ByteEnum[] {ByteEnum.One, ByteEnum.Eight}; var aggregate = values.GetAggregate(); Assert.AreEqual(aggregate, ByteEnum.One | ByteEnum.Eight); } [TestMethod] public void AggregatesUint64Enum() { var values = new Uint64Enum[] { Uint64Enum.One,Uint64Enum.Eight}; var aggregate = values.GetAggregate(); Assert.AreEqual(aggregate, Uint64Enum.One | Uint64Enum.Eight); } } [Flags] public enum ByteEnum : Byte { One = 1, Two = 2, Four = 4, Eight = 8 } [Flags] public enum Uint64Enum : UInt64 { One = 1, Two = 2, Four = 4, Eight = 8 } 

Comments

0

According to here and here, you can convert between a generic enum type parameter and a numeric type using the System.Runtime.CompilerServices.Unsafe.As method. This is "unsafe", in the sense that there is no check to see if the enum's underlying type is actually the same as the numeric type, which is exactly what we want.

By using this, we can convert all the enum values to a long (so that every underlying enum type can fit), then do the Aggregate, then convert it back to the enum type.

public static T CombinedOr<T>(IEnumerable<T> enums) where T: Enum { // you can check for FlagsAttribute here or other optional things... var result = enums // will return 0 if empty, if undesirable, you can easily add a check for Any() first .Aggregate(0L, (acc, t) => acc | Unsafe.As<T, long>(ref t)); return Unsafe.As<long, T>(ref result); } 

I've used the two-argument overload of Aggregate, rather than the one-argument overload. The latter technically also works, but it's 3 calls to Unsafe.As rather than 2.

return enums .Aggregate((t1, t2) => { var temp = Unsafe.As<T, long>(ref t1) | Unsafe.As<T, long>(ref t2); return Unsafe.As<long, T>(ref temp); }); 

1 Comment

I used this approach months ago. Today, random issues were surfacing in the staging environment only, and it took me 4 full hours to realize it was Unsafe.As to blame. This is NOT production-safe code! Byte-based enums (and integer-based as well) can have some memory leftover converted to random values, because of compiler/JIT optimizations.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.