Pisahkan Daftar menjadi Sublists dengan LINQ

377

Apakah ada cara saya bisa memisahkan List<SomeObject>beberapa daftar yang terpisah SomeObject, menggunakan indeks item sebagai pembatas setiap split?

Izinkan saya memberi contoh:

Saya memiliki List<SomeObject>dan saya memerlukan List<List<SomeObject>>atau List<SomeObject>[], sehingga masing-masing daftar yang dihasilkan ini akan berisi sekelompok 3 item dari daftar asli (berurutan).

misalnya.:

  • Daftar Asli: [a, g, e, w, p, s, q, f, x, y, i, m, c]

  • Daftar hasil: [a, g, e], [w, p, s], [q, f, x], [y, i, m], [c]

Saya juga membutuhkan ukuran daftar yang dihasilkan untuk menjadi parameter dari fungsi ini.

Felipe Lima
sumber

Jawaban:

378

Coba kode berikut.

public static IList<IList<T>> Split<T>(IList<T> source)
{
    return  source
        .Select((x, i) => new { Index = i, Value = x })
        .GroupBy(x => x.Index / 3)
        .Select(x => x.Select(v => v.Value).ToList())
        .ToList();
}

Idenya adalah untuk mengelompokkan elemen pertama dengan indeks. Membaginya dengan tiga memiliki efek mengelompokkan mereka ke dalam kelompok 3. Kemudian mengkonversi setiap kelompok untuk daftar dan IEnumerabledari Listke Listdari Lists

JaredPar
sumber
21
GroupBy melakukan semacam implisit. Itu bisa mematikan kinerja. Yang kita butuhkan adalah semacam kebalikan dari SelectMany.
yfeldblum
5
@Justice, GroupBy mungkin diimplementasikan dengan hashing. Bagaimana Anda tahu implementasi GroupBy "dapat membunuh kinerja"?
Amy B
5
GroupBy tidak mengembalikan apa pun sampai semua elemen disebutkan. Itu sebabnya lambat. Daftar yang diinginkan OP berdekatan, sehingga metode yang lebih baik dapat menghasilkan sublist pertama [a,g,e]sebelum menghitung lagi daftar asli.
Kolonel Panic
9
Ambil contoh ekstrim IEnumerable yang tak terbatas. GroupBy(x=>f(x)).First()tidak akan pernah menghasilkan grup. OP bertanya tentang daftar, tetapi jika kami menulis untuk bekerja dengan IEnumerable, hanya membuat satu iterasi, kami menuai keuntungan kinerja.
Kolonel Panic
8
@Nick Order tidak dipertahankan dengan cara Anda. Masih bagus untuk diketahui tetapi Anda akan mengelompokkannya menjadi (0,3,6,9, ...), (1,4,7,10, ...), (2,5,8 , 11, ...). Jika pesanan tidak masalah maka itu baik-baik saja tetapi dalam hal ini kedengarannya itu penting.
Reafexus
325

Pertanyaan ini agak lama, tetapi saya baru saja menulis ini, dan saya pikir ini sedikit lebih elegan daripada solusi yang diusulkan lainnya:

/// <summary>
/// Break a list of items into chunks of a specific size
/// </summary>
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
{
    while (source.Any())
    {
        yield return source.Take(chunksize);
        source = source.Skip(chunksize);
    }
}
CaseyB
sumber
14
Cintai solusi ini. Saya akan merekomendasikan menambahkan cek kewarasan ini untuk mencegah loop tak terbatas: if (chunksize <= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize");
mroach
10
Saya suka ini, tetapi tidak super efisien
Sam Saffron
51
Saya suka yang ini tetapi efisiensi waktu O(n²). Anda dapat mengulangi daftar dan mendapatkan O(n)waktu.
hIpPy
8
@ hIpPy, bagaimana n ^ 2? Tampak linier bagi saya
Vivek Maharajh
13
@vivekmaharajh sourcediganti dengan yang dibungkus IEnumerablesetiap kali. Jadi mengambil elemen dari sourcelapisan Skips
Lasse Espeholt
99

Secara umum pendekatan yang disarankan oleh CaseyB berfungsi dengan baik, bahkan jika Anda mengirimkannya List<T>sulit untuk menyalahkannya, mungkin saya akan mengubahnya menjadi:

public static IEnumerable<IEnumerable<T>> ChunkTrivialBetter<T>(this IEnumerable<T> source, int chunksize)
{
   var pos = 0; 
   while (source.Skip(pos).Any())
   {
      yield return source.Skip(pos).Take(chunksize);
      pos += chunksize;
   }
}

Yang akan menghindari rantai panggilan besar-besaran. Meskipun demikian, pendekatan ini memiliki kelemahan umum. Ini mematerialisasikan dua enumerasi per potong, untuk menyoroti masalah coba jalankan:

foreach (var item in Enumerable.Range(1, int.MaxValue).Chunk(8).Skip(100000).First())
{
   Console.WriteLine(item);
}
// wait forever 

Untuk mengatasinya, kita dapat mencoba pendekatan Cameron , yang lulus tes di atas dalam warna terbang karena hanya berjalan satu kali pencacahan.

Masalahnya adalah ia memiliki cacat yang berbeda, itu mematerialisasi setiap item di setiap chunk, masalah dengan pendekatan itu adalah Anda kehabisan memori.

Untuk mengilustrasikannya, coba jalankan:

foreach (var item in Enumerable.Range(1, int.MaxValue)
               .Select(x => x + new string('x', 100000))
               .Clump(10000).Skip(100).First())
{
   Console.Write('.');
}
// OutOfMemoryException

Akhirnya, setiap implementasi harus dapat menangani keluarnya urutan potongan, misalnya:

Enumerable.Range(1,3).Chunk(2).Reverse().ToArray()
// should return [3],[1,2]

Banyak solusi yang sangat optimal seperti revisi pertama saya untuk jawaban ini gagal di sana. Masalah yang sama dapat dilihat pada jawaban optimal casperOne .

Untuk mengatasi semua masalah ini, Anda dapat menggunakan yang berikut:

namespace ChunkedEnumerator
{
    public static class Extensions 
    {
        class ChunkedEnumerable<T> : IEnumerable<T>
        {
            class ChildEnumerator : IEnumerator<T>
            {
                ChunkedEnumerable<T> parent;
                int position;
                bool done = false;
                T current;


                public ChildEnumerator(ChunkedEnumerable<T> parent)
                {
                    this.parent = parent;
                    position = -1;
                    parent.wrapper.AddRef();
                }

                public T Current
                {
                    get
                    {
                        if (position == -1 || done)
                        {
                            throw new InvalidOperationException();
                        }
                        return current;

                    }
                }

                public void Dispose()
                {
                    if (!done)
                    {
                        done = true;
                        parent.wrapper.RemoveRef();
                    }
                }

                object System.Collections.IEnumerator.Current
                {
                    get { return Current; }
                }

                public bool MoveNext()
                {
                    position++;

                    if (position + 1 > parent.chunkSize)
                    {
                        done = true;
                    }

                    if (!done)
                    {
                        done = !parent.wrapper.Get(position + parent.start, out current);
                    }

                    return !done;

                }

                public void Reset()
                {
                    // per http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.reset.aspx
                    throw new NotSupportedException();
                }
            }

            EnumeratorWrapper<T> wrapper;
            int chunkSize;
            int start;

            public ChunkedEnumerable(EnumeratorWrapper<T> wrapper, int chunkSize, int start)
            {
                this.wrapper = wrapper;
                this.chunkSize = chunkSize;
                this.start = start;
            }

            public IEnumerator<T> GetEnumerator()
            {
                return new ChildEnumerator(this);
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

        }

        class EnumeratorWrapper<T>
        {
            public EnumeratorWrapper (IEnumerable<T> source)
            {
                SourceEumerable = source;
            }
            IEnumerable<T> SourceEumerable {get; set;}

            Enumeration currentEnumeration;

            class Enumeration
            {
                public IEnumerator<T> Source { get; set; }
                public int Position { get; set; }
                public bool AtEnd { get; set; }
            }

            public bool Get(int pos, out T item) 
            {

                if (currentEnumeration != null && currentEnumeration.Position > pos)
                {
                    currentEnumeration.Source.Dispose();
                    currentEnumeration = null;
                }

                if (currentEnumeration == null)
                {
                    currentEnumeration = new Enumeration { Position = -1, Source = SourceEumerable.GetEnumerator(), AtEnd = false };
                }

                item = default(T);
                if (currentEnumeration.AtEnd)
                {
                    return false;
                }

                while(currentEnumeration.Position < pos) 
                {
                    currentEnumeration.AtEnd = !currentEnumeration.Source.MoveNext();
                    currentEnumeration.Position++;

                    if (currentEnumeration.AtEnd) 
                    {
                        return false;
                    }

                }

                item = currentEnumeration.Source.Current;

                return true;
            }

            int refs = 0;

            // needed for dispose semantics 
            public void AddRef()
            {
                refs++;
            }

            public void RemoveRef()
            {
                refs--;
                if (refs == 0 && currentEnumeration != null)
                {
                    var copy = currentEnumeration;
                    currentEnumeration = null;
                    copy.Source.Dispose();
                }
            }
        }

        public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
        {
            if (chunksize < 1) throw new InvalidOperationException();

            var wrapper =  new EnumeratorWrapper<T>(source);

            int currentPos = 0;
            T ignore;
            try
            {
                wrapper.AddRef();
                while (wrapper.Get(currentPos, out ignore))
                {
                    yield return new ChunkedEnumerable<T>(wrapper, chunksize, currentPos);
                    currentPos += chunksize;
                }
            }
            finally
            {
                wrapper.RemoveRef();
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int i = 10;
            foreach (var group in Enumerable.Range(1, int.MaxValue).Skip(10000000).Chunk(3))
            {
                foreach (var n in group)
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
                if (i-- == 0) break;
            }


            var stuffs = Enumerable.Range(1, 10).Chunk(2).ToArray();

            foreach (var idx in new [] {3,2,1})
            {
                Console.Write("idx " + idx + " ");
                foreach (var n in stuffs[idx])
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
            }

            /*

10000001 10000002 10000003
10000004 10000005 10000006
10000007 10000008 10000009
10000010 10000011 10000012
10000013 10000014 10000015
10000016 10000017 10000018
10000019 10000020 10000021
10000022 10000023 10000024
10000025 10000026 10000027
10000028 10000029 10000030
10000031 10000032 10000033
idx 3 7 8
idx 2 5 6
idx 1 3 4
             */

            Console.ReadKey();


        }

    }
}

Ada juga serangkaian optimisasi yang dapat Anda perkenalkan untuk iterasi chunk yang out-of-order, yang berada di luar cakupan di sini.

Metode apa yang harus Anda pilih? Ini benar-benar tergantung pada masalah yang Anda coba selesaikan. Jika Anda tidak peduli dengan cacat pertama, jawaban sederhana sangat menarik.

Catatan seperti pada kebanyakan metode, ini tidak aman untuk multi threading, hal-hal bisa menjadi aneh jika Anda ingin menjadikannya aman, Anda harus mengubahnya EnumeratorWrapper.

Sam Saffron
sumber
Apakah bug menjadi Enumerable.Range (0, 100) .Chunk (3) .Reverse (). ToArray () salah, atau Enumerable.Range (0, 100) .ToArray (). Chunk (3) .Reverse () .ToArray () melempar pengecualian?
Cameron MacFarland
@SamSaffron Saya telah memperbarui jawaban saya dan menyederhanakan kode ini untuk apa yang saya rasakan sebagai use case yang menonjol (dan mengakui peringatannya).
casperOne
Bagaimana dengan memotong IQueryable <>? Dugaan saya adalah bahwa pendekatan Ambil / Lewati akan optimal jika kami ingin mendelegasikan operasi maksimum ke penyedia
Guillaume86
@ Guillaume86 Saya setuju, jika Anda memiliki IList atau IQueryable, Anda dapat mengambil semua jenis pintasan yang akan membuat ini lebih cepat (Linq melakukan ini secara internal untuk semua jenis metode lain)
Sam Saffron
1
Sejauh ini, inilah jawaban terbaik untuk efisiensi. Saya mengalami masalah menggunakan SqlBulkCopy dengan IEnumerable yang menjalankan proses tambahan pada setiap kolom, sehingga harus dijalankan secara efisien hanya dengan satu pass. Ini akan memungkinkan saya untuk memecah IEnumerable menjadi potongan-potongan berukuran dikelola. (Bagi mereka yang bertanya-tanya, saya mengaktifkan mode streaming SqlBulkCopy, yang tampaknya rusak).
Brain2000
64

Anda bisa menggunakan sejumlah pertanyaan yang menggunakan Takedan Skip, tetapi itu akan menambah terlalu banyak iterasi pada daftar asli, saya percaya.

Sebaliknya, saya pikir Anda harus membuat iterator sendiri, seperti:

public static IEnumerable<IEnumerable<T>> GetEnumerableOfEnumerables<T>(
  IEnumerable<T> enumerable, int groupSize)
{
   // The list to return.
   List<T> list = new List<T>(groupSize);

   // Cycle through all of the items.
   foreach (T item in enumerable)
   {
     // Add the item.
     list.Add(item);

     // If the list has the number of elements, return that.
     if (list.Count == groupSize)
     {
       // Return the list.
       yield return list;

       // Set the list to a new list.
       list = new List<T>(groupSize);
     }
   }

   // Return the remainder if there is any,
   if (list.Count != 0)
   {
     // Return the list.
     yield return list;
   }
}

Anda kemudian dapat memanggil ini dan itu diaktifkan LINQ sehingga Anda dapat melakukan operasi lain pada urutan yang dihasilkan.


Mengingat jawaban Sam , saya merasa ada cara yang lebih mudah untuk melakukan ini tanpa:

  • Mengulangi daftar lagi (yang awalnya tidak saya lakukan)
  • Mewujudkan item dalam kelompok sebelum melepaskan bongkahan (untuk bongkahan besar item, akan ada masalah memori)
  • Semua kode yang diposting Sam

Yang mengatakan, inilah pass lain, yang telah saya kodifikasikan dalam metode ekstensi untuk IEnumerable<T>dipanggilChunk :

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
    int chunkSize)
{
    // Validate parameters.
    if (source == null) throw new ArgumentNullException("source");
    if (chunkSize <= 0) throw new ArgumentOutOfRangeException("chunkSize",
        "The chunkSize parameter must be a positive value.");

    // Call the internal implementation.
    return source.ChunkInternal(chunkSize);
}

Tidak ada yang mengejutkan di sana, hanya pengecekan kesalahan dasar.

Pindah ke ChunkInternal:

private static IEnumerable<IEnumerable<T>> ChunkInternal<T>(
    this IEnumerable<T> source, int chunkSize)
{
    // Validate parameters.
    Debug.Assert(source != null);
    Debug.Assert(chunkSize > 0);

    // Get the enumerator.  Dispose of when done.
    using (IEnumerator<T> enumerator = source.GetEnumerator())
    do
    {
        // Move to the next element.  If there's nothing left
        // then get out.
        if (!enumerator.MoveNext()) yield break;

        // Return the chunked sequence.
        yield return ChunkSequence(enumerator, chunkSize);
    } while (true);
}

Pada dasarnya, ia mendapat IEnumerator<T>dan secara manual beralih melalui setiap item. Ia memeriksa untuk melihat apakah ada item yang saat ini akan disebutkan. Setelah setiap potongan dihitung melalui, jika tidak ada barang yang tersisa, itu pecah.

Setelah mendeteksi ada item dalam urutan, itu mendelegasikan tanggung jawab untuk IEnumerable<T>implementasi batin untuk ChunkSequence:

private static IEnumerable<T> ChunkSequence<T>(IEnumerator<T> enumerator, 
    int chunkSize)
{
    // Validate parameters.
    Debug.Assert(enumerator != null);
    Debug.Assert(chunkSize > 0);

    // The count.
    int count = 0;

    // There is at least one item.  Yield and then continue.
    do
    {
        // Yield the item.
        yield return enumerator.Current;
    } while (++count < chunkSize && enumerator.MoveNext());
}

Karena MoveNextsudah dipanggil pada yang IEnumerator<T>diteruskan ke ChunkSequence, itu menghasilkan item yang dikembalikan oleh Currentdan kemudian menambah hitungan, memastikan tidak pernah kembali lebih dari chunkSizeitem dan pindah ke item berikutnya dalam urutan setelah setiap iterasi (tetapi hubung pendek jika jumlah item yang dihasilkan melebihi ukuran chunk).

Jika tidak ada item yang tersisa, maka InternalChunkmetode akan membuat pass lain di loop luar, tetapi ketika MoveNextdipanggil untuk kedua kalinya, itu akan tetap kembali salah, sesuai dokumentasi (penekanan tambang):

Jika MoveNext melewati akhir koleksi, enumerator diposisikan setelah elemen terakhir dalam koleksi dan MoveNext mengembalikan false. Ketika enumerator berada pada posisi ini, panggilan berikutnya ke MoveNext juga mengembalikan false sampai Reset dipanggil.

Pada titik ini, loop akan terputus, dan urutan urutan akan berakhir.

Ini adalah tes sederhana:

static void Main()
{
    string s = "agewpsqfxyimc";

    int count = 0;

    // Group by three.
    foreach (IEnumerable<char> g in s.Chunk(3))
    {
        // Print out the group.
        Console.Write("Group: {0} - ", ++count);

        // Print the items.
        foreach (char c in g)
        {
            // Print the item.
            Console.Write(c + ", ");
        }

        // Finish the line.
        Console.WriteLine();
    }
}

Keluaran:

Group: 1 - a, g, e,
Group: 2 - w, p, s,
Group: 3 - q, f, x,
Group: 4 - y, i, m,
Group: 5 - c,

Catatan penting, ini tidak akan berfungsi jika Anda tidak menguras seluruh urutan anak atau mematahkan pada titik mana pun dalam urutan induk. Ini adalah peringatan penting, tetapi jika kasus penggunaan Anda adalah bahwa Anda akan mengkonsumsi setiap elemen dari urutan sekuens, maka ini akan bekerja untuk Anda.

Selain itu, itu akan melakukan hal-hal aneh jika Anda bermain dengan pesanan, seperti yang dilakukan Sam pada satu titik .

casperOne
sumber
Saya pikir ini adalah solusi terbaik ... satu-satunya masalah adalah daftar itu tidak memiliki Panjang ... ia memiliki Hitungan. Tapi itu mudah diubah. Kita dapat membuat ini lebih baik dengan tidak bahkan membangun Daftar tetapi mengembalikan ienumerables yang berisi referensi ke daftar utama dengan kombinasi offset / panjang. Jadi, jika ukuran grup besar, kami tidak membuang-buang memori. Beri komentar jika Anda ingin saya menuliskannya.
Amir
@Amir saya ingin melihat yang ditulis
samandmoore
Ini bagus dan cepat - Cameron memposting yang sangat mirip juga setelah Anda, hanya peringatan adalah bahwa hal itu buffer potongan, ini dapat menyebabkan kehabisan memori jika potongan dan ukuran barang besar. Lihat jawaban saya untuk alternatif, meskipun lebih hairier, jawab.
Sam Saffron
@SamSaffron Ya, jika Anda memiliki sejumlah besar item dalam List<T>, Anda jelas akan memiliki masalah memori karena buffering. Dalam retrospeksi, saya seharusnya mencatat itu dalam jawabannya, tetapi tampaknya pada saat itu fokusnya adalah pada terlalu banyak iterasi. Yang mengatakan, solusi Anda memang lebih hairier. Saya belum mengujinya, tetapi sekarang saya bertanya-tanya apakah ada solusi yang kurang berbulu.
casperOne
@casperOne ya ... Google memberi saya halaman ini ketika saya sedang mencari cara untuk membagi enumerables, untuk kasus penggunaan khusus saya, saya membagi daftar besar catatan yang dikembalikan dari db, jika saya mewujudkannya menjadi daftar itu akan meledak (sebenarnya dapper memiliki buffer: opsi salah hanya untuk kasus penggunaan ini)
Sam Saffron
48

Oke, ini pendapat saya:

  • benar-benar malas: bekerja pada enumerables yang tak terbatas
  • tidak ada penyalinan / penyangga menengah
  • O (n) waktu eksekusi
  • bekerja juga ketika urutan bagian dalam hanya dikonsumsi sebagian

public static IEnumerable<IEnumerable<T>> Chunks<T>(this IEnumerable<T> enumerable,
                                                    int chunkSize)
{
    if (chunkSize < 1) throw new ArgumentException("chunkSize must be positive");

    using (var e = enumerable.GetEnumerator())
    while (e.MoveNext())
    {
        var remaining = chunkSize;    // elements remaining in the current chunk
        var innerMoveNext = new Func<bool>(() => --remaining > 0 && e.MoveNext());

        yield return e.GetChunk(innerMoveNext);
        while (innerMoveNext()) {/* discard elements skipped by inner iterator */}
    }
}

private static IEnumerable<T> GetChunk<T>(this IEnumerator<T> e,
                                          Func<bool> innerMoveNext)
{
    do yield return e.Current;
    while (innerMoveNext());
}

Contoh Penggunaan

var src = new [] {1, 2, 3, 4, 5, 6}; 

var c3 = src.Chunks(3);      // {{1, 2, 3}, {4, 5, 6}}; 
var c4 = src.Chunks(4);      // {{1, 2, 3, 4}, {5, 6}}; 

var sum   = c3.Select(c => c.Sum());    // {6, 15}
var count = c3.Count();                 // 2
var take2 = c3.Select(c => c.Take(2));  // {{1, 2}, {4, 5}}

Penjelasan

Kode berfungsi dengan menyarangkan dua yield iterator berbasis.

Iterator luar harus melacak berapa banyak elemen yang telah dikonsumsi secara efektif oleh iterator bagian dalam (chunk). Ini dilakukan dengan menutup remainingdengan innerMoveNext(). Unsur-unsur potongan yang tidak dikonsumsi dibuang sebelum potongan berikutnya dihasilkan oleh iterator luar. Ini diperlukan karena jika tidak, Anda mendapatkan hasil yang tidak konsisten, ketika enumerables bagian dalam tidak (sepenuhnya) dikonsumsi (misalnya c3.Count()akan mengembalikan 6).

Catatan: Jawabannya telah diperbarui untuk mengatasi kekurangan yang ditunjukkan oleh @aolszowka.

3dGrabber
sumber
2
Sangat bagus. Solusi "benar" saya jauh lebih rumit dari itu. Ini adalah jawaban # 1 IMHO.
CaseyB
Ini menderita dari perilaku tak terduga (dari sudut pandang API) ketika ToArray () dipanggil, itu juga tidak aman untuk thread.
aolszowka
@ aolszowka: bisa tolong jelaskan?
3dGrabber
@ 3dGrabber Mungkin karena cara saya memfaktorkan ulang kode Anda (maaf agak terlalu lama untuk melewati di sini, pada dasarnya alih-alih metode ekstensi yang saya berikan pada sourceEnumerator). Kasing uji yang saya gunakan adalah sesuatu untuk efek ini: int [] arrayToSort = new int [] {9, 7, 2, 6, 3, 4, 8, 5, 1, 10, 11, 12, 13}; var source = Chunkify <int> (arrayToSort, 3) .ToArray (); Hasilnya dalam Sumber yang menunjukkan bahwa ada 13 bongkahan (jumlah elemen). Ini masuk akal bagi saya karena kecuali Anda menanyakan enumerasi bagian dalam, Enumerator tidak bertambah.
aolszowka
1
@ aolszowka: poin yang sangat valid. Saya telah menambahkan bagian peringatan dan penggunaan. Kode ini mengasumsikan bahwa Anda mengulangi bagian dalam enumerable. Dengan solusi Anda, Anda kehilangan kemalasan. Saya pikir itu harus mungkin untuk mendapatkan yang terbaik dari kedua dunia dengan kebiasaan, caching IEnumerator. Jika saya menemukan solusi, saya akan mempostingnya di sini ...
3dGrabber
18

benar-benar malas, tidak ada penghitungan atau penyalinan:

public static class EnumerableExtensions
{

  public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int len)
  {
     if (len == 0)
        throw new ArgumentNullException();

     var enumer = source.GetEnumerator();
     while (enumer.MoveNext())
     {
        yield return Take(enumer.Current, enumer, len);
     }
  }

  private static IEnumerable<T> Take<T>(T head, IEnumerator<T> tail, int len)
  {
     while (true)
     {
        yield return head;
        if (--len == 0)
           break;
        if (tail.MoveNext())
           head = tail.Current;
        else
           break;
     }
  }
}
xtofs
sumber
Solusi ini sangat elegan sehingga saya menyesal tidak dapat menjawab pertanyaan ini lebih dari satu kali.
Tandai
3
Saya kira ini tidak akan pernah gagal. Tapi itu pasti bisa memiliki beberapa perilaku aneh. Jika Anda memiliki 100 item, dan Anda membaginya menjadi batch 10, dan Anda menghitung semua batch tanpa menyebutkan item apa pun dari batch tersebut, Anda akan berakhir dengan 100 batch dari 1.
CaseyB
1
Seperti @CaseyB sebutkan, ini menderita dari 3dGrabber gagal yang sama yang dibahas di sini stackoverflow.com/a/20953521/1037948 , tetapi manusia itu cepat!
drzaus
1
Ini solusi yang indah. Melakukan persis apa yang dijanjikannya.
Rod Hartzell
Sejauh ini solusi yang paling elegan dan to the point. Satu-satunya hal adalah, Anda harus menambahkan tanda centang untuk angka negatif, dan mengganti ArgumentNullException dengan ArgumentException
Romain Vergnory
13

Saya pikir saran berikut akan menjadi yang tercepat. Saya mengorbankan kemalasan sumber yang tak terhitung jumlahnya untuk kemampuan menggunakan Array. Salin dan ketahui sebelumnya berapa lama masing-masing sublists saya.

public static IEnumerable<T[]> Chunk<T>(this IEnumerable<T> items, int size)
{
    T[] array = items as T[] ?? items.ToArray();
    for (int i = 0; i < array.Length; i+=size)
    {
        T[] chunk = new T[Math.Min(size, array.Length - i)];
        Array.Copy(array, i, chunk, 0, chunk.Length);
        yield return chunk;
    }
}
Marc-André Bertrand
sumber
Bukan hanya tercepat, ia juga menangani dengan benar operasi enumerable lebih lanjut pada hasilnya, yaitu item.Chunk (5) .Reverse (). SelectMany (x => x)
juga
9

Kami dapat meningkatkan solusi @ JaredPar untuk melakukan evaluasi malas yang sebenarnya. Kami menggunakan aGroupAdjacentBy metode yang menghasilkan grup elemen berurutan dengan kunci yang sama:

sequence
.Select((x, i) => new { Value = x, Index = i })
.GroupAdjacentBy(x=>x.Index/3)
.Select(g=>g.Select(x=>x.Value))

Karena grup dihasilkan satu-per-satu, solusi ini bekerja secara efisien dengan urutan panjang atau tak terbatas.

Kolonel Panic
sumber
8

Saya menulis metode ekstensi Clump beberapa tahun yang lalu. Bekerja dengan baik, dan merupakan implementasi tercepat di sini. : P

/// <summary>
/// Clumps items into same size lots.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source">The source list of items.</param>
/// <param name="size">The maximum size of the clumps to make.</param>
/// <returns>A list of list of items, where each list of items is no bigger than the size given.</returns>
public static IEnumerable<IEnumerable<T>> Clump<T>(this IEnumerable<T> source, int size)
{
    if (source == null)
        throw new ArgumentNullException("source");
    if (size < 1)
        throw new ArgumentOutOfRangeException("size", "size must be greater than 0");

    return ClumpIterator<T>(source, size);
}

private static IEnumerable<IEnumerable<T>> ClumpIterator<T>(IEnumerable<T> source, int size)
{
    Debug.Assert(source != null, "source is null.");

    T[] items = new T[size];
    int count = 0;
    foreach (var item in source)
    {
        items[count] = item;
        count++;

        if (count == size)
        {
            yield return items;
            items = new T[size];
            count = 0;
        }
    }
    if (count > 0)
    {
        if (count == size)
            yield return items;
        else
        {
            T[] tempItems = new T[count];
            Array.Copy(items, tempItems, count);
            yield return tempItems;
        }
    }
}
Cameron MacFarland
sumber
itu harus bekerja tetapi buffering 100% dari potongan, saya mencoba untuk menghindari itu ... tapi ternyata sangat berbulu.
Sam Saffron
@SamSaffron Yap. Terutama jika Anda memasukkan hal-hal seperti plinq ke dalam campuran, untuk itulah implementasi saya awalnya.
Cameron MacFarland
memperluas jawaban saya, beri tahu saya apa pendapat Anda
Sam Saffron
@CameronMacFarland - dapatkah Anda menjelaskan mengapa pemeriksaan kedua untuk jumlah == diperlukan? Terima kasih.
dugas
8

System.Interactive menyediakan Buffer()untuk tujuan ini. Beberapa pengujian cepat menunjukkan kinerja mirip dengan solusi Sam.

dahlbyk
sumber
1
Anda tahu semantik buffering? misal: jika Anda memiliki enumerator yang mengeluarkan string berukuran 300k dan mencoba untuk memecahnya menjadi 10.000 ukuran, apakah Anda akan mendapatkan memori?
Sam Saffron
Buffer()kembali IEnumerable<IList<T>>jadi ya, Anda mungkin memiliki masalah di sana - tidak mengalir seperti milik Anda.
dahlbyk
7

Berikut adalah daftar rutin yang saya tulis beberapa bulan yang lalu:

public static List<List<T>> Chunk<T>(
    List<T> theList,
    int chunkSize
)
{
    List<List<T>> result = theList
        .Select((x, i) => new {
            data = x,
            indexgroup = i / chunkSize
        })
        .GroupBy(x => x.indexgroup, x => x.data)
        .Select(g => new List<T>(g))
        .ToList();

    return result;
}
Amy B
sumber
6

Saya menemukan potongan kecil ini melakukan pekerjaan dengan cukup baik.

public static IEnumerable<List<T>> Chunked<T>(this List<T> source, int chunkSize)
{
    var offset = 0;

    while (offset < source.Count)
    {
        yield return source.GetRange(offset, Math.Min(source.Count - offset, chunkSize));
        offset += chunkSize;
    }
}
erlando
sumber
5

Bagaimana dengan yang ini?

var input = new List<string> { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };
var k = 3

var res = Enumerable.Range(0, (input.Count - 1) / k + 1)
                    .Select(i => input.GetRange(i * k, Math.Min(k, input.Count - i * k)))
                    .ToList();

Sejauh yang saya tahu, GetRange () adalah linier dalam hal jumlah item yang diambil. Jadi ini harus berkinerja baik.

Roman Pekar
sumber
5

Ini adalah pertanyaan lama tetapi inilah yang akhirnya saya dapatkan; itu menghitung enumerable hanya sekali, tetapi membuat daftar untuk masing-masing partisi. Itu tidak menderita dari perilaku tak terduga ketika ToArray()dipanggil seperti beberapa implementasi lakukan:

    public static IEnumerable<IEnumerable<T>> Partition<T>(IEnumerable<T> source, int chunkSize)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }

        if (chunkSize < 1)
        {
            throw new ArgumentException("Invalid chunkSize: " + chunkSize);
        }

        using (IEnumerator<T> sourceEnumerator = source.GetEnumerator())
        {
            IList<T> currentChunk = new List<T>();
            while (sourceEnumerator.MoveNext())
            {
                currentChunk.Add(sourceEnumerator.Current);
                if (currentChunk.Count == chunkSize)
                {
                    yield return currentChunk;
                    currentChunk = new List<T>();
                }
            }

            if (currentChunk.Any())
            {
                yield return currentChunk;
            }
        }
    }
aolszowka
sumber
Akan bagus untuk mengonversikan ini menjadi metode Extension:public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int chunkSize)
krizzzn
+1 untuk jawaban Anda. Namun saya merekomendasikan dua hal 1. menggunakan foreach bukan saat dan menggunakan blok. 2. Lulus chunkSize di konstruktor Daftar sehingga daftar mengetahui ukuran maksimum yang diharapkan.
Usman Zafar
4

Kami menemukan solusi David B bekerja paling baik. Tetapi kami mengadaptasinya ke solusi yang lebih umum:

list.GroupBy(item => item.SomeProperty) 
   .Select(group => new List<T>(group)) 
   .ToArray();
mwjackson
sumber
3
Ini bagus, tetapi sangat berbeda dari apa yang diminta oleh penanya semula.
Amy B
4

Solusi berikut ini adalah yang paling ringkas yang bisa saya buat yaitu O (n).

public static IEnumerable<T[]> Chunk<T>(IEnumerable<T> source, int chunksize)
{
    var list = source as IList<T> ?? source.ToList();
    for (int start = 0; start < list.Count; start += chunksize)
    {
        T[] chunk = new T[Math.Min(chunksize, list.Count - start)];
        for (int i = 0; i < chunk.Length; i++)
            chunk[i] = list[start + i];

        yield return chunk;
    }
}
Marc-André Bertrand
sumber
4

Kode lama, tapi ini yang saya gunakan:

    public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
    {
        var toReturn = new List<T>(max);
        foreach (var item in source)
        {
            toReturn.Add(item);
            if (toReturn.Count == max)
            {
                yield return toReturn;
                toReturn = new List<T>(max);
            }
        }
        if (toReturn.Any())
        {
            yield return toReturn;
        }
    }
Robert McKee
sumber
Setelah posting, saya menyadari ini hampir persis kode yang sama casperOne diposting 6 tahun yang lalu dengan perubahan menggunakan .Any () bukannya .Count () karena saya tidak perlu seluruh jumlah, hanya perlu tahu apakah ada .
Robert McKee
3

Jika daftar ini bertipe system.collections.generic Anda dapat menggunakan metode "CopyTo" yang tersedia untuk menyalin elemen array Anda ke sub array lainnya. Anda menentukan elemen awal dan jumlah elemen yang akan disalin.

Anda juga dapat membuat 3 klon dari daftar asli Anda dan menggunakan "RemoveRange" pada setiap daftar untuk mengecilkan daftar ke ukuran yang Anda inginkan.

Atau buat metode pembantu untuk melakukannya untuk Anda.

Jobo
sumber
2

Ini solusi lama tapi saya punya pendekatan berbeda. Saya gunakan Skipuntuk pindah ke offset yang diinginkan dan Takeuntuk mengekstrak jumlah elemen yang diinginkan:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
                                                   int chunkSize)
{
    if (chunkSize <= 0)
        throw new ArgumentOutOfRangeException($"{nameof(chunkSize)} should be > 0");

    var nbChunks = (int)Math.Ceiling((double)source.Count()/chunkSize);

    return Enumerable.Range(0, nbChunks)
                     .Select(chunkNb => source.Skip(chunkNb*chunkSize)
                     .Take(chunkSize));
}
Bertrand
sumber
1
Sangat mirip dengan pendekatan yang saya gunakan, tetapi saya merekomendasikan bahwa sumber tidak dapat dihitung. Sebagai contoh, jika sumber adalah hasil dari query LINQ, Skip / Take akan memicu enumerasi nbChunk dari query. Bisa jadi mahal. Lebih baik menggunakan IList atau ICollection sebagai jenis sumber. Itu menghindari masalah sama sekali.
RB Davidson
2

Bagi siapa pun yang tertarik dengan solusi yang dipaket / dipelihara, perpustakaan MoreLINQ menyediakan Batchmetode ekstensi yang cocok dengan perilaku yang Anda minta:

IEnumerable<char> source = "Example string";
IEnumerable<IEnumerable<char>> chunksOfThreeChars = source.Batch(3);

The Batchpelaksanaan mirip dengan jawaban Cameron MacFarland ini , dengan penambahan kelebihan beban untuk mengubah potongan / batch sebelum kembali, dan melakukan cukup baik.

Kevinoid
sumber
ini harus menjadi jawaban yang diterima. Alih-alih menciptakan kembali roda, morelinq harus digunakan
Otabek Kholikov
1

Menggunakan partisi modular:

public IEnumerable<IEnumerable<string>> Split(IEnumerable<string> input, int chunkSize)
{
    var chunks = (int)Math.Ceiling((double)input.Count() / (double)chunkSize);
    return Enumerable.Range(0, chunks).Select(id => input.Where(s => s.GetHashCode() % chunks == id));
}
Janosz G.
sumber
1

Hanya memasukkan dua sen saya. Jika Anda ingin "menyatukan" daftar (visualisasikan dari kiri ke kanan), Anda dapat melakukan hal berikut:

 public static List<List<T>> Buckets<T>(this List<T> source, int numberOfBuckets)
    {
        List<List<T>> result = new List<List<T>>();
        for (int i = 0; i < numberOfBuckets; i++)
        {
            result.Add(new List<T>());
        }

        int count = 0;
        while (count < source.Count())
        {
            var mod = count % numberOfBuckets;
            result[mod].Add(source[count]);
            count++;
        }
        return result;
    }
mattylantz
sumber
1

Cara lain menggunakan operator Rx Buffer

//using System.Linq;
//using System.Reactive.Linq;
//using System.Reactive.Threading.Tasks;

var observableBatches = anAnumerable.ToObservable().Buffer(size);

var batches = aList.ToObservable().Buffer(size).ToList().ToTask().GetAwaiter().GetResult();
frhack
sumber
IMHO jawaban yang paling porper.
Stanislav Berkov
1
public static List<List<T>> GetSplitItemsList<T>(List<T> originalItemsList, short number)
    {
        var listGroup = new List<List<T>>();
        int j = number;
        for (int i = 0; i < originalItemsList.Count; i += number)
        {
            var cList = originalItemsList.Take(j).Skip(i).ToList();
            j += number;
            listGroup.Add(cList);
        }
        return listGroup;
    }
Joy Zhu
sumber
0

Saya mengambil jawaban utama dan membuatnya menjadi wadah IOC untuk menentukan di mana harus dibagi. ( Untuk siapa yang benar-benar hanya ingin membagi 3 item, dalam membaca posting ini sambil mencari jawaban? )

Metode ini memungkinkan seseorang untuk membagi pada setiap jenis item sesuai kebutuhan.

public static List<List<T>> SplitOn<T>(List<T> main, Func<T, bool> splitOn)
{
    int groupIndex = 0;

    return main.Select( item => new 
                             { 
                               Group = (splitOn.Invoke(item) ? ++groupIndex : groupIndex), 
                               Value = item 
                             })
                .GroupBy( it2 => it2.Group)
                .Select(x => x.Select(v => v.Value).ToList())
                .ToList();
}

Jadi untuk OP kodenya

var it = new List<string>()
                       { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };

int index = 0; 
var result = SplitOn(it, (itm) => (index++ % 3) == 0 );
GamegaMan
sumber
0

Begitu performatik seperti pendekatan Sam Saffron .

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size), "Size must be greater than zero.");

    return BatchImpl(source, size).TakeWhile(x => x.Any());
}

static IEnumerable<IEnumerable<T>> BatchImpl<T>(this IEnumerable<T> source, int size)
{
    var values = new List<T>();
    var group = 1;
    var disposed = false;
    var e = source.GetEnumerator();

    try
    {
        while (!disposed)
        {
            yield return GetBatch(e, values, group, size, () => { e.Dispose(); disposed = true; });
            group++;
        }
    }
    finally
    {
        if (!disposed)
            e.Dispose();
    }
}

static IEnumerable<T> GetBatch<T>(IEnumerator<T> e, List<T> values, int group, int size, Action dispose)
{
    var min = (group - 1) * size + 1;
    var max = group * size;
    var hasValue = false;

    while (values.Count < min && e.MoveNext())
    {
        values.Add(e.Current);
    }

    for (var i = min; i <= max; i++)
    {
        if (i <= values.Count)
        {
            hasValue = true;
        }
        else if (hasValue = e.MoveNext())
        {
            values.Add(e.Current);
        }
        else
        {
            dispose();
        }

        if (hasValue)
            yield return values[i - 1];
        else
            yield break;
    }
}

}

Leandromoh
sumber
0

Dapat bekerja dengan generator yang tak terbatas:

a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1)))
 .Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1)))
 .Where((x, i) => i % 3 == 0)

Kode demo: https://ideone.com/GKmL7M

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

public class Test
{
  private static void DoIt(IEnumerable<int> a)
  {
    Console.WriteLine(String.Join(" ", a));

    foreach (var x in a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1))).Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1))).Where((x, i) => i % 3 == 0))
      Console.WriteLine(String.Join(" ", x));

    Console.WriteLine();
  }

  public static void Main()
  {
    DoIt(new int[] {1});
    DoIt(new int[] {1, 2});
    DoIt(new int[] {1, 2, 3});
    DoIt(new int[] {1, 2, 3, 4});
    DoIt(new int[] {1, 2, 3, 4, 5});
    DoIt(new int[] {1, 2, 3, 4, 5, 6});
  }
}
1

1 2

1 2 3
1 2 3

1 2 3 4
1 2 3

1 2 3 4 5
1 2 3

1 2 3 4 5 6
1 2 3
4 5 6

Tapi sebenarnya saya lebih suka menulis metode yang sesuai tanpa LINQ.

Qwertiy
sumber
0

Lihat ini! Saya memiliki daftar elemen dengan penghitung urutan dan tanggal. Untuk setiap kali urutan dimulai ulang, saya ingin membuat daftar baru.

Ex. daftar pesan.

 List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

Saya ingin membagi daftar menjadi daftar terpisah ketika penghitung dimulai ulang. Ini kodenya:

var arraylist = new List<List<dynamic>>();

        List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

        //group by FcntUp and CommTimestamp
        var query = messages.GroupBy(x => new { x.FcntUp, x.CommTimestamp });

        //declare the current item
        dynamic currentItem = null;

        //declare the list of ranges
        List<dynamic> range = null;

        //loop through the sorted list
        foreach (var item in query)
        {
            //check if start of new range
            if (currentItem == null || item.Key.FcntUp < currentItem.Key.FcntUp)
            {
                //create a new list if the FcntUp starts on a new range
                range = new List<dynamic>();

                //add the list to the parent list
                arraylist.Add(range);
            }

            //add the item to the sublist
            range.Add(item);

            //set the current item
            currentItem = item;
        }
Claes-Philip Staiger
sumber
-1

Untuk memasukkan dua sen saya ...

Dengan menggunakan tipe daftar untuk sumber yang akan dipotong, saya menemukan solusi yang sangat kompak:

public static IEnumerable<IEnumerable<TSource>> Chunk<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
    // copy the source into a list
    var chunkList = source.ToList();

    // return chunks of 'chunkSize' items
    while (chunkList.Count > chunkSize)
    {
        yield return chunkList.GetRange(0, chunkSize);
        chunkList.RemoveRange(0, chunkSize);
    }

    // return the rest
    yield return chunkList;
}
Patrick
sumber