6

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

Andrew Jocelyn
  • 399
  • 1
  • 3
  • 18
  • 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`),. – Chayim Friedman Dec 05 '18 at 16:48
  • Too. It's also for structures. But Iam checking wth `System.Enum`, just a moment... – Chayim Friedman Dec 05 '18 at 16:50
  • You could handle each individual case with `if(underlyingType == typeof(byte)) {//convert to byte and aggregate }` – FCin Dec 05 '18 at 17:36
  • public static T ToCombined(this IEnumerable 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); } – Andrew Jocelyn Dec 05 '18 at 17:40
  • don't use sum, use bitwise or then it won't overflow. – BurnsBA Dec 05 '18 at 17:45
  • Sorry, I edit the original too. – Andrew Jocelyn Dec 05 '18 at 17:45
  • Why do you need to convert it to an int first, why not just aggregate the flag enum? – Tod Aug 23 '22 at 16:15

4 Answers4

11

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.

madreflection
  • 4,744
  • 3
  • 19
  • 29
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.

Doctor Jones
  • 21,196
  • 13
  • 77
  • 99
  • 1
    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. – madreflection Jan 09 '19 at 00:51
  • 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. – madreflection Jan 09 '19 at 00:56
  • 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. – Doctor Jones Jan 09 '19 at 09:35
  • With this implementation, I'd ensure that this class is registered as a singleton during your dependency injection setup, then you don't need to worry about it. – Doctor Jones Jan 09 '19 at 09:37
  • 1
    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 Jan 11 '19 at 09:11
  • 1
    @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. – Doctor Jones Jan 11 '19 at 09:37
1

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
}
Scott Hannen
  • 27,588
  • 3
  • 45
  • 62
1

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);
    });
Sweeper
  • 213,210
  • 22
  • 193
  • 313