动态使用LINQ聚合器

时间:2016-01-22 19:52:06

标签: c# linq reflection

我正在尝试创建一个使用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]'类型的对象。

我该怎么办?

3 个答案:

答案 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>intdouble等有很多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;
    }
}