我正在尝试创建一个使用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");
当我运行此代码时,它会在此行上抛出一个错误“return Convert.ToDouble(generic.Invoke(null,parametersArray));”。
错误:
'Manager.Business.Tests.Foo []'类型的对象无法转换为'System.Collections.Generic.IEnumerable`1 [System.Linq.Enumerable]'类型的对象。
我该怎么办?
答案 0 :(得分:3)
问题在于:
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));
答案 1 :(得分:3)
让我们退一步看看问题:(我猜)你想支持在编译时已知的类型的聚合函数(因此是通用的),但是不知道他们将使用什么属性或聚合函数选择。
我建议您采用另一种方法来查找函数,并简单地使用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");
}
}
尝试使用反射为每个人正确地找到所有聚合函数是一件噩梦。你可以让编译器为你解决这个混乱的部分。
答案 2 :(得分:2)
首先,这一行
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;
}
}