C # Метод для объединения общего списка значений перечисления в одно значение

2

Я хочу передать IEnumerable<T> значений enum (enum имеет атрибут Flags) и возвращает агрегированное значение. Метод ниже работает, но только если перечисление использует тип Int32 по умолчанию. Если он использует byte или Int64 это не сработает.

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;
}

Я знаю, что могу получить базовый тип:

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

но я не вижу, как использовать его в методе. Как я могу сделать метод расширения, чтобы он мог обрабатывать список любых enums с атрибутом flags?

Лучше, но может быть проблемой с действительно большими 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);
}

Спасибо, Андрей

  • 1
    @RyanWilson Нет, так как это C # Generics, а не C ++ Templates, вы не сможете выполнить | (для object не определен operator |() ).
  • 0
    Слишком. Это также для структур. Но я проверяю System.Enum , минутку ...
Показать ещё 4 комментария
Теги:

4 ответа

4

Это решение превращает преобразования в базовый тип и обратно в тип перечисления в выражении.

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);
}

Обратите внимание, что я использовал ограничение типа С# 7.3 Enum. Если вы не используете С# 7.3, ограничение struct с IsEnum проверки IsEnum все еще остается.

1

Ответ @madreflection великолепен, но он компилирует выражение каждый раз, когда вызывается метод, что значительно снижает производительность.

Преимущество компиляции выражений в том, что, если вы кешируете полученный делегат, вы не получите никакого снижения производительности по сравнению с отражением. Мне было стыдно упустить эту возможность, поэтому я сделал следующее, основываясь на его ответе.

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
        );
    }
}

Ваш метод ToCombined затем заменяется следующими перегрузками:

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 });

Пока вы держитесь за один и тот же экземпляр GenericBitwise, вы не понесете снижения производительности нескольких компиляций.

  • 0
    Просто сейчас вижу это. Во-первых, спасибо. Во-вторых, нет сомнений, он может быть оптимизирован. Я был больше сосредоточен на том, чтобы избегать нетривиальных преобразований типов, сохраняя при этом его универсальность. Хорошая работа, подняв его на новый уровень.
  • 0
    Что касается привязки к тому же экземпляру, сделайте его статическим классом, и это станет проблемой. Затем, если вы перемещаете инициализации в объявления - вместо того, чтобы начинать с null а затем присваивать их в инициализаторе типа - вы получаете флаг beforefieldinit , который имеет преимущества в производительности для вызова инициализаторов типов.
Показать ещё 4 комментария
1

Если вы уверены, что базовый тип будет byte, int или long то просто отбросьте каждое значение до long или их и верните результат в T

0

Поскольку базовый тип неизвестен, он преобразует их все в 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
}

Ещё вопросы

Сообщество Overcoder
Наверх
Меню