Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Method to combine a generic list of enum values to a single value

Tags:

c#

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

like image 581
Andrew Jocelyn Avatar asked Dec 05 '18 16:12

Andrew Jocelyn


2 Answers

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.

like image 161
madreflection Avatar answered Sep 29 '22 03:09

madreflection


@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.

like image 26
Doctor Jones Avatar answered Sep 29 '22 03:09

Doctor Jones