2010-12-28 6 views
52

Posso dividere uno IEnumerable<T> in due IEnumerable<T> utilizzando LINQ e solo una singola query/istruzione LINQ?Posso dividere un oggetto IEnumerable in due in base a un criterio booleano senza due query?

Desidero evitare di ripetere due volte lo IEnumerable<T>. Ad esempio, è possibile combinare le ultime due istruzioni di seguito in modo che tutti i valori vengano solo attraversati una volta?

IEnumerable<MyObj> allValues = ... 
List<MyObj> trues = allValues.Where(val => val.SomeProp).ToList(); 
List<MyObj> falses = allValues.Where(val => !val.SomeProp).ToList(); 

risposta

52

È possibile utilizzare questo:

var groups = allValues.GroupBy(val => val.SomeProp); 

Per forzare la valutazione immediata, come nel tuo esempio:

var groups = allValues.GroupBy(val => val.SomeProp) 
         .ToDictionary(g => g.Key, g => g.ToList()); 
List<MyObj> trues = groups[true]; 
List<MyObj> falses = groups[false]; 
+1

elegante! Posso sempre utilizzare groups.ContainsKey() o groups.TryGetValue() per gestire il caso in cui manca la chiave. – SFun28

+3

Arresta in modo anomalo se una chiave (vera o falsa) non viene mai utilizzata. per esempio. se SomeProp è sempre true, i gruppi [false] si arrestano in modo anomalo con Eccezione "La chiave specificata non era presente nel dizionario" – buffjape

+0

GroupBy utilizza l'ordinamento per creare gruppi. Non so se è meglio mantenere la scansione ripetibile due volte rispetto a una sorta di scansione. – ejmarino

48

Alcune persone, come dizionari, ma io preferisco Ricerche a causa del comportamento quando un manca la chiave

IEnumerable<MyObj> allValues = ... 
ILookup<bool, MyObj> theLookup = allValues.ToLookup(val => val.SomeProp); 

    //does not throw when there are not any true elements. 
List<MyObj> trues = theLookup[true].ToList(); 
    //does not throw when there are not any false elements. 
List<MyObj> falses = theLookup[false].ToList(); 

Sfortunatamente, questo approccio enumera due volte - una volta per creare la ricerca, poi una volta per creare le liste.

Se non si ha realmente bisogno di liste, è possibile ottenere questo fino a una singola iterazione:

IEnumerable<MyObj> trues = theLookup[true]; 
IEnumerable<MyObj> falses = theLookup[false]; 
+4

+1 per suggerire Ricerca e per notare il potenziale problema con le chiavi mancanti quando si utilizza Dizionario. –

+0

Penso che l'IGrouping dovrebbe essere IEnumerable nel secondo esempio? L'IGrouping non viene compilato e sembra essere il risultato del metodo di estensione GroupBy. O forse è richiesto un cast per IGrouping? – SFun28

+0

Preferisco anche 'ToLookup'. – leppie

3

Copia metodo di estensione di pasta per la vostra convenienza.

public static void Fork<T>(
    this IEnumerable<T> source, 
    Func<T, bool> pred, 
    out IEnumerable<T> matches, 
    out IEnumerable<T> nonMatches) 
{ 
    var groupedByMatching = source.ToLookup(pred); 
    matches = groupedByMatching[true]; 
    nonMatches = groupedByMatching[false]; 
} 

o utilizzando tuple in C# 7,0

public static (IEnumerable<T> matches, IEnumerable<T> nonMatches) Fork<T>(
    this IEnumerable<T> source, 
    Func<T, bool> pred) 
{ 
    var groupedByMatching = source.ToLookup(pred); 
    return (groupedByMatching[true], groupedByMatching[false]); 
} 

// Ex. 
var numbers = new [] { 1, 2, 3, 4, 5, 6, 7, 8 }; 
var (numbersLessThanEqualFour, numbersMoreThanFour) = numbers.Fork(x => x <= 4); 
Problemi correlati