2013-11-23 46 views
4

我一直在嘗試很長時間來找到一個「乾淨」模式來處理.SelectMany與匿名類型,當你不總是想要返回結果。我最常見的使用案例如下所示:SelectMany匿名類型和跳過迭代

  1. 我們有一個我想要進行報告的客戶列表。
  2. 每位客戶的數據都駐留在一個單獨的數據庫中,因此我做了一個並行處理.SelectMany
  3. 在每個lambda表達式中,我收集客戶對最終報告的結果。
  4. 如果一個特定的客戶應該被跳過,我需要返回一個空列表。
  5. 爲了快速報告我經常鞭打這些,所以我更喜歡匿名類型。

例如,邏輯可能看起來是這樣的:

//c is a customer 
var context = GetContextForCustomer(c); 
// look up some data, myData using the context connection 
if (someCondition) 
    return myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 }); 
else 
    return null; 

這可以被實現爲foreach語句:

var results = new List<WhatType?>(); 
foreach (var c in customers) { 
    var context = GetContextForCustomer(c); 
    if (someCondition) 
    results.AddRange(myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 })); 
} 

或者,它可以與.SelectMany是實施用.Where預過濾:

customers 
    .Where(c => someCondition) 
    .AsParallel() 
    .SelectMany(c => { 
    var context = GetContextForCustomer(c); 
    return myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 }); 
    }) 
    .ToList(); 

這兩種方法都存在問題。 foreach解決方案需要初始化List來存儲結果,並且您必須定義類型。 .SelectMany.Where通常不切實際,因爲someCondition的邏輯相當複雜,並且取決於某些數據查找。所以,我的理想的解決方案將是這個樣子:

customers 
    .AsParallel() 
    .SelectMany(c => { 
    var context = GetContextForCustomer(c); 
    if (someCondition) 
     return myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 }); 
    else 
     continue? return null? return empty list? 
    }) 
    .ToList(); 

我放在else線什麼跳過一個返回值?該解決方案中沒有我可以拿出工作或理想:

  1. continue不能編譯,因爲它不是一個積極的foreach循環
  2. return null導致NRE
  3. return空單需要我初始化再次列出匿名類型。

有沒有辦法完成上述的乾淨,簡單,整潔,並滿足我所有(挑剔)的要求?

回答

1

您可能會返回一個空的Enumerable<dynamic>。下面是一個例子(儘管沒有你的客戶和someCondition,因爲我不知道他們是什麼,但你的例子相同的一般形式的):

new int[] { 1, 2, 3, 4 } 
    .AsParallel() 
    .SelectMany(i => { 
     if (i % 2 == 0) 
      return Enumerable.Repeat(new { i, squared = i * i }, i); 
     else 
      return Enumerable.Empty<dynamic>(); 
     }) 
    .ToList(); 

所以,隨着您的對象和someCondition,它看起來像

customers 
    .AsParallel() 
    .SelectMany(c => { 
     var context = GetContextForCustomer(c); 
     if (someCondition) 
      return myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 }); 
     else 
      return Enumerable.Empty<dynamic>(); 
     }) 
    .ToList(); 
+0

這將導致大量的動態調用,並反過來反射。在性能方面,這不是最好的選擇。 – Athari

+1

好點。但他也表示他只是需要這個來產生一些快速報告。 –

+0

我最喜歡這個解決方案,因爲它很短,不需要任何自定義的擴展方法。我嘗試了'Enumerable.Empty()',但它不能確定類型,沒有''。謝謝!! – mellamokb

1

您可以嘗試以下操作:

customers 
    .AsParallel() 
    .SelectMany(c => { 
    var context = GetContextForCustomer(c); 
    if (someCondition) 
     return myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 }); 
    else 
     return Enumerable.Empty<int>().Select(x => new { CustomerID = 0, X1 = "defValue", X2 = "defValue" }); 
    }) 
    .ToList(); 

所有匿名類型具有相同的屬性集(相同的名稱和類型)被合併成一個編譯器一個匿名類。這就是爲什麼您的SelectEnumerable.Empty將返回相同的T

+0

這個解決方案與'foreach'和'return'空列表基本相同 - 您必須動態構建一個匿名類型的列表。例如,我知道諸如[this one]這樣的解決方案(http://stackoverflow.com/questions/612689)。它有效,但現在我有兩個屬性列表,如果我決定在即時報告中添加/刪除屬性,我必須更新這兩個屬性列表。 – mellamokb

1

不知道什麼someConditionmyData樣子......

你爲什麼不只是SelectWhere上下文以及:

customers 
.Select(c => GetContextForCustomer(c)) 
.Where(ctx => someCondition) 
.SelectMany(ctx => 
    myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 }); 

編輯:我只是意識到你需要同時customercontext進一步落實,所以你可以這樣做:

customers 
.Select(c => new { Customer = c, Context = GetContextForCustomer(c) }) 
.Where(x => someCondition(x.Context)) 
.SelectMany(x => 
    myData.Select(d => new { CustomerID = x.Customer, X1 = d.x1, X2 = d.x2 }); 
+0

我意識到我的例子並不正義。實際上,'someCondition'和'myData'背後的邏輯是20-30行代碼,並且正在迅速發展,所以我最喜歡'.SelectMany'。 – mellamokb

1

您可以創建自己的SelectMany LINQ方法的variarion其中支持null s:

public static class EnumerableExtensions 
{ 
    public static IEnumerable<TResult> NullableSelectMany<TSource, TResult> (
     this IEnumerable<TSource> source, 
     Func<TSource, IEnumerable<TResult>> selector) 
    { 
     if (source == null) 
      throw new ArgumentNullException("source"); 
     if (selector == null) 
      throw new ArgumentNullException("selector"); 
     foreach (TSource item in source) { 
      IEnumerable<TResult> results = selector(item); 
      if (results != null) { 
       foreach (TResult result in results) 
        yield return result; 
      } 
     } 
    } 
} 

現在您可以在selector lambda中返回null

+0

這是一個不錯的方法。我考慮的另一件事是'.Select(...)。其中(x => x!= null).SelectMany(x => x)'。不是很漂亮,但不需要我在任何地方指定匿名類型。 – mellamokb

+1

@mellamokb ReSharper建議我實際上。 :)我也有許多'IEnumerable'擴展方法,所以我會寫'.Select(...)。WhereNotNull()。Flatten()'我自己,這對我來說看起來非常棒。 – Athari

0

接受的答案返回dynamic。最簡潔的方法是將過濾邏輯轉換爲Where,這使得linq上下文看起來更好。既然你明確地解決了這個問題,而且我不是一個在linq調用中寫成多行的代表的粉絲,我會嘗試this,但是可以爭辯說它更冒險。

var results = new 
{ 
    customerID = default(int), //notice the casing of property names 
    x1 = default(U), //whatever types they are 
    x2 = default(V) 
}.GetEmptyListOfThisType(); 

foreach (var customerID in customers) { 
    var context = GetContextForCustomer(customerID); 
    if (someCondition) 
    results.AddRange(myData.Select(x => new { customerID, x.x1, x.x2 })); 
} 

public static List<T> GetEmptyListOfThisType<T>(this T item) 
{ 
    return new List<T>(); 
} 

注意適當使用屬性名稱是根據其他變量名,所以你不要有寫的屬性名稱在Select呼叫的第二時間。