动态使用 LINQ 聚合器
本文关键字:LINQ 动态 | 更新日期: 2023-09-27 18:31:39
我正在尝试创建一个使用 Linq 聚合器函数的方法,如 Sum、Average 和 Count。我有以下代码:
private double AgreggateDynamic<T>(IEnumerable<T> list, string propertyName, string func)
{
//Already tried this
//IEnumerable<T> listEnum = list.ToList();
Type enumerableType = typeof(Enumerable);
MethodInfo sumMethod = typeof(Enumerable).GetMethods().First(
m => m.Name == func
&& m.IsGenericMethod);
MethodInfo generic = sumMethod.MakeGenericMethod(enumerableType);
Func<T, double> expression = x => Convert.ToDouble(x.GetType().GetProperty(propertyName).GetValue(x, null));
object[] parametersArray = new object[] { list, expression };
return Convert.ToDouble(generic.Invoke(null, parametersArray));
}
AgreggateDynamic(list, "FooValue", "Sum");
当我运行此代码时,它会在此行上抛出错误"返回 Convert.ToDouble(通用。Invoke(null, parametersArray));".
错误:
类型为"Manager.Business.Tests.Foo[]"的对象不能转换为类型为"System.Collections.Generic.IEnumerable'1[System.Linq.Enumerable]"的对象。
我能做什么?
问题就在这里:
MethodInfo sumMethod = typeof(Enumerable).GetMethods().First(
m => m.Name == func
&& m.IsGenericMethod);
您首先从无法占用Func<T, double>
的聚合函数的重载中获得
试试这个:
MethodInfo sumMethod = typeof(Enumerable).GetMethods().First(
m => m.Name == func
&& m.IsGenericMethod
&& m.ReturnType == typeof(double));
让我们退后一步,看看这个问题:(我猜)你想支持编译时已知的类型(因此是泛型)的聚合函数,但不知道他们会选择什么属性或聚合函数。
我建议你采取另一种方法来查找函数,并简单地使用 switch 语句,如下所示:
private double AggregateDynamic<T>(IEnumerable<T> list, string propertyName, string func)
{
var propertyInfo = typeof(T).GetProperty(propertyName);
Func<T, double> propertyFunction = x => Convert.ToDouble(propertyInfo.GetValue(x, null));
switch (func)
{
case "Sum":
return list.Sum(propertyFunction);
case "Average":
return list.Average(propertyFunction);
case "Count":
return list.Count();
case "Max":
return list.Max(propertyFunction);
default:
throw new ArgumentException("Unknown aggregate function");
}
}
试图使用反射为每个函数正确找到所有聚合函数将是一场噩梦。你可以让编译器用这个为你解决混乱的部分。
首先,这一行
Type enumerableType = typeof(Enumerable);
应该是
Type enumerableType = typeof(T);
这是因为MakeGenericMethod
参数需要实际的泛型类型参数,在Enumerable.Sum<TSource>(this IEnumerable<TSource>
重载的情况下,泛型类型参数是TSource
的,即可枚举元素的类型。
其次,用于查找聚合泛型方法的标准是不够的,因为例如有许多Sum<TSource>
重载 - 对于int
、double
、decimal
等。您需要的是找到double
的过载。
第三,功能效率很低。将为列表中的每个元素调用selector
func(在代码中称为 expression
)。不仅使用反射来获取值,还使用反射来查找属性本身。至少你应该把GetProperty
移到外面。
所有这些问题都可以通过使用System.Linq.Expressions
构建整个东西来轻松解决,编译委托并调用它,就像这样
public static class DynamicAggregator
{
public static double AggregateDynamic<T>(this IEnumerable<T> source, string propertyName, string func)
{
return GetFunc<T>(propertyName, func)(source);
}
static Func<T, double> GetFunc<T>(string propertyName, string func)
{
return BuildFunc<T>(propertyName, func);
}
static Func<T, double> BuildFunc<T>(string propertyName, string func)
{
var source = Expression.Parameter(typeof(IEnumerable<T>), "source");
var item = Expression.Parameter(typeof(T), "item");
Expression value = Expression.PropertyOrField(item, propertyName);
if (value.Type != typeof(double)) value = Expression.Convert(value, typeof(double));
var selector = Expression.Lambda<Func<T, double>>(value, item);
var methodCall = Expression.Lambda<Func<IEnumerable<T>, double>>(
Expression.Call(typeof(Enumerable), func, new Type[] { item.Type }, source, selector),
source);
return methodCall.Compile();
}
}
用法:
var result = list.AggregateDynamic("FooValue", "Sum");
更新:正如评论中正确指出的那样,Expression.Compile
具有显着的性能开销,这基本上扼杀了这种方法的好处。但是,添加缓存编译的委托非常容易,然后一切都是应该的。
为此,首先我通过分离方法构建/编译部分来稍微重构初始代码。然后,通过修改类来添加缓存非常简单,如下所示:
static readonly Dictionary<Tuple<Type, string, string>, Delegate> funcCache = new Dictionary<Tuple<Type, string, string>, Delegate>();
static Func<IEnumerable<T>, double> GetFunc<T>(string propertyName, string func)
{
var cacheKey = Tuple.Create(typeof(T), propertyName, func);
Delegate cachedValue;
lock (funcCache)
{
if (funcCache.TryGetValue(cacheKey, out cachedValue))
return (Func<IEnumerable<T>, double>)cachedValue;
var method = BuildFunc<T>(propertyName, func);
funcCache.Add(cacheKey, method);
return method;
}
}