2010-08-08 16 views
2

Я недавно был в ситуации, когда мне нужно было выполнить операцию, сгруппированную медленно, получая запрос Linq.«Lazy» GroupBy with Linq

Теперь groupBy теряет свою ленивость, это означает, что вам нужно дождаться завершения всей последовательности, пока вы не получите какие-либо группы. Это для меня логически кажется не лучшим решением, так как группа может быть возвращена сразу после ее первой встречи.

Я написал следующий код, который, кажется, работает нормально, и я ищу подводные камни и общие улучшения, а также мысли о самой концепции (например, может/должен метод groupBy возвращать группы как можно скорее) ,

public static IEnumerable<KeyValuePair<R, IEnumerable<T>>> GroupByLazy<T, R>(this IEnumerable<T> source, Func<T, R> keySelector) 
     { 
      var dic = new Dictionary<R, BlockingCollection<T>>(); 
      foreach (var item in source) 
      { 
       var Key = keySelector(item); 
       BlockingCollection<T> i; 
       if (!dic.TryGetValue(Key, out i)) 
       { 
        i = new BlockingCollection<T>(); 
        i.Add(item); 
        dic.Add(Key, i); 
        yield return new KeyValuePair<R, IEnumerable<T>>(Key, i); 
       } 
       else i.TryAdd(item); 
      } 
      // mark all the groups as completed so that enumerations of group-items can finish 
      foreach (var groupedValues in dic.Values) 
       groupedValues.CompleteAdding(); 
     } 

Простой тест:

var slowIE = Observable.Interval(TimeSpan.FromSeconds(1)).ToEnumerable().Take(10); 
      var debug = slowIE.Do(i => Console.WriteLine("\teval " + i)); 

      var gl = debug.GroupByLazy(i => i % 2 == 0); 

      var g = debug.GroupBy(i => i % 2 == 0); 

      Console.WriteLine("Lazy:"); 
      gl.Run(i => Console.WriteLine("Group returned: " + i.Key)); 
      Console.WriteLine(gl.Single(i => i.Key).Value.Count()); 

      Console.WriteLine("NonLazy:"); 
      g.Run(i => Console.WriteLine("Group returned: " + i.Key)); 
      Console.WriteLine(g.Single(i => i.Key).Count()); 

      Console.ReadLine(); 

который печатает:

Lazy: 
     eval 0 
Group returned: True 
     eval 1 
Group returned: False 
     eval 2 
     eval 3 
     eval 4 
     eval 5 
     eval 6 
     eval 7 
     eval 8 
     eval 9 
NonLazy: 
     eval 0 
     eval 1 
     eval 2 
     eval 3 
     eval 4 
     eval 5 
     eval 6 
     eval 7 
     eval 8 
     eval 9 
Group returned: True 
Group returned: False 

Как вы можете видеть, в моей LazyGroupBy группы возвращаются, как только они впервые встретились, и может, таким образом, действовать, не дожидаясь сгруппировки всей последовательности.

Мысли?

Редактировать: быстро подумал, я думаю, что «Lazy» - неправильный термин ... Я не носитель языка, какой термин я действительно ищу?

+1

Вы сразу же получите группу, но вы будете уверены, что у группы есть все ее участники только после того, как весь исходный текст был повторен. Так что если вы просто заботитесь о ключах - есть лучшие способы извлечь только групповые ключи, хотя ... –

+0

Не уверен, что это на 100% подходит, но мне интересно, [Push LINQ] (http://msmvps.com /blogs/jon_skeet/archive/2008/01/04/quot-push-quot-linq-revisited-next-attempt-at-an-explanation.aspx) может помочь здесь –

ответ

1

Это «ленивое» исполнение называется отложенным исполнением.

Когда вы возвращаете группу, она содержит только первый элемент, и никакие предметы не будут добавлены к ней, пока вы не получите больше групп. Таким образом, этот подход работает только в том случае, если вы обрабатываете группы в отдельном потоке, чтобы основной поток мог продолжить чтение коллекции, или если вы сначала прочитали все группы, а затем обработали их, что, конечно же, делает отложенную обработку бессмысленной.

Кроме того, вы всегда должны прочитать все группы для групп, которые будут заполнены, если вы используете Take, чтобы ограничить запрос, метод не будет завершен, и уже возвращенные группы никогда не будут завершены. Это также означает, что у вас могут быть потоки, которые все еще ждут данных, которые никогда не будут там.

+0

Это правда, однако я считаю, что подход Rune FS обойдет оба эти, правильно? – chrisaut

+0

@Steven: Да, будет. Однако он добавляет еще одно ограничение; он работает только в том случае, если вы можете прочитать коллекцию несколько раз. Если вы, например, читаете из базы данных, это потребует результата для каждой группы, что может сделать ее намного медленнее, чем ждать всего результата один раз. – Guffa

4

В вашем решении кажется, что возвращенные группы будут изменены после возвращения группы. Это может походить на некоторые шаблоны программирования, но я не считаю его полезным.

Представьте, что вы обрабатываете группу, когда она сначала возвращается, а затем в какой-то момент времени добавляется новый элемент в группу. Как вы знаете, чтобы перерабатывать членов группы? Я предполагаю, что некоторые сгруппированные элементы никогда не будут обработаны вызывающим. Несмотря на то, что вызывается CompleteAdding, уведомление потребителю LazyGroupBy не предоставляется.

Опять же, это может поместиться в некоторых ситуациях, но я не могу придумать, когда я буду использовать его вслух.

2

Это интересно, но вы можете показать реальный случай использования в этом мире?

Я бы предположил, что в большинстве ситуаций в реальном мире вы будете перебирать группы и для каждой группы перебирать элементы или называть какую-то совокупную функцию в этой группе. В этом случае эта совокупная операция будет блокироваться в любом случае. В этой ситуации нет преимущества перед использованием GroupBy.

Другая ситуация, когда вас не интересуют элементы, только группы. Но тогда вам вообще не нужен GroupBy - вы можете использовать Select then Distinct.

Если у вас возникли ситуации, когда вам понадобился этот «ленивый» GroupBy, пожалуйста, добавьте его в свой вопрос, чтобы дать немного фона и мотивации.

+0

Мой сценарий: у меня есть несколько тысяч файлов, которые могут быть логически сгруппированы. Группе Foreach мне нужно сделать несколько дорогих веб-поисков, и только когда это будет завершено (и немного больше постобработки), мне нужны фактические файлы, принадлежащие каждой группе (чтобы записать их в db). – chrisaut

0

я бы об этом differrently

public static IEnumerable<KeyValuePair<R, IEnumerable<T>>> GroupByLazy<T, R>(this IEnumerable<T> source, Func<T, R> keySelector) 
     { 
      var set = HashSet(); 
      foreach (var item in source) 
      { 
       var Key = keySelector(item); 
       if(set.Add(Key)) 
       { 
        var groupedItems = from i in source 
             where keySelector(i) == Key 
             select i; 
        yield return new KevValuePair<R,IEnumerable<T>>(Key, groupedItems); 
       } 
      } 
     } 

вниз сторона причины, что фильтрация будет применяться ко всему источника для каждой группы, но обычно при Ленивый оценки является обязательным, что это из-за латентности более сквозная скорость

+0

Моя первая мысль заключается в том, что ваше решение охватывает несколько ограничений (упомянутых в ответе Гуффа). Мой ключевой селектор немного дороже unorntunaltyl, хотя, возможно, стоит посмотреть на memoization. – chrisaut