2013-07-03 15 views
5

czekałem na zasadzie dzielenia foreach pętlę na kilka części i natknąłem się na następujący kod:Optymalizacja LINQ w foreach

foreach(var item in items.Skip(currentPage * itemsPerPage).Take(itemsPerPage)) 
{ 
    //Do stuff 
} 

Byłoby items.Skip(currentPage * itemsPerPage).Take(itemsPerPage) być przetwarzane w każdej iteracji, czy to być przetwarzane raz i mieć tymczasowy wynik używany z pętli foreach automatycznie przez kompilator?

+1

umieścić punkt załamania i zobaczyć. –

+0

To tylko jeden podział. Czy to też nazywasz z pętli? –

Odpowiedz

6

Konstrukcja foreach jest równoznaczne z:

IEnumerator enumerator = myCollection.GetEnumerator(); 
try 
{ 
    while (enumerator.MoveNext()) 
    { 
     object current = enumerator.Current; 
     Console.WriteLine(current); 
    } 
} 
finally 
{ 
    IDisposable e = enumerator as IDisposable; 
    if (e != null) 
    { 
     e.Dispose(); 
    } 
} 

Więc nie, myCollection będą przetwarzane tylko raz.

Aktualizacja:

Należy pamiętać, że to zależy od realizacji IEnumerator że IEnumerable zastosowań.

W tym (EVIL) Przykład:

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


namespace TestStack 
{ 
    class EvilEnumerator<T> : IEnumerator<T> { 

     private IEnumerable<T> enumerable; 
     private int index = -1; 

     public EvilEnumerator(IEnumerable<T> e) 
     { 
      enumerable = e; 
     } 


     #region IEnumerator<T> Membres 

     public T Current 
     { 
      get { return enumerable.ElementAt(index); } 
     } 

     #endregion 

     #region IDisposable Membres 

     public void Dispose() 
     { 

     } 

     #endregion 

     #region IEnumerator Membres 

     object IEnumerator.Current 
     { 
      get { return enumerable.ElementAt(index); } 
     } 

     public bool MoveNext() 
     { 
      index++; 
      if (index >= enumerable.Count()) 
       return false; 
      return true; 
     } 

     public void Reset() 
     { 

     } 

     #endregion 
    } 
    class DemoEnumerable<T> : IEnumerable<T> 
    { 

     private IEnumerable<T> enumerable; 

     public DemoEnumerable(IEnumerable<T> e) 
     { 
      enumerable = e; 
     } 


     #region IEnumerable<T> Membres 

     public IEnumerator<T> GetEnumerator() 
     { 
      return new EvilEnumerator<T>(enumerable); 
     } 

     #endregion 

     #region IEnumerable Membres 

     IEnumerator IEnumerable.GetEnumerator() 
     { 
      return this.GetEnumerator(); 
     } 

     #endregion 
    } 

    class Program 
    { 
     static void Main(string[] args) 
     { 
      IEnumerable<int> numbers = Enumerable.Range(0,100); 
      DemoEnumerable<int> enumerable = new DemoEnumerable<int>(numbers); 
      foreach (var item in enumerable) 
      { 
       Console.WriteLine(item); 
      } 
     } 
    } 
} 

Każda iteracja nad enumerable oceni numbers dwa razy.

9

Nie, zostanie przetworzony jeden raz.

To samo jak:

public IEnumerable<Something> GetData() { 
    return someData; 
} 


foreach(var d in GetData()) { 
    //do something with [d] 
} 
+0

Nie jestem pewien, czy to jest poprawne. Chodzi mi o to, że twoja funkcja GetData jest podobna do właściwości getter i za każdym razem, gdy pętla zwiększa jej krok, wywoła ten, który uzyska dostęp lub, w twoim przypadku, twoją metodę, w zasadzie wywołując konstrukcję Skip/Take na każdym kroku. –

+0

@PotecaruTudor: w pętli foreach będą one nazywane one. Aby to udowodnić, po prostu wykonaj prosty test. – Tigran

+0

Tak, właśnie debugowałem przykład testowy i miałeś rację. Dzięki. –

0

Pytanie:

Byłoby items.Skip (currentPage * itemsPerPage) .Take (itemsPerPage) być przetwarzane każdej iteracji, czy byłoby przetwarzane raz i mieć tymczasowy rezultat używany z pętlą foreach automatycznie przez kompilator ?

Odpowiedź:

byłoby przetwarzane raz, nie każda iteracja. Możesz umieścić kolekcję w zmiennej, aby uczynić foreach bardziej czytelnym. Zilustrowany poniżej.

foreach(var item in items.Skip(currentPage * itemsPerPage).Take(itemsPerPage)) 
{ 
    //Do stuff 
} 

vs.

List<MyClass> query = items.Skip(currentPage * itemsPerPage).Take(itemsPerPage).ToList(); 

foreach(var item in query) 
{ 
    //Do stuff 
} 

vs.

IEnumerable<MyClass> query = items.Skip(currentPage * itemsPerPage).Take(itemsPerPage); 

foreach(var item in query) 
{ 
    //Do stuff 
} 
+1

Widzę bitwę między blokami kodu. –

+0

Edytowane powyżej. :) –

0

Kod że przedstawienie będzie iteracyjne tylko pozycje z listy raz, jak inni zwrócili uwagę.

Jednak daje to tylko pozycje dla jednej strony. Jeśli obsługujesz wiele stron, musisz wywoływać ten kod raz dla każdej strony (ponieważ gdzieś musisz zwiększyć wartość currentPage, prawda?).

Chodzi mi o to, że trzeba robić coś takiego:

for (int currentPage = 0; currentPage < numPages; ++currentPage) 
{ 
    foreach (var item in items.Skip(currentPage*itemsPerPage).Take(itemsPerPage)) 
    { 
     //Do stuff 
    } 
} 

Teraz, jeśli zrobić że, wtedy będzie być iteracji sekwencji wiele razy - raz dla każdej strony. Pierwsza iteracja potrwa tylko do końca pierwszej strony, ale następna będzie powtarzać od początku do końca drugiej strony (przez Skip() i Take()) - a następna będzie iterować od początku do końca koniec trzeciej strony. I tak dalej.

Aby tego uniknąć, można napisać metodę rozszerzenia dla IEnumerable<T>, która dzieli dane na partie (które można również opisać jako "dzielenie na strony" danych na "strony").

Zamiast po prostu prezentuje IEnumerable z IEnumerables, może być bardziej przydatna owinąć każdą partię w klasie dostarczyć indeks partii wraz z pozycji w partii, tak jak poniżej:

public sealed class Batch<T> 
{ 
    public readonly int Index; 
    public readonly IEnumerable<T> Items; 

    public Batch(int index, IEnumerable<T> items) 
    { 
     Index = index; 
     Items = items; 
    } 
} 

public static class EnumerableExt 
{ 
    // Note: Not threadsafe, so not suitable for use with Parallel.Foreach() or IEnumerable.AsParallel() 

    public static IEnumerable<Batch<T>> Partition<T>(this IEnumerable<T> input, int batchSize) 
    { 
     var enumerator = input.GetEnumerator(); 
     int index = 0; 

     while (enumerator.MoveNext()) 
      yield return new Batch<T>(index++, nextBatch(enumerator, batchSize)); 
    } 

    private static IEnumerable<T> nextBatch<T>(IEnumerator<T> enumerator, int blockSize) 
    { 
     do { yield return enumerator.Current; } 
     while (--blockSize > 0 && enumerator.MoveNext()); 
    } 
} 

To rozszerzenie metoda nie buforuje danych, a tylko iteruje za jej pomocą tylko jeden raz.

Dzięki tej metodzie rozszerzenia staje się bardziej czytelna do grupowania elementów. Zauważ, że ten przykład wylicza poprzez wszystkie przedmioty dla wszystkich stron, w przeciwieństwie np PO, która tylko iteracji pozycji na jednej stronie:

var items = Enumerable.Range(10, 50); // Pretend we have 50 items. 
int itemsPerPage = 20; 

foreach (var page in items.Partition(itemsPerPage)) 
{ 
    Console.Write("Page " + page.Index + " items: "); 

    foreach (var i in page.Items) 
     Console.Write(i + " "); 

    Console.WriteLine(); 
}