แยกรายการเป็นรายการย่อยด้วย LINQ


377

มีวิธีใดบ้างที่ฉันสามารถแยก a List<SomeObject>ออกเป็นหลาย ๆ รายการSomeObjectโดยใช้ดัชนีรายการเป็นตัวคั่นของแต่ละการแบ่ง

ให้ฉันเป็นตัวอย่าง:

ฉันมีList<SomeObject>และฉันต้องการList<List<SomeObject>>หรือList<SomeObject>[]เพื่อให้แต่ละรายการผลลัพธ์เหล่านี้จะมีกลุ่ม 3 รายการของรายการต้นฉบับ (เรียงตามลำดับ)

เช่น.:

  • รายการเดิม: [a, g, e, w, p, s, q, f, x, y, i, m, c]

  • รายการผลลัพธ์: [a, g, e], [w, p, s], [q, f, x], [y, i, m], [c]

ฉันต้องการขนาดรายการผลลัพธ์เพื่อเป็นพารามิเตอร์ของฟังก์ชันนี้

คำตอบ:


378

ลองรหัสต่อไปนี้

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

แนวคิดคือการจัดกลุ่มองค์ประกอบตามดัชนีเป็นอันดับแรก หารด้วยสามมีผลกระทบของการจัดกลุ่มพวกเขาเข้าไปในกลุ่มของ 3 แล้วแปลงให้แต่ละกลุ่มรายการและที่IEnumerableของListไปListของLists


21
GroupBy จัดเรียงโดยนัย ที่สามารถฆ่าประสิทธิภาพได้ สิ่งที่เราต้องการคือสิ่งที่ตรงกันข้ามของ SelectMany
yfeldblum

5
@ Justice, GroupBy อาจนำไปใช้โดยการแปลงแป้นพิมพ์ คุณจะรู้ได้อย่างไรว่าการใช้งานของ GroupBy สามารถฆ่าประสิทธิภาพได้อย่างไร
Amy B

5
GroupBy ไม่ส่งคืนสิ่งใดจนกว่าจะมีการระบุองค์ประกอบทั้งหมด นั่นเป็นเหตุผลที่มันช้า รายการที่ OP ต้องการนั้นต่อเนื่องกันดังนั้นเมธอดที่ดีกว่าสามารถสร้างรายการย่อยแรก[a,g,e]ก่อนที่จะแจกแจงรายการเดิมได้อีก
พันเอก Panic

9
นำตัวอย่างสุดยอดของ IEnumerable ที่ไม่มีที่สิ้นสุด GroupBy(x=>f(x)).First()จะไม่มีวันยอมกลุ่ม OP ถามเกี่ยวกับรายการ แต่ถ้าเราเขียนเพื่อทำงานกับ IEnumerable ทำให้เกิดซ้ำเพียงครั้งเดียวเราจะได้เปรียบด้านประสิทธิภาพ
พันเอก Panic

8
@Nick Order ไม่ได้เก็บรักษาไว้ในแบบของคุณ มันยังเป็นเรื่องดีที่จะรู้ แต่คุณจะจัดกลุ่มพวกเขาเป็น (0,3,6,9, ... ), (1,4,7,10, ... ), (2,5,8 , 11, ... ) หากคำสั่งซื้อไม่สำคัญก็ถือว่าใช้ได้ แต่ในกรณีนี้ดูเหมือนว่าจะสำคัญ
Reafexus

325

คำถามนี้ค่อนข้างเก่า แต่ฉันเพิ่งเขียนสิ่งนี้และฉันคิดว่ามันดูดีกว่าโซลูชันที่เสนออื่น ๆ เล็กน้อย:

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

14
รักทางออกนี้ ฉันขอแนะนำให้เพิ่มการตรวจสอบสตินี้เพื่อป้องกันการวนซ้ำไม่สิ้นสุด: if (chunksize <= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize");
mroach

10
ฉันชอบสิ่งนี้ แต่มันก็ไม่ได้มีประสิทธิภาพมากนัก
Sam Saffron

51
ผมชอบคนนี้ O(n²)แต่ประสิทธิภาพการใช้เวลา คุณสามารถวนซ้ำในรายการและรับO(n)เวลา
hIpPy

8
@hIpPy มันเป็นอย่างไรบ้าง ^ 2 ดูเหมือนกับฉัน
Vivek Maharajh

13
@vivekmaharajh sourceถูกแทนที่ด้วยการห่อIEnumerableทุกครั้ง ดังนั้นการพิจารณาองค์ประกอบจากการsourceผ่านเลเยอร์ของSkips
Lasse Espeholt

99

โดยทั่วไปแล้ววิธีการที่CaseyBแนะนำให้ใช้งานได้ดีจริง ๆ แล้วถ้าคุณผ่านList<T>มันยากที่จะผิดมันอาจจะเปลี่ยนเป็น:

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

ซึ่งจะหลีกเลี่ยงสายโซ่ขนาดใหญ่ อย่างไรก็ตามวิธีการนี้มีข้อบกพร่องทั่วไป มันเป็นรูปธรรมสอง enumerations ต่ออันเพื่อเน้นปัญหาลองใช้:

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

เพื่อเอาชนะสิ่งนี้เราสามารถลองใช้วิธีของคาเมรอนซึ่งผ่านการทดสอบด้านบนด้วยสีที่บินได้เนื่องจากมันจะทำการแจงนับครั้งเดียวเท่านั้น

ปัญหาคือว่ามันมีข้อบกพร่องที่แตกต่างกันมันเป็นรูปธรรมทุกรายการในแต่ละก้อนปัญหากับวิธีการนั้นคือการที่คุณใช้หน่วยความจำสูง

เพื่อแสดงให้เห็นว่าลองใช้:

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

ในที่สุดการใช้งานใด ๆ ควรจะสามารถจัดการกับการวนซ้ำของลำดับตัวอย่างเช่น:

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

โซลูชันที่ดีที่สุดจำนวนมากเช่นการแก้ไขคำตอบแรกของฉันล้มเหลว ปัญหาเดียวกันสามารถเห็นได้ในคำตอบที่ดีที่สุดของ casperOne

เพื่อแก้ไขปัญหาเหล่านี้คุณสามารถใช้สิ่งต่อไปนี้:

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


        }

    }
}

นอกจากนี้ยังมีรอบของการเพิ่มประสิทธิภาพที่คุณสามารถแนะนำสำหรับการวนซ้ำที่ไม่เป็นไปตามลำดับซึ่งอยู่นอกขอบเขตที่นี่

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

หมายเหตุEnumeratorWrapperเช่นเดียวกับวิธีการส่วนใหญ่นี้ไม่ปลอดภัยสำหรับหลายเธรดสิ่งที่จะได้รับแปลกหากคุณต้องการที่จะทำให้มันด้ายปลอดภัยคุณจะต้องแก้ไข


ข้อผิดพลาดจะเป็น Enumerable.Range (0, 100) .Chunk (3) .Reverse (). ToArray () ผิดหรือ Enumerable.Range (0, 100). ToArray (). Chunk (3) .Reverse () .ToArray () ส่งข้อยกเว้นหรือไม่
Cameron MacFarland

@SamSaffron ฉันได้อัปเดตคำตอบของฉันและทำให้รหัสง่ายขึ้นมากสำหรับสิ่งที่ฉันรู้สึกว่าเป็นกรณีการใช้ที่โดดเด่น (และรับทราบคำเตือน)
casperOne

แล้ว chuncking IQueryable <> ล่ะ? ฉันเดาว่าวิธีการใช้ / ข้ามจะดีที่สุดถ้าเราต้องการมอบอำนาจการดำเนินการสูงสุดให้กับผู้ให้บริการ
Guillaume86

@ Guillaume86 ฉันเห็นด้วยถ้าคุณมี IList หรือ IQueryable คุณสามารถใช้ทางลัดได้ทุกประเภทที่จะทำให้มันเร็วขึ้น (Linq ทำสิ่งนี้ภายในสำหรับวิธีการอื่น ๆ ทุกประเภท)
Sam Saffron

1
นี่คือคำตอบที่ดีที่สุดสำหรับประสิทธิภาพ ฉันมีปัญหาในการใช้ SqlBulkCopy กับ IEnumerable ที่รันกระบวนการเพิ่มเติมในแต่ละคอลัมน์ดังนั้นจึงต้องทำงานอย่างมีประสิทธิภาพด้วยการส่งผ่านเพียงครั้งเดียว สิ่งนี้จะทำให้ฉันสามารถแบ่ง IEnumerable เป็นกลุ่มขนาดที่จัดการได้ (สำหรับผู้ที่สงสัยฉันได้เปิดใช้งานโหมดสตรีมมิ่งของ SqlBulkCopy ซึ่งดูเหมือนว่าจะเสีย)
Brain2000

64

คุณสามารถใช้ข้อความค้นหาจำนวนมากที่ใช้TakeและSkipแต่เชื่อว่าจะเพิ่มการทำซ้ำหลายรายการในรายการเดิมฉันเชื่อ

ค่อนข้างฉันคิดว่าคุณควรสร้างตัววนซ้ำของคุณเองเช่น:

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

จากนั้นคุณสามารถเรียกสิ่งนี้ได้และมันเปิดใช้งาน LINQ เพื่อให้คุณสามารถดำเนินการอื่น ๆ ตามลำดับที่ได้


ในแง่ของคำตอบของแซมผมรู้สึกว่ามีวิธีที่ง่ายต่อการทำเช่นนี้โดย:

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

ที่กล่าวว่านี่คืออีกหนึ่งผ่านซึ่งฉันได้ประมวลในวิธีการขยายไป IEnumerable<T>เรียกว่าChunk:

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

ไม่มีอะไรน่าแปลกใจที่นั่นเพียงตรวจสอบข้อผิดพลาดพื้นฐาน

ย้ายไปที่ 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);
}

โดยทั่วไปจะได้รับ IEnumerator<T>และทำซ้ำด้วยตนเองผ่านแต่ละรายการ มันจะตรวจสอบเพื่อดูว่ามีรายการใด ๆ ที่จะระบุในปัจจุบัน หลังจากแต่ละอันถูกแจกแจงผ่านหากไม่มีรายการใดเหลืออยู่มันจะแบ่งออก

เมื่อตรวจพบว่ามีรายการอยู่ในลำดับมันจะมอบหมายความรับผิดชอบสำหรับIEnumerable<T>การนำไปใช้ภายใน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());
}

เนื่องจากMoveNextมีการเรียกเมื่อIEnumerator<T>ผ่านไปChunkSequenceแล้วจึงให้รายการที่ส่งคืนโดยCurrentแล้วเพิ่มจำนวนการตรวจสอบให้แน่ใจว่าจะไม่ส่งคืนมากกว่าchunkSizeรายการและย้ายไปยังรายการถัดไปในลำดับหลังจากการวนซ้ำทุกครั้ง (แต่ลัดวงจรถ้าจำนวน รายการที่ให้ผลลัพธ์มีขนาดเกินขนาด)

หากไม่มีรายการที่เหลืออยู่InternalChunkวิธีการจะทำการส่งผ่านอีกครั้งในลูปด้านนอก แต่เมื่อMoveNextเรียกว่าครั้งที่สองมันจะยังคงกลับเท็จตามเอกสาร (เน้นการระเบิด):

ถ้า MoveNext ผ่านจุดสิ้นสุดของคอลเลกชันตัวแจงนับจะถูกวางตำแหน่งหลังจากองค์ประกอบสุดท้ายในคอลเลกชันและ MoveNext จะส่งกลับค่าเท็จ เมื่อตัวแจงนับอยู่ที่ตำแหน่งนี้การเรียกใช้ MoveNext ที่ตามมาจะส่งกลับค่าเท็จจนกว่าจะมีการเรียกใช้การตั้งค่าใหม่

ณ จุดนี้ลูปจะแตกและลำดับของลำดับจะสิ้นสุดลง

นี่คือการทดสอบอย่างง่าย:

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

เอาท์พุท:

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

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

นอกจากนี้ก็จะทำสิ่งแปลกถ้าคุณเล่นกับการสั่งซื้อเช่นเดียวกับแซมได้เมื่อถึงจุดหนึ่ง


ฉันคิดว่านี่เป็นทางออกที่ดีที่สุด ... ปัญหาเดียวคือรายการไม่มีความยาว ... มีจำนวน แต่มันง่ายที่จะเปลี่ยน เราสามารถทำให้ดีขึ้นได้โดยไม่ต้องสร้างรายการ แต่คืนค่าตัวเลขที่มีการอ้างอิงไปยังรายการหลักด้วยชุดค่าผสมออฟเซ็ต / ความยาว ดังนั้นถ้ากลุ่มใหญ่เราจะไม่เสียความทรงจำ แสดงความคิดเห็นถ้าคุณต้องการให้ฉันเขียนขึ้น
อาเมียร์

@Amir ฉันต้องการที่จะเห็นว่าเขียนขึ้น
samandmoore

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

@SamSaffron ใช่ถ้าคุณมีรายการจำนวนList<T>มากคุณจะเห็นว่ามีปัญหาเรื่องหน่วยความจำเนื่องจากการบัฟเฟอร์ ในการหวนกลับฉันควรสังเกตว่าในคำตอบ แต่ดูเหมือนว่าในเวลาที่มุ่งเน้นการทำซ้ำมากเกินไป ที่กล่าวว่าวิธีการแก้ปัญหาของคุณคือผมที่แท้จริง ฉันยังไม่ได้ทดสอบ แต่ตอนนี้ฉันสงสัยว่ามีวิธีแก้ปัญหามีขนดกน้อยลงหรือไม่
casperOne

@casperOne ใช่ ... Google ให้ฉันหน้านี้เมื่อฉันกำลังค้นหาวิธีการแยกนับจำนวนสำหรับกรณีการใช้งานเฉพาะของฉันฉันแยกรายการใหญ่ของเมามันที่ถูกส่งคืนจากฐานข้อมูลถ้าฉันเป็นพวกเขาเพื่อ รายการมันจะระเบิดขึ้น (ในความเป็นจริงกระฉับกระเฉงมีบัฟเฟอร์: ตัวเลือกที่ผิดพลาดเพียงสำหรับกรณีการใช้งานนี้)
แซม Saffron

48

ตกลงนี่คือสิ่งที่ฉันจะทำ:

  • สันหลังยาวสมบูรณ์: ทำงานกับนับไม่ถ้วน
  • ไม่มีการคัดลอก / บัฟเฟอร์กลาง
  • เวลาดำเนินการ O (n)
  • ทำงานได้เช่นกันเมื่อลำดับภายในถูกใช้เพียงบางส่วนเท่านั้น

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

ตัวอย่างการใช้งาน

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

คำอธิบาย

รหัสทำงานโดยการซ้อนตัวyieldวนซ้ำสองตัว

ตัววนซ้ำด้านนอกต้องติดตามว่ามีองค์ประกอบจำนวนเท่าใดที่ถูกใช้อย่างมีประสิทธิภาพโดยตัววนรอบภายใน (อัน) นี้จะกระทำโดยการปิดมากกว่าด้วยremaining innerMoveNext()องค์ประกอบที่ไม่มีการระบุของชิ้นจะถูกยกเลิกก่อนที่ชิ้นถัดไปจะให้ผลโดย iterator ด้านนอก นี่เป็นสิ่งจำเป็นเพราะมิฉะนั้นคุณจะได้รับผลลัพธ์ที่ไม่สอดคล้องกันเมื่อจำนวนภายในไม่ได้ถูกใช้จนหมด (เช่นc3.Count()จะคืนค่า 6)

หมายเหตุ: คำตอบได้รับการปรับปรุงเพื่อแก้ไขข้อบกพร่องที่ชี้ให้เห็นโดย @aolszowka


2
ดีมาก. โซลูชัน "ถูกต้อง" ของฉันมีความซับซ้อนกว่านั้น นี่คือ # 1 คำตอบ IMHO
CaseyB

สิ่งนี้ได้รับผลกระทบจากพฤติกรรมที่ไม่คาดคิด (จากจุดยืน API) เมื่อมีการเรียกใช้ ToArray () ซึ่งไม่ปลอดภัยต่อเธรด
aolszowka

@aolszowka: คุณช่วยอธิบายได้ไหม?
3dGrabber

@ 3dGrabber อาจเป็นเพราะฉันได้รับรหัสของคุณอีกครั้ง (ขออภัยที่ผ่านมานานเกินไปแล้วที่นี่โดยทั่วไปแทนที่จะเป็นวิธีการขยายที่ฉันส่งผ่านใน sourceEnumerator) กรณีทดสอบที่ฉันใช้มีผลกับสิ่งนี้: int [] arrayToSort = new int [] {9, 7, 2, 6, 3, 4, 8, 5, 1, 10, 11, 12, 13}; var source = Chunkify <int> (arrayToSort, 3) .ToArray (); ส่งผลให้แหล่งที่มาระบุว่ามี 13 ชิ้น (จำนวนองค์ประกอบ) เรื่องนี้ทำให้ฉันรู้สึกว่าถ้าคุณถามการแจงนับภายในที่ Enumerator ไม่ได้เพิ่มขึ้น
aolszowka

1
@aolszowka: คะแนนที่ถูกต้องมาก ฉันได้เพิ่มคำเตือนและส่วนการใช้งานแล้ว รหัสจะถือว่าคุณวนซ้ำภายในจำนวนเต็ม ด้วยวิธีการแก้ปัญหาของคุณคุณสูญเสียความเกียจ ฉันคิดว่ามันควรจะเป็นไปได้ที่จะได้รับสิ่งที่ดีที่สุดของโลกทั้งสองด้วย IEnumerator แคชที่กำหนดเอง หากฉันพบวิธีแก้ปัญหาฉันจะโพสต์ที่นี่ ...
3dGrabber

18

ขี้เกียจสนิทไม่มีการนับหรือคัดลอก:

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

วิธีแก้ปัญหานี้สวยงามมากจนฉันขอโทษที่ฉันไม่สามารถเอาชนะคำตอบนี้ได้มากกว่าหนึ่งครั้ง
ทำเครื่องหมาย

3
ฉันไม่คิดว่าสิ่งนี้จะล้มเหลวอย่างแน่นอน แต่มันอาจมีพฤติกรรมแปลก ๆ หากคุณมี 100 รายการและคุณแบ่งเป็นแบตช์ 10 และคุณแจกแจงแบตช์ทั้งหมดโดยไม่แจกแจงไอเท็มใด ๆ ของแบทช์เหล่านั้นคุณจะจบด้วย 100 แบตช์ 1
CaseyB

1
ตามที่ @CaseyB กล่าวถึงสิ่งนี้ได้รับผลกระทบจากความล้มเหลวของ 3dGrabber เดียวกันกับที่นี่stackoverflow.com/a/20953521/1037948แต่มนุษย์มันเร็วมาก!
drzaus

1
นี่เป็นทางออกที่สวยงาม ทำสิ่งที่สัญญาไว้อย่างแน่นอน
Rod Hartzell

โดยวิธีที่หรูหราที่สุดและตรงประเด็นที่สุด มีเพียงสิ่งเดียวที่คุณควรเพิ่มการตรวจสอบตัวเลขที่เป็นลบและแทนที่ ArgumentNullException โดย ArgumentException
Romain Vergnory

13

ฉันคิดว่าคำแนะนำต่อไปนี้จะเร็วที่สุด ฉันกำลังสังเวยความเกียจคร้านของแหล่งที่มานับไม่ถ้วนสำหรับความสามารถในการใช้ Array.Copy และรู้ล่วงหน้าเกี่ยวกับความยาวของแต่ละรายการย่อยของฉัน

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

ไม่เพียงแค่เร็วที่สุดเท่านั้น แต่ยังจัดการการดำเนินการที่นับได้อย่างถูกต้องเกี่ยวกับผลลัพธ์เช่นรายการ Chunk (5). Reverse () SelectMany (x => x)
เกินไป

9

เราสามารถปรับปรุงโซลูชันของ @ JaredPar เพื่อทำการประเมินผลที่ขี้เกียจอย่างแท้จริง เราใช้GroupAdjacentByวิธีการที่ให้ผลตอบแทนกลุ่มองค์ประกอบต่อเนื่องด้วยคีย์เดียวกัน:

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

เนื่องจากกลุ่มต่างๆให้ผลลัพธ์แบบหนึ่งต่อหนึ่งโซลูชันนี้ทำงานได้อย่างมีประสิทธิภาพด้วยลำดับที่ยาวหรือไม่สิ้นสุด


8

ฉันเขียนวิธีการขยาย Clump เมื่อหลายปีก่อน ใช้งานได้ดีและเป็นการใช้งานที่รวดเร็วที่สุดที่นี่ : 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;
        }
    }
}

มันควรใช้งานได้ แต่บัฟเฟอร์ 100% ของชิ้นส่วนฉันพยายามหลีกเลี่ยง ... แต่มันกลับมีขนดกอย่างไม่น่าเชื่อ
Sam Saffron

@SamSaffron ใช่ โดยเฉพาะอย่างยิ่งถ้าคุณโยนสิ่งต่าง ๆ เช่น plinq ลงในส่วนผสมซึ่งเป็นสิ่งที่การปรับใช้ของฉันมีไว้สำหรับ
Cameron MacFarland

ขยายคำตอบของฉันให้ฉันรู้ว่าคุณคิดอย่างไร
Sam Saffron

@CameronMacFarland - คุณช่วยอธิบายได้ไหมว่าทำไมการตรวจสอบครั้งที่สองเพื่อนับ == ขนาดจึงจำเป็น ขอบคุณ
dugas

8

ระบบการโต้ตอบให้Buffer()สำหรับวัตถุประสงค์นี้ การทดสอบที่รวดเร็วแสดงให้เห็นว่าประสิทธิภาพคล้ายกับโซลูชันของแซม


1
คุณรู้หรือไม่ความหมายบัฟเฟอร์? เช่น: หากคุณมีตัวแจงนับที่แยกสตริงที่มีขนาดใหญ่ 300k และพยายามแบ่งออกเป็น 10,000 ชิ้นคุณจะได้หน่วยความจำไม่เพียงพอหรือไม่
Sam Saffron

Buffer()กลับมาIEnumerable<IList<T>>ใช่คุณอาจจะมีปัญหาอยู่ที่นั่น - มันไม่สตรีมเหมือนของคุณ
dahlbyk

7

นี่คือรูทีนรายการแยกฉันเขียนสองสามเดือนที่ผ่านมา:

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

6

ฉันพบว่าข้อมูลตัวอย่างนี้ทำงานได้ค่อนข้างดี

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

5

แล้วอันนี้หล่ะ?

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

เท่าที่ฉันทราบGetRange ()เป็นเส้นตรงในแง่ของจำนวนรายการที่ใช้ ดังนั้นสิ่งนี้ควรทำงานได้ดี


5

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

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

จะเป็นการดีที่จะแปลงให้เป็นวิธีการเสริม:public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int chunkSize)
krizzzn

+1 สำหรับคำตอบของคุณ อย่างไรก็ตามฉันขอแนะนำสองสิ่ง 1. ใช้ foreach แทน while และใช้ block 2. ส่ง chunkSize ใน Constructor ของ List เพื่อให้ list รู้ขนาดที่คาดหวังไว้สูงสุด
Usman Zafar

4

เราพบว่าโซลูชันของ David B ทำงานได้ดีที่สุด แต่เราปรับให้เป็นโซลูชันทั่วไปมากขึ้น:

list.GroupBy(item => item.SomeProperty) 
   .Select(group => new List<T>(group)) 
   .ToArray();

3
นี่เป็นสิ่งที่ดี แต่ค่อนข้างแตกต่างจากสิ่งที่ผู้ขอดั้งเดิมถาม
Amy B

4

วิธีแก้ปัญหาต่อไปนี้คือขนาดกะทัดรัดที่สุดที่ฉันสามารถทำได้ด้วยนั่นคือ 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;
    }
}

4

รหัสเก่า แต่นี่คือสิ่งที่ฉันใช้:

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

หลังจากการโพสต์ฉันรู้ว่านี่เป็นรหัสเดียวกัน casperOne โพสต์เมื่อ 6 ปีที่แล้วกับการเปลี่ยนการใช้. Anya () แทนที่จะเป็น. Count () เนื่องจากฉันไม่ต้องการการนับทั้งหมด .
Robert McKee

3

หากรายการเป็นประเภท system.collections.generic คุณสามารถใช้วิธี "CopyTo" ที่มีอยู่เพื่อคัดลอกองค์ประกอบของอาร์เรย์ไปยังอาร์เรย์ย่อยอื่น คุณระบุองค์ประกอบเริ่มต้นและจำนวนองค์ประกอบที่จะคัดลอก

คุณสามารถสร้าง 3 โคลนของรายการเดิมของคุณและใช้ "RemoveRange" ในแต่ละรายการเพื่อย่อขนาดตามขนาดที่คุณต้องการ

หรือเพียงแค่สร้างวิธีการช่วยเหลือเพื่อทำเพื่อคุณ


2

มันเป็นวิธีแก้ปัญหาแบบเก่า แต่ฉันมีวิธีการที่แตกต่าง ฉันใช้Skipเพื่อย้ายไปยังออฟเซ็ตที่ต้องการและTakeเพื่อแยกจำนวนองค์ประกอบที่ต้องการ:

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

1
คล้ายกับวิธีที่ฉันใช้ แต่ฉันขอแนะนำแหล่งที่มานั้นไม่สามารถนับได้จำนวน IE ตัวอย่างเช่นถ้าแหล่งที่มาเป็นผลมาจากการสอบถาม LINQ ข้าม / ใช้จะก่อให้เกิดการระบุจำนวน nbChunk ของแบบสอบถาม อาจมีราคาแพง ดีกว่าที่จะใช้ IList หรือ ICollection เป็นประเภทของแหล่งที่มา ที่หลีกเลี่ยงปัญหาโดยสิ้นเชิง
RB Davidson

2

สำหรับทุกคนที่สนใจในโซลูชันแบบแพคเกจ / ดูแลรักษาห้องสมุดMoreLINQมีBatchวิธีการขยายที่ตรงกับพฤติกรรมที่คุณต้องการ:

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

Batchการดำเนินงานคล้ายกับคำตอบที่คาเมรอนของ MacFarlandด้วยนอกเหนือจากเกินสำหรับการเปลี่ยนชิ้น / ชุดก่อนจะกลับมาและดำเนินการได้ค่อนข้างดี


นี่ควรเป็นคำตอบที่ยอมรับได้ แทนที่จะต้องปรับเปลี่ยนล้อควรใช้
morelinq

1

ใช้แบ่งพาร์ติชันแบบแยกส่วน:

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

1

เพียงแค่ใส่สองเซ็นต์ของฉัน หากคุณต้องการ "ฝากข้อมูล" รายการ (แสดงภาพจากซ้ายไปขวา) คุณสามารถทำสิ่งต่อไปนี้:

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


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

0

ฉันตอบคำถามหลักและทำให้เป็นที่เก็บ IOC เพื่อกำหนดว่าจะแยกจากที่ใด ( สำหรับผู้ที่ต้องการแยกเพียง 3 รายการในการอ่านโพสต์นี้ในขณะที่ค้นหาคำตอบ? )

วิธีนี้ช่วยให้สามารถแยกรายการประเภทใดก็ได้ตามต้องการ

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

ดังนั้นสำหรับรหัสจะเป็น

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

0

ดังนั้น performatic เป็นแซม Saffronวิธี 's

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

}


0

สามารถทำงานกับเครื่องกำเนิดไฟฟ้าที่ไม่มีที่สิ้นสุด:

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)

รหัสสาธิต: 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

แต่จริงๆแล้วฉันอยากจะเขียนวิธีที่สอดคล้องกันโดยไม่ต้อง linq


0

ลองดู! ฉันมีรายการองค์ประกอบที่มีตัวนับลำดับและวันที่ ทุกครั้งที่ลำดับเริ่มต้นใหม่ฉันต้องการสร้างรายการใหม่

อดีต รายการข้อความ

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

ฉันต้องการแยกรายการออกเป็นรายการแยกต่างหากเมื่อตัวนับเริ่มใหม่ นี่คือรหัส:

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

-1

เพื่อแทรกสองเซ็นต์ของฉัน ...

เมื่อใช้ชนิดรายการสำหรับแหล่งข้อมูลที่จะทำการแยกข้อมูลฉันพบวิธีแก้ไขปัญหาที่กะทัดรัดมาก:

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;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.