2011-01-06 14 views
9

Sono il seguente codice:dividere un elenco utilizzando 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) 

Questo sembra brutto. Fondamentalmente, scorrere un elenco e aggiungere elementi a un elenco fino a quando non viene attivata una condizione e aggiungere il resto a un altro elenco.

C'è un modo migliore ad es. usando linq? CheckCondition() è costoso, e le liste possono essere enormi, quindi preferirei non fare nulla che itera le liste due volte.

+1

capisco il vostro codice, ma se '(e.Current)' non ha senso. Penso che stai cercando di memorizzare il valore dell'ultima chiamata 'MoveNext' (dal primo ciclo) e controlla se è andata a buon fine. – Ani

+0

Aggiornato per avere più senso (ad esempio, aggiungere e.Current a 'b' una volta che CheckCondition è true, invece di gestirlo fuori dal ciclo – Anonym

+0

Sì, è chiaro ora. – Ani

risposta

7

Ecco una soluzione che sta per enumerare l'elenco due volte, ma non sarà controllare lo stato per la seconda volta, e quindi dovrebbe essere più veloce:

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

Se someList implementa IList<T>, ogni articolo sarà effettivamente enumerato solo una volta, quindi non ci sarà alcuna penalità.
ho pensato Skip è stato ottimizzato per il caso di IList<T>, ma a quanto pare non è ... Tuttavia si potrebbe facilmente implementare il proprio Skip metodo che utilizza questa ottimizzazione (vedi Jon Skeet's article su questo)

Sarebbe effettivamente essere più elegante se ci fosse un metodo TakeUntil ... possiamo creare facilmente:

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; 
    } 
} 

Con questo metodo, il codice diventa:

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

+1: Bello, ma io * penso * "Skip' non ha l'ottimizzazione di cui parli per' IList 'da. NET 4. Non sono sicuro. – Ani

+0

Ani ha ragione. 'Skip' non ha ottimizzazioni specifiche per' IList 'quindi la prima parte della lista sarà sempre attraversata due volte. – LukeH

+0

@Ani, ho appena controllato, non sembra essere ottimizzato per IList ... aggiornerò la mia risposta. –

2

Personalmente, non penso che ci sia bisogno di LINQ qui.

vorrei fare qualcosa di simile:

bool conditionHit = false; 

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

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

Mi piace meglio del codice dell'OP perché non ha quel discutibile 'if (e.Current)' in esso. Idealmente si dovrebbe impostare 'listTobeAdded' due volte (una volta per ogni lista) invece di calcolarlo ogni iterazione. – Gabe

+0

@Gabe: Grazie. Quello che volevo davvero evitare era iterare la sorgente due volte e calcoli ridondanti di 'CheckCondition'; entrambi questi requisiti sono indicati nel problema. Non pensavo che l'OP avesse problemi con la ramificazione ridondante basata su una bandiera. :) – Ani

1

questo finirà per andare oltre gli elementi nel primo elenco più di una volta, ma chiede solo attraverso CheckCondition la prima volta:

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

a.Count() sta per enumerare la prima query, che dovrà essere enumerata * di nuovo * per ottenere i risultati ... È necessario chiamare ToList alla fine (e si finirà esattamente con la mia soluzione) –

2

Se someList è una concreta List<T> allora questo solo avrà bisogno di un singolo passare attraverso ogni elemento:

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

non volevo cambiare Ani's answer, ma ecco una leggera semplificazione.

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

    listToBeAdded.Add(item); 
} 
+1

+1 - Ottima risposta. – ChaosPandion

0

Prova questo (non riutilizzare metodi incorporati a Linq (noto per riavvolgere iteratori)), solo riutilizzando la logica del PO (che ritengo performante, non rivalutare condizione su successivo la metà di lista) e di imballaggio in un metodo di estensione ordinata e Tuple:

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 

uscita:

First Half : I 
First Half : Love 
First Half : You 
Second Half : More 
Second Half : Today 
Second Half : Than 
Second Half : Yesterday 
Problemi correlati