สร้างแบทช์ใน linq


113

มีใครแนะนำวิธีสร้างแบทช์ที่มีขนาดบางขนาดใน linq ได้ไหม

ตามหลักการแล้วฉันต้องการที่จะสามารถดำเนินการเป็นกลุ่มของจำนวนที่กำหนดได้

คำตอบ:


120

คุณไม่จำเป็นต้องเขียนโค้ดใด ๆ ใช้เมธอด MoreLINQ Batch ซึ่งแบ่งลำดับแหล่งที่มาเป็นกลุ่มขนาด (MoreLINQ พร้อมใช้งานเป็นแพ็คเกจ NuGet ที่คุณสามารถติดตั้งได้):

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

ซึ่งดำเนินการเป็น:

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
4 ไบต์ต่อรายการทำงานได้แย่มาก ? คุณมีการทดสอบบางอย่างที่แสดงให้เห็นว่ามีความหมายอย่างมากหรือไม่? หากคุณกำลังโหลดรายการนับล้านลงในหน่วยความจำฉันจะไม่ทำ ใช้การเพจฝั่งเซิร์ฟเวอร์
Sergey Berezovskiy

4
ฉันไม่ได้ตั้งใจจะทำให้คุณขุ่นเคือง แต่มีวิธีแก้ปัญหาที่ง่ายกว่าที่จะไม่สะสมเลย นอกจากนี้จะจัดสรรพื้นที่แม้กระทั่งสำหรับองค์ประกอบที่ไม่มีอยู่จริง:Batch(new int[] { 1, 2 }, 1000000)
Nick Whaley

8
@NickWhaley เห็นด้วยกับคุณว่าจะมีการจัดสรรพื้นที่เพิ่มเติม แต่ในชีวิตจริงคุณมักจะมีสถานการณ์ตรงกันข้าม - รายการ 1,000 รายการที่ควรจะเป็นชุดละ 50 :)
Sergey Berezovskiy

1
ใช่สถานการณ์ควรเป็นไปในทางอื่น แต่ในชีวิตจริงสิ่งเหล่านี้อาจเป็นปัจจัยที่ผู้ใช้ป้อน
Nick Whaley

9
นี่เป็นทางออกที่ดีอย่างสมบูรณ์แบบ ในชีวิตจริงคุณ: ตรวจสอบการป้อนข้อมูลของผู้ใช้ถือว่าแบทช์เป็นคอลเลคชันทั้งหมดของไอเท็ม (ซึ่งสะสมไอเท็มไว้ตลอดเวลา) และมักจะประมวลผลแบตช์แบบขนาน (ซึ่งไม่ได้รับการสนับสนุนโดยวิธีการทำซ้ำและจะเป็นเรื่องที่น่าประหลาดใจเว้นแต่คุณจะรู้ รายละเอียดการใช้งาน)
Michael Petito

93
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));
    }
}

และการใช้งานจะเป็น:

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));
}

เอาท์พุท:

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

ทำงานได้อย่างสมบูรณ์แบบสำหรับฉัน
FunMatters

16
เมื่อGroupByเริ่มการแจงนับแล้วไม่จำเป็นต้องระบุแหล่งที่มาอย่างครบถ้วนหรือไม่? สิ่งนี้ทำให้สูญเสียการประเมินแหล่งที่มาอย่างเกียจคร้านดังนั้นในบางกรณีผลประโยชน์ทั้งหมดของการแบทช์!
ErikE

1
ว้าวขอบคุณคุณช่วยฉันจากความวิกลจริต ทำงานได้ดีมาก
Riaan de Lange

4
ดังที่ @ErikE กล่าวถึงวิธีนี้จะระบุแหล่งที่มาอย่างครบถ้วนดังนั้นแม้ว่าจะดูดี แต่ก็เอาชนะจุดประสงค์ของการประเมิน / การวางท่อแบบ
เกียจคร้าน

2
ทำสิ่งนี้ - เหมาะสมอย่างยิ่งเมื่อคุณต้องการแยกบล็อกที่มีอยู่ออกเป็นกลุ่มย่อย ๆ สำหรับการประมวลผลที่มีประสิทธิภาพ อีกทางเลือกหนึ่งคือการค้นหาลูปขั้นต้นที่คุณแยกแบทช์ด้วยตนเองและยังคงดำเนินการตามแหล่งที่มาทั้งหมด
StingyJack

35

หากคุณเริ่มต้นด้วยsequenceกำหนดเป็น an IEnumerable<T>และคุณรู้ว่าสามารถแจกแจงหลาย ๆ ครั้งได้อย่างปลอดภัย (เช่นเพราะเป็นอาร์เรย์หรือรายการ) คุณสามารถใช้รูปแบบง่ายๆนี้เพื่อประมวลผลองค์ประกอบในแบทช์:

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

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

3
วิธีที่ดีและง่ายสำหรับการแบทช์โดยไม่มีรหัสมากหรือต้องการไลบรารีภายนอก
DevHawk

5
@DevHawk: มันคือ. อย่างไรก็ตามโปรดทราบว่าประสิทธิภาพดังกล่าวจะได้รับผลกระทบแบบทวีคูณในคอลเล็กชันขนาดใหญ่ (r)
RobIII

28

ทั้งหมดที่กล่าวมาทำงานได้แย่มากกับแบทช์ขนาดใหญ่หรือพื้นที่หน่วยความจำเหลือน้อย ต้องเขียนของฉันเองที่จะไปป์ไลน์ (สังเกตว่าไม่มีการสะสมไอเท็มที่ใดก็ได้):

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

แก้ไข:ปัญหาที่ทราบเกี่ยวกับแนวทางนี้คือแต่ละชุดต้องได้รับการแจกแจงและแจกแจงอย่างครบถ้วนก่อนที่จะย้ายไปยังชุดถัดไป ตัวอย่างเช่นนี้ใช้ไม่ได้:

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

1
รูทีน @LB ที่โพสต์ด้านบนไม่ได้ทำการสะสมไอเท็มเช่นกัน
neontapir

3
@neontapir ยังทำอยู่ เครื่องคัดแยกเหรียญที่ให้เหรียญกษาปณ์ก่อนจากนั้นเล็กน้อยก่อนอื่นต้องตรวจสอบทุกเหรียญก่อนที่จะให้เล็กน้อยเพื่อให้แน่ใจว่าไม่มีนิกเกิลอีกต่อไป
Nick Whaley

2
Ahhh ahha พลาดบันทึกการแก้ไขของคุณเมื่อฉันขัดขวางรหัสนี้ ต้องใช้เวลาพอสมควรในการทำความเข้าใจว่าเหตุใดการทำซ้ำมากกว่ากลุ่มที่ไม่ได้แจกแจงจริงจึงแจกแจงคอลเลกชั่นดั้งเดิมทั้งหมด (!!!) โดยจัดเตรียม X แบทช์โดยแต่ละชุดมีการแจกแจง 1 รายการ (โดยที่ X คือจำนวนของคอลเลกชั่นเดิม)
eli

2
@NickWhaley ถ้าฉัน Count () บนผลลัพธ์ <IEnumerable <T>> ของ IE ที่ไม่สามารถคำนวณได้โดยรหัสของคุณมันให้คำตอบที่ผิดมันให้จำนวนองค์ประกอบทั้งหมดเมื่อคาดว่าจะเป็นจำนวนแบทช์ทั้งหมดที่สร้างขึ้น นี่ไม่ใช่กรณีของ MoreLinq Batch code
Mrinal Kamboj

1
@JohnZabroski - นี่คือส่วนสำคัญสั้น ๆ : gist.github.com/mmurrell/9225ed7c4d107c2195057f77e07f0f68
Matt Murrell

24

นี่คือการใช้งาน Batch แบบหนึ่งฟังก์ชั่นที่ขี้เกียจอย่างเต็มที่และต่ำซึ่งไม่ได้ทำการสะสมใด ๆ ขึ้นอยู่กับ (และแก้ไขปัญหาใน) โซลูชันของ Nick Whaley ด้วยความช่วยเหลือจาก EricRoller

การวนซ้ำมาจาก IE ที่เป็นพื้นฐานโดยตรงดังนั้นจึงต้องมีการแจกแจงองค์ประกอบตามลำดับที่เข้มงวดและเข้าถึงได้ไม่เกินหนึ่งครั้ง หากองค์ประกอบบางอย่างไม่ถูกใช้ไปในวงในองค์ประกอบเหล่านั้นจะถูกทิ้ง (และพยายามเข้าถึงอีกครั้งผ่านตัววนซ้ำที่บันทึกไว้จะถูกโยนทิ้งInvalidOperationException: Enumeration already finished.)

คุณสามารถทดสอบตัวอย่างที่สมบูรณ์.NET ซอ

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
นี่เป็นเพียงการใช้งานที่ขี้เกียจอย่างเต็มที่ที่นี่ สอดคล้องกับการใช้งาน python itertools.GroupBy
Eric Roller

1
คุณสามารถกำจัดกาเครื่องหมายสำหรับdoneโดยเพียงแค่เสมอเรียกหลังจากe.Count() yield return eคุณจะต้องจัดเรียงห่วงใน BatchInner ที่จะไม่ก่อให้เกิดพฤติกรรมที่ไม่ได้กำหนดถ้าsource.Current i >= sizeซึ่งจะช่วยขจัดความจำเป็นในการจัดสรรใหม่BatchInnerสำหรับแต่ละชุด
Eric Roller

1
คุณพูดถูกคุณยังคงต้องรวบรวมข้อมูลเกี่ยวกับความคืบหน้าของแต่ละชุด ฉันไม่พบข้อผิดพลาดในรหัสของคุณถ้าคุณลองรับรายการที่ 2 จากแต่ละชุด: ข้อผิดพลาดซอ การดำเนินการแก้ไขโดยไม่ต้องแยกชั้น (ใช้ C # 7) อยู่ที่นี่: ซอคงที่ โปรดทราบว่าฉันคาดว่า CLR จะยังคงสร้างฟังก์ชันโลคัลหนึ่งครั้งต่อลูปเพื่อจับตัวแปรiดังนั้นสิ่งนี้จึงไม่จำเป็นต้องมีประสิทธิภาพมากกว่าการกำหนดคลาสแยกต่างหาก แต่ฉันคิดว่ามันสะอาดกว่าเล็กน้อย
Eric Roller

1
ฉันเปรียบเทียบเวอร์ชันนี้โดยใช้ BenchmarkDotNet กับ System.Reactive.Linq.EnumerableEx.Buffer และการใช้งานของคุณเร็วขึ้น 3-4 โดยเสี่ยงต่อความปลอดภัย ภายใน EnumerableEx บัฟเฟอร์จัดสรร Queue of List <T> github.com/dotnet/reactive/blob/…
John Zabroski

1
หากคุณต้องการเวอร์ชันบัฟเฟอร์นี้คุณสามารถทำได้: IE แบบคงที่สาธารณะที่ไม่สามารถใช้งานได้ <IReadOnlyList <T>> BatchBuffered <T> (แหล่งที่มา <T> ของ IEnumerable, int size) => Batch (แหล่งขนาด) เลือก (chunk = > (IReadOnlyList <T>) ก้อน. ToList ()); การใช้ IReadOnlyList <T> คือการบอกใบ้ผู้ใช้ว่าเอาต์พุตถูกแคช คุณยังสามารถเก็บ <IEnumerable <T>> ของ IE ไว้แทนได้
gfache

11

ฉันสงสัยว่าทำไมไม่มีใครเคยโพสต์โซลูชัน for-loop ของโรงเรียนเก่า นี่คือหนึ่ง:

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);
}

ความเรียบง่ายนี้เกิดขึ้นได้เนื่องจากวิธี Take:

... แจกแจงsourceและให้องค์ประกอบจนกว่าcountองค์ประกอบจะได้รับหรือsourceไม่มีองค์ประกอบอีกต่อไป หากcountเกินจำนวนองค์ประกอบในsourceองค์ประกอบทั้งหมดsourceจะถูกส่งกลับ

คำเตือน:

การใช้ Skip and Take ภายในลูปหมายความว่าจะมีการแจกแจงหลายครั้ง สิ่งนี้เป็นอันตรายหากการแจงนับถูกเลื่อนออกไป อาจส่งผลให้มีการดำเนินการแบบสอบถามฐานข้อมูลหรือคำขอทางเว็บหรืออ่านไฟล์หลายครั้ง ตัวอย่างนี้มีไว้อย่างชัดเจนสำหรับการใช้รายการที่ไม่ได้เลื่อนออกไปดังนั้นจึงมีปัญหาน้อยกว่า ยังคงเป็นวิธีแก้ปัญหาที่ช้าเนื่องจากการข้ามจะระบุคอลเล็กชันทุกครั้งที่มีการเรียกใช้

สิ่งนี้สามารถแก้ไขได้โดยใช้GetRangeวิธีการนี้ แต่ต้องมีการคำนวณเพิ่มเติมเพื่อแยกชุดที่เหลือที่เป็นไปได้:

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);
}

นี่คือวิธีที่สามในการจัดการสิ่งนี้ซึ่งใช้ได้กับ 2 ลูป เพื่อให้แน่ใจว่าคอลเลกชันจะถูกแจกแจงเพียง 1 ครั้ง!:

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
ทางออกที่ดีมาก ผู้คนลืมวิธีใช้ห่วง
VitalickS

1
การใช้SkipและTakeภายในลูปหมายความว่าจะมีการแจกแจงหลายครั้ง สิ่งนี้เป็นอันตรายหากการแจงนับถูกเลื่อนออกไป อาจส่งผลให้มีการดำเนินการแบบสอบถามฐานข้อมูลหรือคำขอทางเว็บหรืออ่านไฟล์หลายครั้ง ในตัวอย่างของคุณคุณมีปัญหาListที่ไม่รอการตัดบัญชีดังนั้นจึงมีปัญหาน้อยกว่า
Theodor Zoulias

@TheodorZoulias ใช่ฉันรู้นี่คือเหตุผลที่ฉันโพสต์วิธีแก้ปัญหาที่สองในวันนี้ ฉันโพสต์ความคิดเห็นของคุณเป็นข้อจำกัดความรับผิดชอบเนื่องจากคุณกำหนดไว้ค่อนข้างดีฉันจะอ้างถึงคุณหรือไม่
Mong Zhu

ฉันเขียนวิธีแก้ปัญหาที่สามด้วย 2 ลูปเพื่อให้มีการแจกแจงคอลเล็กชันเพียง 1 ครั้ง สิ่งที่ข้ามไปเป็นวิธีแก้ปัญหาที่ไม่มีประสิทธิภาพมาก
Mong Zhu

4

แนวทางเดียวกับ MoreLINQ แต่ใช้ List แทน Array ฉันยังไม่ได้ทำการเปรียบเทียบ แต่ความสามารถในการอ่านมีความสำคัญมากกว่าสำหรับบางคน:

    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
คุณไม่ควรใช้ตัวแปรแบตช์ซ้ำ ผู้บริโภคของคุณอาจรู้สึกแย่กับสิ่งนั้น นอกจากนี้ให้ส่งsizeพารามิเตอร์ไปยังของคุณnew Listเพื่อปรับขนาดให้เหมาะสม
ErikE

1
แก้ไขง่าย: แทนที่batch.Clear();ด้วยbatch = new List<T>();
NetMage

4

นี่คือความพยายามในการปรับปรุงการใช้งานที่ขี้เกียจของ Nick Whaley ( ลิงค์ ) และ infogulch ( ลิงค์ ) Batchอันนี้เข้มงวด คุณระบุแบตช์ตามลำดับที่ถูกต้องหรือคุณได้รับข้อยกเว้น

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++;
            }
        }
    }
}

และนี่คือขี้เกียจการดำเนินงานสำหรับแหล่งที่มาของประเภทBatch IList<T>ข้อนี้ไม่มีข้อ จำกัด ในการแจงนับ แบทช์สามารถแจกแจงได้บางส่วนตามลำดับใด ๆ และมากกว่าหนึ่งครั้ง ข้อ จำกัด ในการไม่แก้ไขคอลเลกชันระหว่างการแจงนับยังคงมีอยู่ สิ่งนี้ทำได้โดยการโทรหลอกenumerator.MoveNext()ก่อนที่จะให้ชิ้นส่วนหรือองค์ประกอบใด ๆ ข้อเสียคือตัวนับถูกปล่อยทิ้งไว้โดยไม่เปิดเผยเนื่องจากไม่ทราบว่าการแจงนับจะเสร็จสิ้นเมื่อใด

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

ดังนั้นเมื่อเปิดหมวกที่ใช้งานได้สิ่งนี้จึงดูไม่สำคัญ .... แต่ใน C # มีข้อเสียที่สำคัญบางประการ

คุณอาจมองว่านี่เป็นส่วนขยายของ IEnumerable (Google และคุณอาจจะจบลงด้วยเอกสาร Haskell บางฉบับ แต่อาจมีบางสิ่งที่ F # ใช้แฉถ้าคุณรู้จัก F # ให้เหล่ที่เอกสาร Haskell และมันจะทำให้ ความรู้สึก).

Unfold เกี่ยวข้องกับการพับ ("รวม") ยกเว้นแทนที่จะวนซ้ำผ่านอินพุต IEnumerable มันจะวนซ้ำผ่านโครงสร้างข้อมูลเอาต์พุต (ความสัมพันธ์ที่คล้ายกันระหว่าง IEnumerable และ IObservable ฉันคิดว่า IObservable ใช้ "แฉ" ที่เรียกว่าสร้าง .. )

ก่อนอื่นคุณต้องมีวิธีการแฉฉันคิดว่ามันใช้ได้ (น่าเสียดายที่มันจะระเบิดสแต็กสำหรับ "รายการ" ขนาดใหญ่ ... คุณสามารถเขียนสิ่งนี้ได้อย่างปลอดภัยใน F # โดยใช้ผลตอบแทน! แทนที่จะเป็น concat);

    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)));
    }

นี่เป็นสิ่งที่ป้านเล็กน้อยเนื่องจาก C # ไม่ได้ใช้บางสิ่งที่ใช้ภาษาที่ใช้งานได้ ... แต่โดยทั่วไปแล้วจะใช้เมล็ดพันธุ์จากนั้นสร้างคำตอบ "อาจจะ" ขององค์ประกอบถัดไปใน IEnumerable และเมล็ดพันธุ์ถัดไป (อาจ ไม่มีอยู่ใน C # ดังนั้นเราจึงใช้ IE ไม่สามารถปลอมได้) และเชื่อมต่อคำตอบที่เหลือ (ฉันไม่สามารถรับรองความซับซ้อน "O (n?)" ของสิ่งนี้ได้)

เมื่อคุณทำเสร็จแล้ว

    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);
    }

ทุกอย่างดูค่อนข้างสะอาด ... คุณใช้องค์ประกอบ "n" เป็นองค์ประกอบ "ถัดไป" ใน IEnumerable และ "tail" คือส่วนที่เหลือของรายการที่ยังไม่ได้ประมวลผล

ถ้าไม่มีอะไรอยู่ในหัว ... คุณจบแล้ว ... คุณกลับ "Nothing" (แต่แกล้งทำเป็น IE ว่างเปล่า>) ... มิฉะนั้นคุณจะส่งคืนองค์ประกอบส่วนหัวและส่วนท้ายเพื่อประมวลผล

คุณอาจทำได้โดยใช้ IObservable อาจมีเมธอดแบบ "แบตช์" อยู่แล้วและคุณอาจใช้วิธีนั้นได้

หากความเสี่ยงของสแต็กล้นทำให้กังวล (อาจเป็นไปได้) คุณควรติดตั้งใน F # (และอาจมีไลบรารี F # (FSharpX?) อยู่แล้วด้วย)

(ฉันได้ทำการทดสอบพื้นฐานบางอย่างเท่านั้นดังนั้นอาจมีข้อบกพร่องแปลก ๆ อยู่ในนั้น)


Maybeสามารถมีอยู่ใน C # - ดูตัวอย่างOptionใน Language Ext.
Tim Barrass

1

ฉันมาช้ามาก แต่ฉันพบสิ่งที่น่าสนใจกว่านี้

ดังนั้นเราสามารถใช้ที่นี่SkipและTakeเพื่อประสิทธิภาพที่ดีขึ้น

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);
        }

    }

ต่อไปฉันตรวจสอบด้วยบันทึก 100000 รายการ การวนซ้ำใช้เวลามากขึ้นเท่านั้นในกรณีของBatch

รหัสของแอปพลิเคชันคอนโซล

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;
}

เวลาถ่ายเป็นแบบนี้

วันแรก - 00: 00: 00.0708, 00: 00: 00.0660

วินาที (Take and Skip One) - 00: 00: 00.0008, 00: 00: 00.0008


2
GroupByแจกแจงอย่างครบถ้วนก่อนที่จะสร้างแถวเดียว นี่ไม่ใช่วิธีที่ดีในการทำแบทช์
ErikE

@ErikE นั่นขึ้นอยู่กับสิ่งที่คุณพยายามบรรลุ หากการจัดชุดไม่ใช่ปัญหาและคุณเพียงแค่ต้องแบ่งรายการออกเป็นชิ้นเล็ก ๆ เพื่อประมวลผลอาจเป็นเพียงสิ่งเดียว ฉันใช้สิ่งนี้สำหรับ MSCRM ซึ่งอาจมี 100 ระเบียนซึ่งไม่มีปัญหาสำหรับ LAMBDA ในการจัดกลุ่ม .. เป็นการประหยัดที่ใช้เวลาไม่กี่วินาที ..
JensB

2
แน่นอนว่ามีกรณีการใช้งานที่การแจงนับเต็มไม่สำคัญ แต่ทำไมต้องเขียนวิธียูทิลิตี้ชั้นสองในเมื่อคุณสามารถเขียนวิธีที่ยอดเยี่ยมได้?
ErikE

ทางเลือกที่ดี แต่ไม่เหมือนกันกับรายการแรกที่แสดงรายการที่ช่วยให้คุณวนซ้ำ
Gareth Hopkins

เปลี่ยนforeach (var batch in Ids2.Batch(5000))เป็นvar gourpBatch = Ids2.Batch(5000)และตรวจสอบผลลัพธ์ตามกำหนดเวลา หรือเพิ่มสิ่งที่ต้องทำเพื่อvar SecBatch = Ids2.Batch2(StartIndex, BatchSize);ฉันจะสนใจถ้าผลลัพธ์ของคุณสำหรับเวลาเปลี่ยน
Seabizkit

1

ฉันเขียนการใช้งาน IEnumerable แบบกำหนดเองซึ่งทำงานโดยไม่ใช้ linq และรับประกันการแจงนับข้อมูลเพียงครั้งเดียว นอกจากนี้ยังดำเนินการทั้งหมดนี้โดยไม่ต้องสำรองรายการหรืออาร์เรย์ที่ทำให้หน่วยความจำระเบิดในชุดข้อมูลขนาดใหญ่

นี่คือการทดสอบพื้นฐานบางส่วน:

    [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);
    }

วิธีการขยายเพื่อแบ่งพาร์ติชันข้อมูล

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

นี่คือคลาสการใช้งาน

    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

การใช้งานอีกบรรทัดเดียว ใช้งานได้แม้จะมีรายการว่างในกรณีนี้คุณจะได้รับคอลเลกชันแบทช์ขนาดศูนย์

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 });

1

อีกวิธีหนึ่งคือการใช้ตัวดำเนินการ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();

1
GetAwaiter().GetResult()คุณไม่ควรจะต้องใช้ นี่คือกลิ่นรหัสสำหรับรหัสซิงโครนัสที่เรียกรหัส async
gfache

1

เวอร์ชันที่ใช้งานง่ายและเข้าใจ

    public static List<List<T>> chunkList<T>(List<T> listToChunk, int batchSize)
    {
        List<List<T>> batches = new List<List<T>>();

        if (listToChunk.Count == 0) return batches;

        bool moreRecords = true;
        int fromRecord = 0;
        int countRange = 0;
        if (listToChunk.Count >= batchSize)
        {
            countRange = batchSize;
        }
        else
        {
            countRange = listToChunk.Count;
        }

        while (moreRecords)
        {
            List<T> batch = listToChunk.GetRange(fromRecord, countRange);
            batches.Add(batch);

            if ((fromRecord + batchSize) >= listToChunk.Count)
            {
                moreRecords = false;
            }

            fromRecord = fromRecord + batch.Count;

            if ((fromRecord + batchSize) > listToChunk.Count)
            {
                countRange = listToChunk.Count - fromRecord;
            }
            else
            {
                countRange = batchSize;
            }
        }
        return batches;
    }

0

ฉันรู้ว่าทุกคนใช้ระบบที่ซับซ้อนในการทำงานนี้และฉันก็ไม่เข้าใจว่าทำไม Take and skip จะอนุญาตให้ดำเนินการทั้งหมดโดยใช้การเลือกร่วมกับFunc<TSource,Int32,TResult>ฟังก์ชัน transform ชอบ:

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
สิ่งนี้อาจไม่มีประสิทธิภาพมากนักเนื่องจากsourceจะมีการทำซ้ำบ่อยมาก
Kevin Meier

1
สิ่งนี้ไม่เพียง แต่ไม่มีประสิทธิภาพ แต่ยังอาจให้ผลลัพธ์ที่ไม่ถูกต้องอีกด้วย ไม่มีการรับประกันว่าการแจงนับจะให้องค์ประกอบเดียวกันเมื่อแจกแจงสองครั้ง ยกตัวอย่างการแจกแจงนี้: Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next())).
Theodor Zoulias

-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);
    }

เพิ่มคำอธิบาย / ข้อความในคำตอบของคุณ การใส่รหัสเพียงอย่างเดียวอาจมีความหมายน้อยกว่าโดยส่วนใหญ่
Ariful Haque
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.