2011-01-06 5 views
9

Я следующий код:разделить список, используя LINQ

var e = someList.GetEnumerator(); 
    var a = new List<Foo>(); 
    var b = new List<Foo>(); 
    while(e.MoveNext()) { 
    if(CheckCondition(e.Current)) { 
     b.Add(e.Current); 
     break; 
    } 
    a.Add(e.Current); 
} 

while(e.MoveNext()) 
    b.Add(e.Current) 

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

Есть ли лучший способ, например. используя linq? CheckCondition() стоит дорого, и списки могут быть огромными, поэтому я бы предпочел не делать ничего, что повторяет списки дважды.

+1

Я понимаю ваш код, но 'если (e.Current)' не имеет смысла. Я думаю, вы ищете, чтобы сохранить значение последнего вызова MoveNext (из первого цикла) и проверить, успешно ли он. – Ani

+0

Обновлено, чтобы иметь больше смысла (т. Е. Добавить e.Current к 'b' после проверки CheckCondition, а не обрабатывать этот элемент за пределами цикла – Anonym

+0

Да, теперь ясно. – Ani

ответ

7

Вот решение, которое будет перечислять список дважды, но он не будет проверять состояние во второй раз, так что это должно быть быстрее:

var a = someList.TakeWhile(x => !CheckCondition(x)).ToList(); 
var b = someList.Skip(a.Count).ToList(); 

Если someList реализует IList<T>, каждый элемент будет фактически перечислить только один раз, поэтому штраф не будет.
Я думал Skip был оптимизирован для случая IList<T>, но, видимо, это не ... Тем не менее вы можете легко реализовать свой собственный Skip метод, который использует эту оптимизацию (см Jon Skeet's article об этом)

Это будет фактически более элегантно, если был TakeUntil метод ... мы можем легко создать его:

public static IEnumerable<TSource> TakeUntil<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) 
{ 
    foreach(var item in source) 
    { 
     if (predicate(item)) 
      break; 
     yield return item; 
    } 
} 

С помощью этого метода, код становится:

var a = someList.TakeUntil(CheckCondition).ToList(); 
var b = someList.Skip(a.Count).ToList(); 
+5

+1: Ницца, но я * думаю * 'Пропустить' не имеет оптимизации, о которой вы говорите для' IList 'от .NET 4. Не уверен, хотя. – Ani

+0

Ани справа. 'Skip' не имеет особых оптимизаций для' IList ', поэтому первая часть списка всегда будет проходить дважды. – LukeH

+0

@ Ани, я только что проверил, он, похоже, не оптимизирован для IList ... Я обновлю свой ответ. –

2

Лично я не думаю, что здесь нужна LINQ.

Я хотел бы сделать что-то вроде:

bool conditionHit = false; 

foreach (var item in someList) 
{ 
    if (!conditionHit) 
     conditionHit = CheckCondition(item); 

    var listToBeAdded = conditionHit ? b : a; 
    listToBeAdded.Add(item); 
} 
+2

Мне нравится это лучше, чем код OP, потому что в нем нет сомнительного 'if (e.Current)'. В идеале вы бы дважды установили 'listTobeAdded' дважды (один раз для каждого списка), а не вычисляли каждую итерацию. – Gabe

+0

@Gabe: Спасибо. То, чего я действительно хотел избежать, это повторить исходный код дважды и избыточные вычисления 'CheckCondition'; оба эти требования указаны в проблеме. Не думал, что у ОР возникли проблемы с избыточным ветвлением на основе флага. :) – Ani

1

Это будет в конечном итоге будет более пунктов в первом списке несколько раз, но только звонки через CheckCondition в первый раз:

var a = someList.TakeWhile(e => !CheckCondition(e)); 
var b = someList.Skip(a.Count()); 
+2

a.Count() собирается перечислить первый запрос, который нужно будет перечислить * снова *, чтобы получить результаты ... В конце вам нужно вызвать ToList (и вы получите именно мое решение) –

2

Если someList конкретная List<T> то это нужно только один пройти через каждый элемент:

var a = someList.TakeWhile(x => !CheckCondition(x)).ToList(); 
var b = someList.GetRange(a.Count, someList.Count - a.Count); 
4

Я не хочу, чтобы изменить Ani's answer, но вот небольшое упрощение.

var listToBeAdded = a; 
foreach (var item in someList) 
{ 
    if (listToBeAdded == a && CheckCondition(item)) 
     listToBeAdded = b; 

    listToBeAdded.Add(item); 
} 
+1

+1 - Отличный ответ. – ChaosPandion

0

Попробуйте это (не повторное использование встроенных методов Linq (в известных для перематывать итераторы)), просто повторно использовать логику OP (который я считаю, это производительный, не переоценивать состояние на следующий половина из списка) и упаковки его в методе аккуратным расширения и Кортеж:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 



namespace Craft 
{ 
    class Act 
    { 
     static void Main(string[] args) 
     { 

      var a = new List<string> 
       { "I", "Love", "You", "More", "Today", "Than", "Yesterday" }; 

      var tx = a.SplitByCondition(s => s == "More"); 

      foreach (var s in tx.Item1) 
       Console.WriteLine("First Half : {0}", s); 

      foreach (var s in tx.Item2) 
       Console.WriteLine("Second Half : {0}", s); 

      Console.ReadLine();      
     } 

    }//Act 

    public static class Helper 
    { 

     public static Tuple<List<T>, List<T>> SplitByCondition<T> 
      (this IEnumerable<T> t, Func<T, bool> terminator) 
     { 


      var tx = new Tuple<List<T>, List<T>> 
          (new List<T>(), new List<T>()); 

      var iter = t.GetEnumerator(); 

      while (iter.MoveNext()) 
      { 
       if (terminator(iter.Current)) 
       { 
        tx.Item2.Add(iter.Current); 
        break; 
       } 

       tx.Item1.Add(iter.Current); 
      } 

      while (iter.MoveNext()) 
       tx.Item2.Add(iter.Current); 

      return tx; 
     }  

    }//Helper 

}//Craft 

Выход:

First Half : I 
First Half : Love 
First Half : You 
Second Half : More 
Second Half : Today 
Second Half : Than 
Second Half : Yesterday 
Смежные вопросы