2016-01-22 85 views
0

我正在嘗試創建一個使用Linq聚合器函數(如Sum,Average和Count)的方法。我有以下代碼:動態使用LINQ聚合器

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(generic.Invoke(NULL,parametersArray));」。

錯誤:

Object of type 'Manager.Business.Tests.Foo[]'cannot be converted to object of type 'System.Collections.Generic.IEnumerable`1[System.Linq.Enumerable]'.

我能做些什麼?

回答

2

首先,這條線

Type enumerableType = typeof(Enumerable); 

應該

Type enumerableType = typeof(T); 

這是因爲MakeGenericMethod參數預期的實際泛型類型參數,這些參數在Enumerable.Sum<TSource>(this IEnumerable<TSource>過載情況下是TSource,即類型可枚舉元素的元素

其次,用於發現聚集泛型方法的標準是不夠的,因爲比如有很多Sum<TSource>重載 - 爲intdoubledecimal等你需要的是找到double過載。

三,功能非常低效。將爲列表的每個元素調用selector func(在您的代碼中稱爲expression)。不僅如此,您使用反射來獲得價值,但也反映找到財產本身。至少你應該把GetProperty移到外面。

var result = list.AggregateDynamic("FooValue", "Sum"); 

UPDATE:作爲

所有這些問題都可以很容易地通過使用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(); 
    } 
} 

用法來解決在評論中正確地指出,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; 
    } 
} 
+1

我很欣賞表達式樹的用法,但是要注意編譯它們會有不俗的表現。如果你在生產中使用它,我會看看是否有一個體面的方法來緩存生成的方法。 – willaien

+0

@willaien好點!我做了一個測試,是的,沒有緩存,我們似乎失去了所有的好處,因爲你指出了大量的編譯開銷。但是在添加一個緩存之後(這很簡單),所有事情都應該是這樣,而且這個方法比其他任何一個都快。但是,讓我問一些問題 - 你真的需要'Convert.ToDouble'還是僅僅因爲反思 - 即屬性應該是'雙'? –

+0

我不知道他爲什麼使用Convert.ToDouble,除非他只是隨意的想要double而不管實際的類型(int32或double) – willaien

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)); 
+0

而且他可能需要更強健地檢查方法簽名。 – usr

+0

@usr是的,當然,這只是一個想法。 –

3

讓我們退後一步並看看問題:(我猜)你想支持編譯時已知類型的聚合函數(因此是泛型的),但不知道什麼屬性或集合f他們會選擇他們。

我建議你採取另一種方法查找的功能和簡單地使用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"); 
    } 
} 

試圖使人們有可能找到所有的聚合函數正確地爲他們每個人的使用反思將是一場噩夢。你可以讓編譯器用這個來解決凌亂的零件。

+0

有趣的思維方式。雖然我發佈了自己的答案,但我喜歡你對問題的(不同的)看法! +1 –

+0

這是更好的代碼,但不清楚他是否想採取這種方法。 +1雖然。 – usr