2015-08-10 10 views
5

Ho una collezione, in particolare uno IList<T>. Conosco due elementi all'interno della collezione, startElement e endElement.Come posso estrarre una parte di un oggetto IEnumerable in base agli elementi noti della raccolta?

Esiste una query LINQ che restituisce l'enumerabile da startElement a endElement, incluso?

Avevo pensato di usare sequence.SkipWhile(p=>p!=startElement).TakeWhile(q=>q!=endElement) ma che ci rimette l'ultimo elemento ...

+0

no, ma dovrebbe essere facile per creare il proprio. Se chiarisci le tue esigenze su ciò che è accettabile, qualcuno può trovare codice duplicato o suggerito. –

+0

Se si conoscono i due elementi nell'elenco, in questo ordine, utilizzare solo questa query e "Concat" il comando "endElement" fino alla fine.Se non sai se gli elementi ci sono nell'ordine richiesto, dovrai materializzare completamente la parte richiesta della lista prima di restituire anche il primo elemento al chiamante (perché non devi restituire nulla se il 'endElement 'non è in realtà lì, o addirittura lanciare un'eccezione). – GSerg

risposta

1

Il meglio che posso pensare è:

var subSection = TestData.SkipWhile(p => p != startElement).ToList(); 
var result = subSection.Take(subSection.IndexOf(endElement) + 1); 
+0

ToArray aumenta i requisiti di memoria quando non è necessario –

+0

Avevo bisogno di creare l'array per utilizzare il metodo IndexOf e il consumo di memoria sarebbe trascurabile, sarebbe solo un clone superficiale. –

+0

Poiché la domanda originale non specifica la dimensione dell'iterazione, non si può mai dire se è trascurabile. Nel mio attuale progetto ETL, posso difficilmente permettermi un'intera collezione in memoria in molti casi. L'ienumerable di origine potrebbe portare miliardi di record da un database e anche la dimensione dell'elemento può essere elevata. Nel caso in cui si enumeri attentamente con i metodi linq, questi oggetti sono soggetti alla garbage collection, e si può iterare praticamente per sempre. –

1

George ha scritto un'estensione più flessibile, si può trovare in qui: https://stackoverflow.com/a/31940000/5106041

vecchia versione:

public static class MyExtensions 
{ 
    public static IEnumerable <TData> InBetween <TData> (this IEnumerable <TData> Target, TData StartItem, TData EndItem) 
    { 
     var Comparer = EqualityComparer <TData>.Default; 
     var FetchData = false; 
     var StopIt = false; 

     foreach (var Item in Target) { 
      if (StopIt) 
       break; 

      if (Comparer.Equals (Item, StartItem)) 
       FetchData = true; 

      if (Comparer.Equals (Item, EndItem)) 
       StopIt = true; 

      if (FetchData) 
       yield return Item; 
     } 

     yield break; 
    } 
} 

Così, ora è possibile utilizzare in questo modo:

sequence.InBetween (startElement, endElement); 

E non ci vorrà iterare l'intera sequenza. Si noti che qui ci sono molte estensioni in lettura http://linqlib.codeplex.com/

+0

Questo valuterà la condizione per tutti gli elementi nella sequenza. Credo che il punto sia smettere di enumerare non appena tutto è stato preso. – GSerg

+0

Ma è così che funziona linq, utilizza iteratori "collegati" e, a essere onesti, un confronto di riferimento è molto economico, C# confronta solo l'indirizzo di memoria. –

+0

Linq non valuta ciò che non è necessario. 'TakeWhile' smetterà di recuperare elementi non appena la condizione è falsa. Il tuo codice continuerà a prenderli (e scartare). Se la sequenza contiene un milione di elementi e quelli richiesti sono 1-10, il tuo codice eseguirà il ciclo dell'intero milione e restituirà solo i primi dieci. 'TakeWhile' eseguirà solo un ciclo di dieci. – GSerg

1

Questo non usa LINQ, ma è probabilmente l'approccio più diretto/leggibile.

 int startIndex = sequence.IndexOf(startElement), 
      endIndex = sequence.IndexOf(endElement); 

     var range = sequence.GetRange(
         startIndex, 
         // +1 to account for zero-based indexing 
         1 + endIndex - startIndex 
        ); 

Si noti che questo è tecnicamente meno efficiente rispetto alle alternative, ma se si dispone già di un IList in memoria, le differenze sarà probabilmente meno di un millisecondo, che è un piccolo sacrificio per rendere il codice leggibile.

Suggerirei di avvolgere il blocco di codice con un cronometro per testare la tua situazione specifica per essere sicuro, comunque.

+1

Questo valuterà la sequenza tre volte. – GSerg

+1

Vero, ma ho il sospetto che vada bene per la situazione. Se c'è già un IList in memoria , è probabile che non ci sarà nemmeno una differenza di millisecondo rispetto ad approcci alternativi. Leggibilità> miglioramenti delle prestazioni inferiori al millisecondo, IMO. – Colin

1

Questo sarà il più efficiente, in quanto non crea alcun oggetto enumeratore non necessario e attraversa l'elenco solo una volta.

var result = new List<T>(); 
var inSequence = false; 

for (var i = 0; i < list.Length; i++) 
{ 
    var current = list[i]; 

    if (current == startElement) inSequence = true; 
    if (!inSequence) continue; 

    result.add(current); 
    if (current == endElement) break; 
} 

Questo non gestire il caso in cui endElement manca, ma si potrebbe farlo abbastanza facilmente assegnando result = null come l'ultima riga del ciclo in cui fori = list.Length - 1

1

Suppongo che non vuoi utilizzare memoria extra e non voler superare la complessità algoritmica del metodo di iterazione sottostante, quindi ToList, GroupBy, IndexOf non sono consentiti nelle mie implementazioni proposte.

Inoltre, per non mettere vincoli sul tipo di elemento, sto usando predicati.

public static class EnumerableExtensions 
    { 
     /// <summary> 
     /// This one works using existing linq methods. 
     /// </summary> 
     public static IEnumerable<T> GetRange<T>(this IEnumerable<T> source, Func<T, bool> isStart, Func<T, bool> isStop) 
     { 
      var provideExtraItem = new[] { true, false }; 
      return source 
       .SkipWhile(i => !isStart(i)) 
       .SelectMany(i => provideExtraItem, (item, useThisOne) => new {item, useThisOne }) 
       .TakeWhile(i => i.useThisOne || !isStop(i.item)) 
       .Where(i => i.useThisOne) 
       .Select(i => i.item); 
     } 

     /// <summary> 
     /// This one is probably a bit faster. 
     /// </summary> 
     public static IEnumerable<T> GetRangeUsingIterator<T>(this IEnumerable<T> source, Func<T, bool> isStart, Func<T, bool> isStop) 
     { 
      using (var iterator = source.GetEnumerator()) 
      { 
       while (iterator.MoveNext()) 
       { 
        if (isStart(iterator.Current)) 
        { 
         yield return iterator.Current; 
         break; 
        } 
       } 
       while (iterator.MoveNext()) 
       { 
        yield return iterator.Current; 
        if (isStop(iterator.Current)) 
         break; 
       } 
      } 
     } 
    } 

Questi metodi possono essere usati come metodi di estensione:

new[]{"apple", "orange", "banana", "pineapple"}.GetRange(i => i == "orange", i => i == "banana") 
+0

Ho appena modificato leggermente il metodo GetRange. Rimosso una selezione non desiderata. –

Problemi correlati