Linq'te gruplar oluşturun


105

Birisi, linq'te belirli bir boyutta partiler oluşturmanın bir yolunu önerebilir mi?

İdeal olarak, bazı yapılandırılabilir miktardaki parçalar halinde işlemler gerçekleştirebilmek istiyorum.

Yanıtlar:


116

Herhangi bir kod yazmanıza gerek yok. Kaynak dizisini boyutlu paketler halinde gruplayan MoreLINQ Batch yöntemini kullanın (MoreLINQ, yükleyebileceğiniz bir NuGet paketi olarak mevcuttur):

int size = 10;
var batches = sequence.Batch(size);

Hangisi şu şekilde uygulanır:

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
                  this IEnumerable<TSource> source, int size)
{
    TSource[] bucket = null;
    var count = 0;

    foreach (var item in source)
    {
        if (bucket == null)
            bucket = new TSource[size];

        bucket[count++] = item;
        if (count != size)
            continue;

        yield return bucket;

        bucket = null;
        count = 0;
    }

    if (bucket != null && count > 0)
        yield return bucket.Take(count).ToArray();
}

3
Öğe başına 4 bayt korkunç bir performans mı? Korkunç ne anlama geldiğini gösteren bazı testleriniz var mı? Belleğe milyonlarca öğe yüklüyorsanız, bunu yapmam. Sunucu tarafı sayfalamayı kullan
Sergey Berezovskiy

4
Sizi kırmak istemem ama hiç birikmeyen daha basit çözümler var. Dahası, bu, var olmayan öğeler için bile alan tahsis edecektir:Batch(new int[] { 1, 2 }, 1000000)
Nick Whaley

7
@NickWhaley, ek alan tahsis edileceği konusunda sizinle aynı fikirdeyim, ancak gerçek hayatta genellikle tam tersi bir durumunuz var - 50'lik gruplar halinde yapılması gereken 1000 öğenin listesi :)
Sergey Berezovskiy

1
Evet, durum genellikle tam tersi olmalıdır, ancak gerçek hayatta bunlar kullanıcı girdileri olabilir.
Nick Whaley

8
Bu mükemmel bir çözüm. Gerçek hayatta siz: kullanıcı girdisini doğrulayın, partileri tüm öğe koleksiyonları olarak ele alın (bu, öğeleri her halükarda biriktirir) ve sıklıkla paralel olarak işleyin (bu, yineleyici yaklaşımı tarafından desteklenmez ve bilmediğiniz sürece kötü bir sürpriz olur). uygulama ayrıntıları).
Michael Petito

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

ve kullanım şöyle olacaktır:

List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

foreach(var batch in list.Batch(3))
{
    Console.WriteLine(String.Join(",",batch));
}

ÇIKTI:

0,1,2
3,4,5
6,7,8
9

Benim için mükemmel çalıştı
FunMatters

16
Bir kez GroupBybaşladığında numaralandırma, tam kaynağını numaralandırmak zorunda değildir? Bu, kaynağın tembel değerlendirmesini ve dolayısıyla bazı durumlarda, gruplamanın tüm faydalarını kaybeder!
ErikE

1
Vay canına, teşekkürler, beni delilikten kurtardın. Çok iyi çalışıyor
Riaan de Lange

4
@ErikE'nin de belirttiği gibi, bu yöntem kaynağını tam olarak numaralandırır, bu nedenle güzel görünmesine rağmen tembel değerlendirme / ardışık
düzen oluşturma

1
Bunu yapın - mevcut bir şeyler bloğunu performant işleme için daha küçük parçalara ayırmanız gerektiğinde tamamen uygundur. Alternatif, toplu işleri manuel olarak böldüğünüz ve yine de tüm kaynağı gözden geçirdiğiniz brüt arama döngüsüdür.
StingyJack

31

Bir sequenceolarak tanımlanmış olarak başlarsanız IEnumerable<T>ve birden çok kez güvenli bir şekilde numaralandırılabileceğini biliyorsanız (örneğin, bir dizi veya bir liste olduğu için), öğeleri toplu işlerde işlemek için bu basit modeli kullanabilirsiniz:

while (sequence.Any())
{
    var batch = sequence.Take(10);
    sequence = sequence.Skip(10);

    // do whatever you need to do with each batch here
}

2
Fazla kod
olmadan

5
@DevHawk: öyle. Bununla birlikte, performansın büyük (r) koleksiyonlarda katlanarak azalacağını unutmayın .
RobIII

28

Yukarıdakilerin tümü, büyük gruplar veya düşük bellek alanıyla korkunç performans gösterir. Boru hattını kendim yazmam gerekiyordu (hiçbir yerde öğe birikimine dikkat edin):

public static class BatchLinq {
    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size) {
        if (size <= 0)
            throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");

        using (IEnumerator<T> enumerator = source.GetEnumerator())
            while (enumerator.MoveNext())
                yield return TakeIEnumerator(enumerator, size);
    }

    private static IEnumerable<T> TakeIEnumerator<T>(IEnumerator<T> source, int size) {
        int i = 0;
        do
            yield return source.Current;
        while (++i < size && source.MoveNext());
    }
}

Düzenleme: Bu yaklaşımla ilgili bilinen sorun, her partinin bir sonraki partiye geçmeden önce tam olarak numaralandırılması ve numaralandırılması gerektiğidir. Örneğin bu çalışmıyor:

//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())

1
Yukarıda yayınlanan @LB rutini de materyal biriktirme yapmaz.
neontapir

3
@neontapir Hala var. Önce beş sent, sonra on sent veren bir madeni para ayırma makinesi, daha fazla bozuk para olmadığından emin olmak için size bir kuruş vermeden önce her bir madeni parayı incelemek ZORUNDADIR.
Nick Whaley

2
Ahhh ahha, bu kodu ele geçirdiğimde düzenleme notunuzu kaçırdım. Numaralandırılmamış gruplar üzerinde yinelemenin neden orijinal koleksiyonun tamamını (!!!) numaralandırdığını, her biri 1 öğeyi numaralandıran (burada X, orijinal koleksiyon öğelerinin sayısıdır) X grubu sağladığını anlamak biraz zaman aldı.
eli

2
@NickWhaley IEnumerable <IEnumerable <T>> sonucuna kodunuzdan Count () yaparsam yanlış cevap veriyor, beklenen toplam parti sayısı beklendiğinde toplam eleman sayısı veriyor. MoreLinq Parti kodunda durum böyle değil
Mrinal Kamboj

1
@JohnZabroski - İşte hızlı bir özet
Matt Murrell

24

Bu tamamen tembel, düşük ek yük, Batch'in herhangi bir biriktirme yapmayan tek işlevli uygulamasıdır. Nick Whaley'nin çözümüne dayanır (ve sorunları giderir) EricRoller yardımıyla.

Yineleme doğrudan temeldeki IEnumerable'dan gelir, bu nedenle öğeler kesin sırayla numaralandırılmalı ve bir kereden fazla erişilmemelidir. Bazı öğeler bir iç döngüde tüketilmezse, atılırlar (ve kaydedilmiş bir yineleyici aracılığıyla bunlara yeniden erişmeye çalışmak,InvalidOperationException: Enumeration already finished. ).

NET Fiddle'da eksiksiz bir örneği test edebilirsiniz .

public static class BatchLinq
{
    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
    {
        if (size <= 0)
            throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");
        using (var enumerator = source.GetEnumerator())
            while (enumerator.MoveNext())
            {
                int i = 0;
                // Batch is a local function closing over `i` and `enumerator` that
                // executes the inner batch enumeration
                IEnumerable<T> Batch()
                {
                    do yield return enumerator.Current;
                    while (++i < size && enumerator.MoveNext());
                }

                yield return Batch();
                while (++i < size && enumerator.MoveNext()); // discard skipped items
            }
    }
}

2
Bu, buradaki tek tamamen tembel uygulama. Python itertools.GroupBy ile uyumludur.
Eric Roller

1
Her donezaman e.Count()sonra arayarak çeki ortadan kaldırabilirsiniz yield return e. Tanımlanmamış davranışı source.Currentif çağırmamak için BatchInner'daki döngüyü yeniden düzenlemeniz gerekir i >= size. Bu, BatchInnerher parti için yeni bir ayırma ihtiyacını ortadan kaldıracaktır .
Eric Roller

1
Haklısınız, yine de her partinin ilerleyişi hakkında bilgi toplamanız gerekiyor. Her partiden 2. öğeyi almaya çalışırsanız kodunuzda bir hata buldum: hata keman . Ayrı bir sınıf olmadan (C # 7 kullanarak) sabit uygulama burada: sabit keman . CLR'nin değişkeni yakalamak için döngü başına bir kez yerel işlevi oluşturmasını beklediğime dikkat edin, ibu nedenle bu, ayrı bir sınıf tanımlamaktan daha verimli olmayabilir, ancak biraz daha temiz olduğunu düşünüyorum.
Eric Roller

1
Bu sürümü BenchmarkDotNet kullanarak System.Reactive.Linq.EnumerableEx.Buffer ile karşılaştırdım ve uygulamanız güvenlik riski altında 3-4 daha hızlıydı. Dahili olarak, EnumerableEx.Buffer bir Sıra Listesi ayırır
John Zabroski

1
Bunun arabelleğe alınmış bir sürümünü istiyorsanız, şunları yapabilirsiniz: public static IEnumerable <IReadOnlyList <T>> BatchBuffered <T> (this IEnumerable <T> source, int size) => Batch (source, size). (Chunk = > (IReadOnlyList <T>) yığın.ToList ()); IReadOnlyList <T> kullanımı, kullanıcıya çıktının önbelleğe alındığını belirtmek içindir. Bunun yerine IEnumerable <IEnumerable <T>> öğesini de tutabilirsiniz.
gfache

11

Merak ediyorum, neden hiç kimse eski tarz bir for-loop çözümü yayınlamadı. İşte burada:

List<int> source = Enumerable.Range(1,23).ToList();
int batchsize = 10;
for (int i = 0; i < source.Count; i+= batchsize)
{
    var batch = source.Skip(i).Take(batchsize);
}

Bu basitlik mümkündür çünkü Take yöntemi:

... sourceöğeler verilinceye countveya sourcebaşka öğe içermeyene kadar öğeleri numaralandırır ve verir . Eğer counteleman sayısını aşarsa source, tüm unsurları sourcedöndürülür

Feragatname:

Döngünün içinde Atla ve Al seçeneğinin kullanılması, numaralandırılabilirin birden çok kez numaralandırılacağı anlamına gelir. Numaralandırılırsa bu tehlikelidir. Bir veritabanı sorgusunun veya bir web isteğinin veya bir dosyanın okunmasının birden çok yürütülmesine neden olabilir. Bu örnek, açık bir şekilde ertelenmemiş bir Listenin kullanımı içindir, bu nedenle daha az sorun teşkil eder. Skip, her çağrıldığında koleksiyonu numaralandıracağından, hala yavaş bir çözümdür.

Bu, GetRangeyöntem kullanılarak da çözülebilir , ancak olası bir dinlenme grubunu çıkarmak için ekstra bir hesaplama gerektirir:

for (int i = 0; i < source.Count; i += batchsize)
{
    int remaining = source.Count - i;
    var batch = remaining > batchsize  ? source.GetRange(i, batchsize) : source.GetRange(i, remaining);
}

İşte bunu halletmenin 2 döngü ile çalışan üçüncü bir yolu. Bu, koleksiyonun yalnızca 1 kez numaralandırılmasını sağlar !:

int batchsize = 10;
List<int> batch = new List<int>(batchsize);

for (int i = 0; i < source.Count; i += batchsize)
{
    // calculated the remaining items to avoid an OutOfRangeException
    batchsize = source.Count - i > batchsize ? batchsize : source.Count - i;
    for (int j = i; j < i + batchsize; j++)
    {
        batch.Add(source[j]);
    }           
    batch.Clear();
}

2
Çok güzel çözüm. İnsanlar döngü için kullanmayı unuttu
VitalickS

1
Kullanılması Skipve Takeenumerable birden çok kez numaralandırılan döngü vasıtası iç. Numaralandırılabilir ertelenmişse bu tehlikelidir. Bir veritabanı sorgusunun veya bir web isteğinin veya bir dosyanın okunmasının birden çok yürütülmesine neden olabilir. Örnekte, bir var Listbir sorun daha azdır bu nedenle, ertelenmiş edilememesinden kaynaklanmaktadır.
Theodor Zoulias

@TheodorZoulias evet biliyorum, bu yüzden bugün ikinci çözümü yayınladım. Yorumunuzu bir sorumluluk reddi olarak gönderdim, çünkü oldukça iyi formüle ettiniz, size alıntı yapayım mı?
Mong Zhu

Koleksiyonun yalnızca 1 kez numaralandırılması için 2 döngü ile üçüncü bir çözüm yazdım. atlama olayı çok verimsiz bir çözüm
Mong Zhu

4

MoreLINQ ile aynı yaklaşım, ancak Array yerine List'i kullanıyor. Kıyaslama yapmadım, ancak okunabilirlik bazı insanlar için daha önemli:

    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
    {
        List<T> batch = new List<T>();

        foreach (var item in source)
        {
            batch.Add(item);

            if (batch.Count >= size)
            {
                yield return batch;
                batch.Clear();
            }
        }

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

1
Toplu iş değişkenini yeniden KULLANMAMALISINIZ. Tüketicileriniz bundan tamamen mahvolabilir. Ayrıca, boyutunu optimize etmek için sizeparametreyi sizin new Listhesabınıza iletin.
ErikE

1
Kolay düzeltme: değiştirmek batch.Clear();ilebatch = new List<T>();
NetMage

4

İşte Nick Whaley'nin ( bağlantı ) ve infogulch'un ( bağlantı ) tembel Batchuygulamalarının iyileştirilmesi denenmiştir . Bu katı. Ya partileri doğru sırada numaralandırırsınız ya da bir istisna alırsınız.

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
    this IEnumerable<TSource> source, int size)
{
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
    using (var enumerator = source.GetEnumerator())
    {
        int i = 0;
        while (enumerator.MoveNext())
        {
            if (i % size != 0) throw new InvalidOperationException(
                "The enumeration is out of order.");
            i++;
            yield return GetBatch();
        }
        IEnumerable<TSource> GetBatch()
        {
            while (true)
            {
                yield return enumerator.Current;
                if (i % size == 0 || !enumerator.MoveNext()) break;
                i++;
            }
        }
    }
}

Ve burada Batchtür kaynakları için tembel bir uygulama IList<T>. Bu, numaralandırmaya herhangi bir kısıtlama getirmez. Partiler kısmen, herhangi bir sırayla ve birden fazla numaralandırılabilir. Numaralandırma sırasında koleksiyonu değiştirmeme kısıtlaması yine de yürürlüktedir. Bu, enumerator.MoveNext()herhangi bir parça veya öğe vermeden önce sahte bir çağrı yaparak elde edilir . Olumsuz yanı, numaralandırmanın ne zaman biteceği bilinmediğinden, numaralandırıcının elden çıkarılmamış olmasıdır.

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
    this IList<TSource> source, int size)
{
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
    var enumerator = source.GetEnumerator();
    for (int i = 0; i < source.Count; i += size)
    {
        enumerator.MoveNext();
        yield return GetChunk(i, Math.Min(i + size, source.Count));
    }
    IEnumerable<TSource> GetChunk(int from, int toExclusive)
    {
        for (int j = from; j < toExclusive; j++)
        {
            enumerator.MoveNext();
            yield return source[j];
        }
    }
}

2

Yani işlevsel bir şapka takılıyken, bu önemsiz görünüyor ... ama C # 'da bazı önemli dezavantajlar var.

Muhtemelen bunu IEnumerable'ın bir açılımı olarak görürsünüz (google it ve muhtemelen bazı Haskell belgelerinde yer alacaksınız, ancak F # biliyorsanız, Haskell belgelerinde şaşkınlık varsa, açılmayı kullanan bazı F # maddeleri olabilir ve anlamda).

Unfold, IEnumerable girdisi üzerinden yinelemekten ziyade katlama ("aggregate") ile ilgilidir, çıktı veri yapılarını yineler (IEnumerable ve IObservable arasında benzer bir ilişki, aslında IObservable'ın create adında bir "açılma" uyguladığını düşünüyorum. ..)

her neyse, önce bir açılma yöntemine ihtiyacınız var, bence bu işe yarıyor (maalesef eninde sonunda büyük "listeler" için yığını patlatacak ... bunu concat yerine verim! kullanarak güvenle yazabilirsiniz);

    static IEnumerable<T> Unfold<T, U>(Func<U, IEnumerable<Tuple<U, T>>> f, U seed)
    {
        var maybeNewSeedAndElement = f(seed);

        return maybeNewSeedAndElement.SelectMany(x => new[] { x.Item2 }.Concat(Unfold(f, x.Item1)));
    }

bu biraz abartılı çünkü C # işlevsel dillerin kabul ettiği bazı şeyleri uygulamıyor ... ama temelde bir tohum alır ve sonra IEnumerable ve sonraki tohumda bir sonraki öğenin "Belki" cevabını üretir (Belki C # 'da mevcut değil, bu yüzden onu taklit etmek için IEnumerable kullandık) ve cevabın geri kalanını birleştirdik (bunun "O (n?)" karmaşıklığına kefil olamam).

Bunu yaptıktan sonra;

    static IEnumerable<IEnumerable<T>> Batch<T>(IEnumerable<T> xs, int n)
    {
        return Unfold(ys =>
            {
                var head = ys.Take(n);
                var tail = ys.Skip(n);
                return head.Take(1).Select(_ => Tuple.Create(tail, head));
            },
            xs);
    }

hepsi oldukça temiz görünüyor ... IEnumerable'da "n" elemanlarını "sonraki" eleman olarak alırsınız ve "kuyruk" işlenmemiş listenin geri kalanıdır.

kafada hiçbir şey yoksa ... bittiniz ... "Hiçbir şey" döndürürsünüz (ancak boş bir IEnumerable> olarak takılıyorsunuz) ... aksi takdirde baş öğesini ve kuyruğu işlemek için döndürürsünüz.

Bunu muhtemelen IObservable kullanarak yapabilirsiniz, muhtemelen zaten orada "Toplu İş" benzeri bir yöntem vardır ve muhtemelen bunu kullanabilirsiniz.

Yığın taşması riski endişeleniyorsa (muhtemelen gerekir), o zaman F # uygulamalısınız (ve muhtemelen bununla birlikte bir F # kitaplığı (FSharpX?) Vardır).

(Bununla ilgili yalnızca bazı temel testler yaptım, bu yüzden orada tuhaf hatalar olabilir).


1

Çok geç katılıyorum ama daha ilginç bir şey buldum.

Yani burada Skipve Takedaha iyi performans için kullanabiliriz .

public static class MyExtensions
    {
        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));
        }

        public static IEnumerable<T> Batch2<T>(this IEnumerable<T> items, int skip, int take)
        {
            return items.Skip(skip).Take(take);
        }

    }

Sonra 100000 kayıtla kontrol ettim. Döngü yalnızca şu durumlarda daha fazla zaman alıyorBatch

Konsol uygulamasının kodu.

static void Main(string[] args)
{
    List<string> Ids = GetData("First");
    List<string> Ids2 = GetData("tsriF");

    Stopwatch FirstWatch = new Stopwatch();
    FirstWatch.Start();
    foreach (var batch in Ids2.Batch(5000))
    {
        // Console.WriteLine("Batch Ouput:= " + string.Join(",", batch));
    }
    FirstWatch.Stop();
    Console.WriteLine("Done Processing time taken:= "+ FirstWatch.Elapsed.ToString());


    Stopwatch Second = new Stopwatch();

    Second.Start();
    int Length = Ids2.Count;
    int StartIndex = 0;
    int BatchSize = 5000;
    while (Length > 0)
    {
        var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
        // Console.WriteLine("Second Batch Ouput:= " + string.Join(",", SecBatch));
        Length = Length - BatchSize;
        StartIndex += BatchSize;
    }

    Second.Stop();
    Console.WriteLine("Done Processing time taken Second:= " + Second.Elapsed.ToString());
    Console.ReadKey();
}

static List<string> GetData(string name)
{
    List<string> Data = new List<string>();
    for (int i = 0; i < 100000; i++)
    {
        Data.Add(string.Format("{0} {1}", name, i.ToString()));
    }

    return Data;
}

Geçen zaman böyledir.

İlk - 00: 00: 00.0708, 00: 00: 00.0660

İkinci (Birini Al ve Atla) - 00: 00: 00.0008, 00: 00: 00.0008


2
GroupBytek bir satır oluşturmadan önce tam olarak numaralandırır. Bu, gruplama yapmanın iyi bir yolu değildir.
ErikE

@ErikE Bu, neyi başarmaya çalıştığınıza bağlıdır. Sorun gruplama değilse ve işlemek için öğeleri daha küçük parçalara ayırmanız gerekiyorsa, bu tam da önemli olabilir. Bunu,
LAMBDA'nın

2
Elbette, tam numaralandırmanın önemli olmadığı kullanım durumları vardır. Ama mükemmel bir yöntem yazabilecekken neden ikinci sınıf bir yardımcı yöntem yazasınız ki?
ErikE

İyi bir alternatif, ancak ilk olarak aynı değil, döngü yapmanıza izin veren bir liste listesi döndürür.
Gareth Hopkins

değiştirmek foreach (var batch in Ids2.Batch(5000))için var gourpBatch = Ids2.Batch(5000)ve zamanlanmış sonuçlarını kontrol edin. veya listeye ekle, var SecBatch = Ids2.Batch2(StartIndex, BatchSize);eğer zamanlama sonuçlarınız değişirse ilgilenirim.
Seabizkit

1

Linq olmadan çalışan ve veriler üzerinde tek bir numaralandırmayı garanti eden özel bir IEnumerable uygulaması yazdım. Ayrıca tüm bunları, büyük veri kümelerinde bellek patlamalarına neden olan yedekleme listeleri veya dizileri gerektirmeden gerçekleştirir.

İşte bazı temel testler:

    [Fact]
    public void ShouldPartition()
    {
        var ints = new List<int> {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
        var data = ints.PartitionByMaxGroupSize(3);
        data.Count().Should().Be(4);

        data.Skip(0).First().Count().Should().Be(3);
        data.Skip(0).First().ToList()[0].Should().Be(0);
        data.Skip(0).First().ToList()[1].Should().Be(1);
        data.Skip(0).First().ToList()[2].Should().Be(2);

        data.Skip(1).First().Count().Should().Be(3);
        data.Skip(1).First().ToList()[0].Should().Be(3);
        data.Skip(1).First().ToList()[1].Should().Be(4);
        data.Skip(1).First().ToList()[2].Should().Be(5);

        data.Skip(2).First().Count().Should().Be(3);
        data.Skip(2).First().ToList()[0].Should().Be(6);
        data.Skip(2).First().ToList()[1].Should().Be(7);
        data.Skip(2).First().ToList()[2].Should().Be(8);

        data.Skip(3).First().Count().Should().Be(1);
        data.Skip(3).First().ToList()[0].Should().Be(9);
    }

Verileri bölümlemek için Uzantı Yöntemi.

/// <summary>
/// A set of extension methods for <see cref="IEnumerable{T}"/>. 
/// </summary>
public static class EnumerableExtender
{
    /// <summary>
    /// Splits an enumerable into chucks, by a maximum group size.
    /// </summary>
    /// <param name="source">The source to split</param>
    /// <param name="maxSize">The maximum number of items per group.</param>
    /// <typeparam name="T">The type of item to split</typeparam>
    /// <returns>A list of lists of the original items.</returns>
    public static IEnumerable<IEnumerable<T>> PartitionByMaxGroupSize<T>(this IEnumerable<T> source, int maxSize)
    {
        return new SplittingEnumerable<T>(source, maxSize);
    }
}

Bu uygulama sınıfıdır

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

    internal class SplittingEnumerable<T> : IEnumerable<IEnumerable<T>>
    {
        private readonly IEnumerable<T> backing;
        private readonly int maxSize;
        private bool hasCurrent;
        private T lastItem;

        public SplittingEnumerable(IEnumerable<T> backing, int maxSize)
        {
            this.backing = backing;
            this.maxSize = maxSize;
        }

        public IEnumerator<IEnumerable<T>> GetEnumerator()
        {
            return new Enumerator(this, this.backing.GetEnumerator());
        }

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

        private class Enumerator : IEnumerator<IEnumerable<T>>
        {
            private readonly SplittingEnumerable<T> parent;
            private readonly IEnumerator<T> backingEnumerator;
            private NextEnumerable current;

            public Enumerator(SplittingEnumerable<T> parent, IEnumerator<T> backingEnumerator)
            {
                this.parent = parent;
                this.backingEnumerator = backingEnumerator;
                this.parent.hasCurrent = this.backingEnumerator.MoveNext();
                if (this.parent.hasCurrent)
                {
                    this.parent.lastItem = this.backingEnumerator.Current;
                }
            }

            public bool MoveNext()
            {
                if (this.current == null)
                {
                    this.current = new NextEnumerable(this.parent, this.backingEnumerator);
                    return true;
                }
                else
                {
                    if (!this.current.IsComplete)
                    {
                        using (var enumerator = this.current.GetEnumerator())
                        {
                            while (enumerator.MoveNext())
                            {
                            }
                        }
                    }
                }

                if (!this.parent.hasCurrent)
                {
                    return false;
                }

                this.current = new NextEnumerable(this.parent, this.backingEnumerator);
                return true;
            }

            public void Reset()
            {
                throw new System.NotImplementedException();
            }

            public IEnumerable<T> Current
            {
                get { return this.current; }
            }

            object IEnumerator.Current
            {
                get { return this.Current; }
            }

            public void Dispose()
            {
            }
        }

        private class NextEnumerable : IEnumerable<T>
        {
            private readonly SplittingEnumerable<T> splitter;
            private readonly IEnumerator<T> backingEnumerator;
            private int currentSize;

            public NextEnumerable(SplittingEnumerable<T> splitter, IEnumerator<T> backingEnumerator)
            {
                this.splitter = splitter;
                this.backingEnumerator = backingEnumerator;
            }

            public bool IsComplete { get; private set; }

            public IEnumerator<T> GetEnumerator()
            {
                return new NextEnumerator(this.splitter, this, this.backingEnumerator);
            }

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

            private class NextEnumerator : IEnumerator<T>
            {
                private readonly SplittingEnumerable<T> splitter;
                private readonly NextEnumerable parent;
                private readonly IEnumerator<T> enumerator;
                private T currentItem;

                public NextEnumerator(SplittingEnumerable<T> splitter, NextEnumerable parent, IEnumerator<T> enumerator)
                {
                    this.splitter = splitter;
                    this.parent = parent;
                    this.enumerator = enumerator;
                }

                public bool MoveNext()
                {
                    this.parent.currentSize += 1;
                    this.currentItem = this.splitter.lastItem;
                    var hasCcurent = this.splitter.hasCurrent;

                    this.parent.IsComplete = this.parent.currentSize > this.splitter.maxSize;

                    if (this.parent.IsComplete)
                    {
                        return false;
                    }

                    if (hasCcurent)
                    {
                        var result = this.enumerator.MoveNext();

                        this.splitter.lastItem = this.enumerator.Current;
                        this.splitter.hasCurrent = result;
                    }

                    return hasCcurent;
                }

                public void Reset()
                {
                    throw new System.NotImplementedException();
                }

                public T Current
                {
                    get { return this.currentItem; }
                }

                object IEnumerator.Current
                {
                    get { return this.Current; }
                }

                public void Dispose()
                {
                }
            }
        }
    }

1

Sadece başka bir tek satırlık uygulama. Boş bir listeyle bile çalışır, bu durumda sıfır boyutlu bir toplu iş koleksiyonu elde edersiniz.

var aList = Enumerable.Range(1, 100).ToList(); //a given list
var size = 9; //the wanted batch size
//number of batches are: (aList.Count() + size - 1) / size;

var batches = Enumerable.Range(0, (aList.Count() + size - 1) / size).Select(i => aList.GetRange( i * size, Math.Min(size, aList.Count() - i * size)));

Assert.True(batches.Count() == 12);
Assert.AreEqual(batches.ToList().ElementAt(0), new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
Assert.AreEqual(batches.ToList().ElementAt(1), new List<int>() { 10, 11, 12, 13, 14, 15, 16, 17, 18 });
Assert.AreEqual(batches.ToList().ElementAt(11), new List<int>() { 100 });

0

Herkesin bu işi yapmak için karmaşık sistemler kullandığını biliyorum ve nedenini gerçekten anlamıyorum. Al ve atla, Func<TSource,Int32,TResult>dönüştürme işlevi ile ortak seçimi kullanan tüm bu işlemlere izin verecektir . Sevmek:

public IEnumerable<IEnumerable<T>> Buffer<T>(IEnumerable<T> source, int size)=>
    source.Select((item, index) => source.Skip(size * index).Take(size)).TakeWhile(bucket => bucket.Any());

3
Verilen sourceçok sık yineleneceği için bu çok verimsiz olabilir .
Kevin Meier

1
Bu sadece verimsiz olmakla kalmaz, aynı zamanda yanlış sonuçlar da verebilir. Bir numaralandırılmanın iki kez numaralandırıldığında aynı öğeleri vereceğinin garantisi yoktur. Örnek olarak bu enumerator atın: Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next())).
Theodor Zoulias

0

Başka bir yol da Rx Buffer operatörünü kullanmaktır

//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();

1
Asla kullanmak zorunda kalmamalısın GetAwaiter().GetResult(). Bu, zaman uyumsuz kodu zorla çağıran eşzamanlı kod için bir kod kokusudur.
gfache

-3
    static IEnumerable<IEnumerable<T>> TakeBatch<T>(IEnumerable<T> ts,int batchSize)
    {
        return from @group in ts.Select((x, i) => new { x, i }).ToLookup(xi => xi.i / batchSize)
               select @group.Select(xi => xi.x);
    }

Cevabınıza bir açıklama / metin ekleyin. Yalnızca kod koymak çoğu zaman daha az anlam ifade edebilir.
Ariful Haque
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.