Podziel listę na mniejsze listy o rozmiarze N.


209

Próbuję podzielić listę na serię mniejszych list.

Mój problem: Moja funkcja dzielenia list nie dzieli ich na listy o odpowiednim rozmiarze. Powinien podzielić je na listy o rozmiarze 30, ale zamiast tego podzieli je na listy o rozmiarze 114?

Jak sprawić, by moja funkcja podzieliła listę na liczbę X list o rozmiarze 30 lub mniejszym ?

public static List<List<float[]>> splitList(List <float[]> locations, int nSize=30) 
{       
    List<List<float[]>> list = new List<List<float[]>>();

    for (int i=(int)(Math.Ceiling((decimal)(locations.Count/nSize))); i>=0; i--) {
        List <float[]> subLocat = new List <float[]>(locations); 

        if (subLocat.Count >= ((i*nSize)+nSize))
            subLocat.RemoveRange(i*nSize, nSize);
        else subLocat.RemoveRange(i*nSize, subLocat.Count-(i*nSize));

        Debug.Log ("Index: "+i.ToString()+", Size: "+subLocat.Count.ToString());
        list.Add (subLocat);
    }

    return list;
}

Jeśli użyję funkcji z listy o rozmiarze 144, wówczas wynik będzie:

Indeks: 4, Rozmiar: 120
Indeks: 3, Rozmiar: 114
Indeks: 2, Rozmiar: 114
Indeks: 1, Rozmiar: 114
Indeks: 0, Rozmiar: 114


1
Jeśli rozwiązanie LINQ jest dopuszczalne, to pytanie może być pomocne .

W szczególności odpowiedź Sama Saffrona na poprzednie pytanie. I jeśli nie jest to zadanie szkolne, po prostu użyję jego kodu i przestanę.
jcolebrand

Odpowiedzi:


268
public static List<List<float[]>> SplitList(List<float[]> locations, int nSize=30)  
{        
    var list = new List<List<float[]>>(); 

    for (int i = 0; i < locations.Count; i += nSize) 
    { 
        list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i))); 
    } 

    return list; 
} 

Wersja ogólna:

public static IEnumerable<List<T>> SplitList<T>(List<T> locations, int nSize=30)  
{        
    for (int i = 0; i < locations.Count; i += nSize) 
    { 
        yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i)); 
    }  
} 

Więc jeśli mam zillion o długości listy i chcę podzielić na mniejsze listy o długości 30 i z każdej mniejszej listy chcę tylko wziąć (1), to nadal tworzę listy 30 przedmiotów, z których wyrzucam 29 przedmiotów. Można to zrobić mądrzej!
Harald Coppoolse

Czy to naprawdę działa? Czy nie zawiedzie przy pierwszym podziale, ponieważ otrzymujesz zakres od nSize do nSize? Na przykład, jeśli nSize to 3, a moja tablica ma rozmiar 5, to pierwszym zwróconym zakresem indeksu jestGetRange(3, 3)
Matthew Pigram

2
@MatthewPigram przetestowany i działa. Math.Min przyjmuje wartość minimalną, więc jeśli ostatni fragment jest mniejszy niż nSize (2 <3), tworzy listę z pozostałymi elementami.
Phate01

1
@HaraldCoppoolse OP nie poprosił o wybranie, tylko do podzielenia list
Phate01

@MatthewPigram Pierwsza iteracja - GetRange (0,3), druga iteracja - GetRange (3,2)
Serj-Tm

381

Sugerowałbym użycie tej metody rozszerzenia do podzielenia listy źródłowej na podlisty według określonego rozmiaru fragmentu:

/// <summary>
/// Helper methods for the lists.
/// </summary>
public static class ListExtensions
{
    public static List<List<T>> ChunkBy<T>(this List<T> source, int chunkSize) 
    {
        return source
            .Select((x, i) => new { Index = i, Value = x })
            .GroupBy(x => x.Index / chunkSize)
            .Select(x => x.Select(v => v.Value).ToList())
            .ToList();
    }
}

Na przykład, jeśli podzielisz listę 18 pozycji na 5 pozycji na porcję, otrzymasz listę 4 sub-list z następującymi elementami: 5-5-5-3.


25
Zanim użyjesz tego w środowisku produkcyjnym, upewnij się, że rozumiesz, jakie są konsekwencje dla pamięci i wydajności w czasie wykonywania. To, że LINQ może być zwięzłe, nie oznacza, że ​​to dobry pomysł.
Nick

4
Zdecydowanie @Nick Ogólnie sugerowałbym, aby pomyśleć przed zrobieniem czegokolwiek. Łączenie za pomocą LINQ nie powinno być częstą operacją powtarzaną tysiące razy. Zwykle trzeba tworzyć listy fragmentów w celu przetwarzania elementów partia po partii i / lub równolegle.
Dmitry Pavlov,

6
Nie sądzę, że pamięć i wydajność powinny być tutaj dużym problemem. Zdarzyło mi się, że muszę podzielić listę zawierającą ponad 200 000 rekordów na mniejsze listy, z których każda ma około 3000, co doprowadziło mnie do tego wątku. Przetestowałem obie metody i stwierdziłem, że czas działania jest prawie taki sam. Następnie przetestowałem podział tej listy na listy zawierające 3 rekordy, a wydajność nadal jest OK. Myślę, że rozwiązanie Serj-Tm jest prostsze i ma lepszą konserwację.
Silent Sojourner,

2
Zauważ, że najlepiej byłoby zrezygnować z ToList()liter i pozwolić leniwej ocenie zrobić magię.
Yair Halberstadt

3
@DmitryPavlov Przez cały ten czas nigdy nie wiedziałem o możliwości wyświetlania indeksu w ten sposób w instrukcji select! Myślałem, że to nowa funkcja, dopóki nie zauważyłem, że opublikowałeś to w 2014 roku, co naprawdę mnie zaskoczyło! Dzięki za udostępnienie tego. Czy nie byłoby lepiej mieć tej metody rozszerzenia dostępnej dla IEnumerable, a także zwrócić IEnumerable?
Aydin

37

Co powiesz na:

while(locations.Any())
{    
    list.Add(locations.Take(nSize).ToList());
    locations= locations.Skip(nSize).ToList();
}

Czy to zajmie dużo pamięci? Za każdym razem, gdy zdarza się lokalizacja.
Zasz

2
tak, nowa lista tworzona jest w każdej pętli. Tak, zużywa pamięć. Ale jeśli masz problemy z pamięcią, nie jest to miejsce do optymalizacji, ponieważ instancje tych list są gotowe do zebrania w następnej pętli. Możesz wymienić wydajność pamięci, pomijając, ToListale nie zawracałbym sobie głowy optymalizacją - jest to tak banalne i mało prawdopodobne, aby było wąskim gardłem. Główną korzyścią z tej implementacji jest jej łatwość zrozumienia. Jeśli chcesz, możesz użyć zaakceptowanej odpowiedzi, która nie tworzy tych list, ale jest nieco bardziej złożona.
Rafał

2
.Skip(n)iteruje nelementy za każdym razem, gdy jest wywoływane, chociaż może to być w porządku, ważne jest, aby wziąć pod uwagę kod krytyczny dla wydajności. stackoverflow.com/questions/20002975/…
Chakrava,

@Chakrava na pewno moje rozwiązanie nie powinno być stosowane w kodzie krytycznym dla wydajności, ale z mojego doświadczenia najpierw piszesz działający kod, a następnie określasz, co jest krytyczne dla wydajności i rzadko kiedy moje operacje linq na obiektach wykonywane są na powiedzmy 50 obiektach. Należy to oceniać indywidualnie dla każdego przypadku.
Rafał

@Rafal Zgadzam się, znalazłem wiele .Skip()s w bazie kodu mojej firmy i chociaż mogą nie być „optymalne”, działają dobrze. Rzeczy takie jak operacje DB i tak trwają znacznie dłużej. Myślę jednak, że należy zauważyć, że .Skip()„dotyka” każdego elementu <n na swojej drodze, zamiast skakać bezpośrednio do n-tego elementu (jak można się spodziewać). Jeśli iterator ma skutki uboczne dotykania elementu, .Skip()może być przyczyną trudnych do znalezienia błędów.
Chakrava,

11

Rozwiązanie Serj-Tm jest w porządku, jest to również wersja ogólna jako metoda rozszerzenia list (umieść ją w klasie statycznej):

public static List<List<T>> Split<T>(this List<T> items, int sliceSize = 30)
{
    List<List<T>> list = new List<List<T>>();
    for (int i = 0; i < items.Count; i += sliceSize)
        list.Add(items.GetRange(i, Math.Min(sliceSize, items.Count - i)));
    return list;
} 

10

Uważam, że zaakceptowana odpowiedź (Serj-Tm) jest najsolidniejsza, ale chciałbym zasugerować ogólną wersję.

public static List<List<T>> splitList<T>(List<T> locations, int nSize = 30)
{
    var list = new List<List<T>>();

    for (int i = 0; i < locations.Count; i += nSize)
    {
        list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i)));
    }

    return list;
}

8

Biblioteka MoreLinq ma wywołaną metodę Batch

List<int> ids = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; // 10 elements
int counter = 1;
foreach(var batch in ids.Batch(2))
{
    foreach(var eachId in batch)
    {
        Console.WriteLine("Batch: {0}, Id: {1}", counter, eachId);
    }
    counter++;
}

Wynik jest

Batch: 1, Id: 1
Batch: 1, Id: 2
Batch: 2, Id: 3
Batch: 2, Id: 4
Batch: 3, Id: 5
Batch: 3, Id: 6
Batch: 4, Id: 7
Batch: 4, Id: 8
Batch: 5, Id: 9
Batch: 5, Id: 0

ids są podzielone na 5 części z 2 elementami.


To musi być zaakceptowana odpowiedź. Lub przynajmniej dużo wyżej na tej stronie.
Zar Shardan

7

Mam ogólną metodę, która obejmowałaby dowolne typy, w tym zmiennoprzecinkowe, i została przetestowana jednostkowo, mam nadzieję, że pomaga:

    /// <summary>
    /// Breaks the list into groups with each group containing no more than the specified group size
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="values">The values.</param>
    /// <param name="groupSize">Size of the group.</param>
    /// <returns></returns>
    public static List<List<T>> SplitList<T>(IEnumerable<T> values, int groupSize, int? maxCount = null)
    {
        List<List<T>> result = new List<List<T>>();
        // Quick and special scenario
        if (values.Count() <= groupSize)
        {
            result.Add(values.ToList());
        }
        else
        {
            List<T> valueList = values.ToList();
            int startIndex = 0;
            int count = valueList.Count;
            int elementCount = 0;

            while (startIndex < count && (!maxCount.HasValue || (maxCount.HasValue && startIndex < maxCount)))
            {
                elementCount = (startIndex + groupSize > count) ? count - startIndex : groupSize;
                result.Add(valueList.GetRange(startIndex, elementCount));
                startIndex += elementCount;
            }
        }


        return result;
    }

Dzięki. Zastanawiasz się, czy możesz zaktualizować komentarze za pomocą definicji parametru maxCount? Siatka bezpieczeństwa?
Andrew Jens,

2
uważaj na wiele wyliczeń tego, co wyliczalne. values.Count()spowoduje pełne wyliczenie, a następnie values.ToList()kolejne. Jest to bezpieczniejsze, values = values.ToList()bo już się zmaterializowało.
ręcznie

7

Chociaż wiele powyższych odpowiedzi spełnia swoje zadanie, wszystkie strasznie zawodzą w niekończącej się sekwencji (lub naprawdę długiej sekwencji). Poniżej znajduje się całkowicie internetowa implementacja, która gwarantuje najlepszy możliwy czas i złożoność pamięci. My tylko iterujemy źródło wyliczalne dokładnie raz i używamy zwrotu z zysku do leniwej oceny. Konsument może wyrzucić listę przy każdej iteracji, dzięki czemu ślad pamięci będzie równy śladowi na liście z batchSizeliczbą elementów.

public static IEnumerable<List<T>> BatchBy<T>(this IEnumerable<T> enumerable, int batchSize)
{
    using (var enumerator = enumerable.GetEnumerator())
    {
        List<T> list = null;
        while (enumerator.MoveNext())
        {
            if (list == null)
            {
                list = new List<T> {enumerator.Current};
            }
            else if (list.Count < batchSize)
            {
                list.Add(enumerator.Current);
            }
            else
            {
                yield return list;
                list = new List<T> {enumerator.Current};
            }
        }

        if (list?.Count > 0)
        {
            yield return list;
        }
    }
}

EDYCJA: Właśnie teraz zdając sobie sprawę z tego, że OP wymaga podzielenia List<T>na mniejsze List<T>, więc moje komentarze dotyczące nieskończonych liczb nie mają zastosowania do PO, ale mogą pomóc innym, którzy tu trafią. Te komentarze były odpowiedzią na inne opublikowane rozwiązania, które wykorzystują IEnumerable<T>dane wejściowe do ich funkcji, ale wielokrotnie wyliczają źródło, które można wyliczyć.


Myślę, że IEnumerable<IEnumerable<T>>wersja jest lepsza, ponieważ nie wymaga tak dużej Listkonstrukcji.
NetMage,

@NetMage - jednym z problemów IEnumerable<IEnumerable<T>>jest to, że implementacja prawdopodobnie będzie polegać na tym, że konsument będzie w pełni wyliczał każdy uzyskany wewnętrzny wyliczalny koszt . Jestem pewien, że rozwiązanie można sformułować w sposób pozwalający uniknąć tego problemu, ale myślę, że wynikowy kod może szybko się skomplikować. Ponadto, ponieważ jest on leniwy, generujemy tylko jedną listę na raz, a przydzielanie pamięci odbywa się dokładnie raz na listę, ponieważ znamy rozmiar z góry.
ręcznie

Masz rację - moja implementacja korzysta z nowego typu modułu wyliczającego (modułu wyliczającego pozycję), który śledzi twoją bieżącą pozycję, otaczając standardowy moduł wyliczający i pozwala przejść do nowej pozycji.
NetMage,

6

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ć, SkipIteratorwywoł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 Currentelementó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


czy to nie wyliczy dwa razy na partię? więc naprawdę wyliczamy 2*chunkSizeczasy źródłowe ? Jest to śmiertelnie niebezpieczne w zależności od źródła elementu wymiennego (być może kopii zapasowej DB lub innego niezapisanego źródła). Wyobraź sobie to Enumerable.Range(0, 10000).Select(i => DateTime.UtcNow)wyliczenie jako dane wejściowe - otrzymasz różne czasy za każdym razem, gdy wyliczasz wyliczenie, ponieważ nie jest ono zapamiętywane
ręcznie

Rozważmy: Enumerable.Range(0, 10).Select(i => DateTime.UtcNow). Przez wywołanie Anybędziesz za każdym razem obliczał aktualny czas. Nie jest tak źle DateTime.UtcNow, ale weź pod uwagę wyliczenie wspierane przez połączenie z bazą danych / kursor SQL lub podobny. Widziałem przypadki, gdzie tysiące połączeń DB zostały wydane, ponieważ deweloper nie rozumieli potencjalne reperkusje „wielu wyliczeń w przeliczalna” - ReSharper stanowi wskazówkę do tego, jak dobrze
mhand

4
public static IEnumerable<IEnumerable<T>> SplitIntoSets<T>
    (this IEnumerable<T> source, int itemsPerSet) 
{
    var sourceList = source as List<T> ?? source.ToList();
    for (var index = 0; index < sourceList.Count; index += itemsPerSet)
    {
        yield return sourceList.Skip(index).Take(itemsPerSet);
    }
}

3
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items, int maxItems)
{
    return items.Select((item, index) => new { item, index })
                .GroupBy(x => x.index / maxItems)
                .Select(g => g.Select(x => x.item));
}

2

Co powiesz na ten? Chodziło o użycie tylko jednej pętli. A kto wie, może używasz tylko implementacji IList do dokładnego kodowania i nie chcesz rzutować na List.

private IEnumerable<IList<T>> SplitList<T>(IList<T> list, int totalChunks)
{
    IList<T> auxList = new List<T>();
    int totalItems = list.Count();

    if (totalChunks <= 0)
    {
        yield return auxList;
    }
    else 
    {
        for (int i = 0; i < totalItems; i++)
        {               
            auxList.Add(list[i]);           

            if ((i + 1) % totalChunks == 0)
            {
                yield return auxList;
                auxList = new List<T>();                
            }

            else if (i == totalItems - 1)
            {
                yield return auxList;
            }
        }
    }   
}

1

Jeszcze jeden

public static IList<IList<T>> SplitList<T>(this IList<T> list, int chunkSize)
{
    var chunks = new List<IList<T>>();
    List<T> chunk = null;
    for (var i = 0; i < list.Count; i++)
    {
        if (i % chunkSize == 0)
        {
            chunk = new List<T>(chunkSize);
            chunks.Add(chunk);
        }
        chunk.Add(list[i]);
    }
    return chunks;
}

1
public static List<List<T>> ChunkBy<T>(this List<T> source, int chunkSize)
    {           
        var result = new List<List<T>>();
        for (int i = 0; i < source.Count; i += chunkSize)
        {
            var rows = new List<T>();
            for (int j = i; j < i + chunkSize; j++)
            {
                if (j >= source.Count) break;
                rows.Add(source[j]);
            }
            result.Add(rows);
        }
        return result;
    }

0
List<int> list =new List<int>(){1,2,3,4,5,6,7,8,9,10,12};
Dictionary<int,List<int>> dic = new Dictionary <int,List<int>> ();
int batchcount = list.Count/2; //To List into two 2 parts if you want three give three
List<int> lst = new List<int>();
for (int i=0;i<list.Count; i++)
{
lstdocs.Add(list[i]);
if (i % batchCount == 0 && i!=0)
{
Dic.Add(threadId, lstdocs);
lst = new List<int>();**strong text**
threadId++;
}
}
Dic.Add(threadId, lstdocs);

2
lepiej wyjaśnić swoją odpowiedź niż podać fragment kodu
Kevin

0

Spotkałem tę samą potrzebę i użyłem kombinacji metod Skip () i Take () Linqa. Mnożę liczbę, którą biorę do tej pory przez liczbę iteracji, co daje mi liczbę elementów do pominięcia, a następnie biorę następną grupę.

        var categories = Properties.Settings.Default.MovementStatsCategories;
        var items = summariesWithinYear
            .Select(s =>  s.sku).Distinct().ToList();

        //need to run by chunks of 10,000
        var count = items.Count;
        var counter = 0;
        var numToTake = 10000;

        while (count > 0)
        {
            var itemsChunk = items.Skip(numToTake * counter).Take(numToTake).ToList();
            counter += 1;

            MovementHistoryUtilities.RecordMovementHistoryStatsBulk(itemsChunk, categories, nLogger);

            count -= numToTake;
        }

0

Na podstawie Dimitry Pawłowa answere Chciałbym usunąć .ToList(). A także unikaj anonimowej klasy. Zamiast tego lubię używać struktury, która nie wymaga alokacji pamięci sterty. (A ValueTuplerównież wykona pracę.)

public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
    if (source is null)
    {
        throw new ArgumentNullException(nameof(source));
    }
    if (chunkSize <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(chunkSize), chunkSize, "The argument must be greater than zero.");
    }

    return source
        .Select((x, i) => new ChunkedValue<TSource>(x, i / chunkSize))
        .GroupBy(cv => cv.ChunkIndex)
        .Select(g => g.Select(cv => cv.Value));
} 

[StructLayout(LayoutKind.Auto)]
[DebuggerDisplay("{" + nameof(ChunkedValue<T>.ChunkIndex) + "}: {" + nameof(ChunkedValue<T>.Value) + "}")]
private struct ChunkedValue<T>
{
    public ChunkedValue(T value, int chunkIndex)
    {
        this.ChunkIndex = chunkIndex;
        this.Value = value;
    }

    public int ChunkIndex { get; }

    public T Value { get; }
}

Można tego użyć w następujący sposób, który iteruje kolekcję tylko raz, a także nie przydziela żadnej znaczącej pamięci.

int chunkSize = 30;
foreach (var chunk in collection.ChunkBy(chunkSize))
{
    foreach (var item in chunk)
    {
        // your code for item here.
    }
}

Jeśli rzeczywiście potrzebna jest konkretna lista, zrobiłbym to w ten sposób:

int chunkSize = 30;
var chunkList = new List<List<T>>();
foreach (var chunk in collection.ChunkBy(chunkSize))
{
    // create a list with the correct capacity to be able to contain one chunk
    // to avoid the resizing (additional memory allocation and memory copy) within the List<T>.
    var list = new List<T>(chunkSize);
    list.AddRange(chunk);
    chunkList.Add(list);
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.