Dodanie po bardzo przydatnym komentarzu mhand na końcu
Oryginalna odpowiedź
Chociaż większość rozwiązań może działać, myślę, że nie są one zbyt wydajne. Załóżmy, że chcesz tylko kilka pierwszych elementów z kilku pierwszych części. Wtedy nie chcesz iterować wszystkich (zillionowych) przedmiotów w sekwencji.
Następujące wartości będą co najwyżej dwukrotnie wyliczone: raz dla Take i raz dla Skip. Nie będzie wyliczać więcej elementów niż użyjesz:
public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>
(this IEnumerable<TSource> source, int chunkSize)
{
while (source.Any()) // while there are elements left
{ // still something to chunk:
yield return source.Take(chunkSize); // return a chunk of chunkSize
source = source.Skip(chunkSize); // skip the returned chunk
}
}
Ile razy to wyliczy sekwencję?
Załóżmy, że dzielisz źródło na części chunkSize
. Zliczasz tylko pierwsze N fragmentów. Z każdego wyliczonego fragmentu wyliczysz tylko pierwsze M elementów.
While(source.Any())
{
...
}
dowolna otrzyma moduł wyliczający, wykona 1 operację MoveNext () i zwróci zwróconą wartość po usunięciu modułu wyliczającego. Zostanie to zrobione N razy
yield return source.Take(chunkSize);
Według źródła referencyjnego zrobi to coś takiego:
public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count)
{
return TakeIterator<TSource>(source, count);
}
static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> source, int count)
{
foreach (TSource element in source)
{
yield return element;
if (--count == 0) break;
}
}
Nie robi to wiele, dopóki nie zaczniesz wyliczać ponad pobranego kawałka. Jeśli pobierzesz kilka fragmentów, ale zdecydujesz, aby nie wyliczać więcej niż pierwszego fragmentu, foreach nie zostanie wykonany, ponieważ wyświetli się twój debugger.
Jeśli zdecydujesz się wziąć pierwsze M elementów pierwszego fragmentu, wówczas zwrot wydajności jest wykonywany dokładnie M razy. To znaczy:
- pobierz moduł wyliczający
- wywołaj MoveNext () i bieżące czasy M.
- Usuń moduł wyliczający
Po zwróceniu pierwszego fragmentu pomijamy ten pierwszy fragment:
source = source.Skip(chunkSize);
Jeszcze raz: przyjrzymy się źródłu odniesienia, aby znaleźćskipiterator
static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (count > 0 && e.MoveNext()) count--;
if (count <= 0)
{
while (e.MoveNext()) yield return e.Current;
}
}
}
Jak widać, SkipIterator
wywołania MoveNext()
raz dla każdego elementu w części. Nie dzwoni Current
.
Tak więc na porcję widzimy, że następujące czynności są wykonywane:
- Any (): GetEnumerator; 1 MoveNext (); Dispose Enumerator;
Brać():
- nic, jeśli zawartość fragmentu nie jest wyliczona.
Jeśli treść jest wyliczona: GetEnumerator (), jeden MoveNext i jeden bieżący na wyliczony element, Dispose enumerator;
Skip (): dla każdego wyliczonego fragmentu (NIE jego zawartości): GetEnumerator (), MoveNext () porcja Wielkość porcji, brak bieżącej! Usuń moduł wyliczający
Jeśli spojrzysz na to, co dzieje się z modułem wyliczającym, zobaczysz, że istnieje wiele wywołań MoveNext () i tylko wywołania Current
elementów TSource, do których faktycznie chcesz się dostać.
Jeśli weźmiesz N Chunks o rozmiarze chunkSize, wówczas wywołania MoveNext ()
- N razy dla Any ()
- nie ma jeszcze czasu na Take, o ile nie wyliczysz Kawałków
- N razy chunkSize dla Skip ()
Jeśli zdecydujesz się wyliczyć tylko pierwsze M elementów każdego pobranego fragmentu, musisz wywołać MoveNext M razy na wyliczony fragment.
Łącznie
MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)
Więc jeśli zdecydujesz się wyliczyć wszystkie elementy wszystkich porcji:
MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once
To, czy MoveNext wymaga dużo pracy, zależy od rodzaju sekwencji źródłowej. W przypadku list i tablic jest to prosty przyrost indeksu, z możliwością sprawdzenia poza zakresem.
Ale jeśli Twój IEnumerable jest wynikiem zapytania do bazy danych, upewnij się, że dane są naprawdę zmaterializowane na twoim komputerze, w przeciwnym razie dane zostaną pobrane kilka razy. DbContext i Dapper poprawnie prześlą dane do procesu lokalnego, zanim będzie można uzyskać do nich dostęp. Jeśli wyliczysz tę samą sekwencję kilka razy, nie zostanie ona pobrana kilka razy. Dapper zwraca obiekt będący Listą, DbContext pamięta, że dane zostały już pobrane.
Zależy od Twojego Repozytorium, czy rozsądnie jest wywołać AsEnumerable () lub ToLists () zanim zaczniesz dzielić elementy w Chunks