Я хочу передать 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);
}
Спасибо, Андрей
Это решение превращает преобразования в базовый тип и обратно в тип перечисления в выражении.
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
все еще остается.
Ответ @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, вы не понесете снижения производительности нескольких компиляций.
null
а затем присваивать их в инициализаторе типа - вы получаете флаг beforefieldinit
, который имеет преимущества в производительности для вызова инициализаторов типов.
Если вы уверены, что базовый тип будет byte
, int
или long
то просто отбросьте каждое значение до long
или их и верните результат в T
Поскольку базовый тип неизвестен, он преобразует их все в 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
}
|
(дляobject
не определенoperator |()
).System.Enum
, минутку ...