2013-11-23 4 views
4

Я долго пытался найти «чистый» шаблон для обработки .SelectMany с анонимными типами, когда вы не всегда хотите вернуть результат. Мой наиболее распространенный случай использования выглядит так:SelectMany Anonymous Type и Skip Iterations

  1. У нас есть список клиентов, с которыми я хочу вести отчет.
  2. Данные каждого пользователя находятся в отдельной базе данных, поэтому я делаю параллель .SelectMany
  3. В каждом выражении лямбда я собираю результаты для клиента в отношении окончательного отчета.
  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; 

Это может быть реализовано как Еогеасп заявление:

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(); 

Все анонимные типы с одинаковым набором свойств (те же имена и типы) объединяются в один один анонимный класс компилятором. Вот почему и ваши Select, и один на Enumerable.Empty вернут то же самое T.

+0

Это решение в основном в той же лодке, что и 'foreach' и' return' empty list - вам нужно динамически построить список анонимного типа. Например, я знаю такие решения, как [этот] (http://stackoverflow.com/questions/612689). Он работает, но теперь у меня есть два списка свойств, которые я должен обновить, если я решит добавить/удалить свойство в своем отчете «на лету». – mellamokb

1

не зная, что someCondition и myData взгляд, как ...

Почему вы не просто Select и Where контексты, а также:

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

EDIT: Я только что понял, что вам нужно носить как в customer и context дальше, так что вы можете сделать это:

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

Вы можете создать свой собственный variarion из SelectMany метода LINQ, который опоры 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; 
      } 
     } 
    } 
} 

Теперь вы можете вернуть null в лямбда selector.

+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 вызова.