Chia danh sách thành các danh sách con với LINQ


377

Có cách nào để tôi có thể tách một List<SomeObject>thành nhiều danh sách riêng biệt SomeObject, sử dụng chỉ mục vật phẩm làm dấu phân cách của mỗi phân chia không?

Hãy để tôi làm gương:

Tôi có một List<SomeObject>và tôi cần một List<List<SomeObject>>hoặc List<SomeObject>[], để mỗi danh sách kết quả này sẽ chứa một nhóm 3 mục của danh sách gốc (tuần tự).

ví dụ.:

  • Danh sách gốc: [a, g, e, w, p, s, q, f, x, y, i, m, c]

  • Danh sách kết quả: [a, g, e], [w, p, s], [q, f, x], [y, i, m], [c]

Tôi cũng cần kích thước danh sách kết quả là một tham số của hàm này.

Câu trả lời:


378

Hãy thử đoạn mã sau.

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

Ý tưởng là đầu tiên nhóm các yếu tố theo chỉ mục. Chia cho ba có tác dụng nhóm chúng thành các nhóm 3. Sau đó chuyển đổi từng nhóm thành một danh sách và IEnumerabletừ Listmột Listđến Lists


21
GroupBy thực hiện một loại ngầm định. Điều đó có thể giết chết hiệu suất. Những gì chúng ta cần là một số loại nghịch đảo của SelectMany.
yfeldblum

5
@Justice, GroupBy có thể được thực hiện bằng cách băm. Làm thế nào để bạn biết việc triển khai GroupBy "có thể giết chết hiệu suất"?
Amy B

5
GroupBy không trả lại bất cứ điều gì cho đến khi liệt kê tất cả các yếu tố. Đó là lý do tại sao nó chậm. Các danh sách mà OP muốn liên tục, vì vậy một phương pháp tốt hơn có thể mang lại danh sách con đầu tiên [a,g,e]trước khi liệt kê thêm bất kỳ danh sách gốc nào.
Đại tá Panic

9
Lấy ví dụ cực đoan về một IEnumerable vô hạn. GroupBy(x=>f(x)).First()sẽ không bao giờ mang lại một nhóm. OP đã hỏi về danh sách, nhưng nếu chúng ta viết để làm việc với IEnumerable, chỉ thực hiện một lần lặp duy nhất, chúng ta sẽ gặt hái được lợi thế về hiệu suất.
Đại tá Panic

8
@Nick Đặt hàng không được bảo quản theo cách của bạn mặc dù. Đây vẫn là một điều tốt để biết nhưng bạn sẽ nhóm chúng thành (0,3,6,9, ...), (1,4,7,10, ...), (2,5,8 , 11, ...). Nếu thứ tự không quan trọng thì nó vẫn ổn nhưng trong trường hợp này có vẻ như nó quan trọng.
Reafexus

325

Câu hỏi này hơi cũ, nhưng tôi chỉ viết câu này và tôi nghĩ nó thanh lịch hơn một chút so với các giải pháp được đề xuất khác:

/// <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
Yêu giải pháp này. Tôi khuyên bạn nên thêm kiểm tra độ tỉnh táo này để ngăn chặn vòng lặp vô hạn: if (chunksize <= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize");
mroach

10
Tôi thích điều này, nhưng nó không siêu hiệu quả
Sam Saffron

51
Tôi thích cái này nhưng hiệu quả về thời gian O(n²). Bạn có thể lặp qua danh sách và có được một O(n)thời gian.
hIpPy

8
@hIpPy, thế nào rồi n ^ 2? Có vẻ tuyến tính với tôi
Vivek Maharajh

13
@vivekmaharajh sourceđược thay thế bằng một gói IEnumerablemỗi lần. Vì vậy, lấy các yếu tố từ sourcetrải qua các lớp Skips
Lasse Espeholt

99

Nói chung, cách tiếp cận được đề xuất bởi CaseyB hoạt động tốt, trên thực tế nếu bạn đang List<T>gặp khó khăn, có lẽ tôi sẽ thay đổi nó thành:

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

Điều này sẽ tránh các chuỗi cuộc gọi lớn. Tuy nhiên, cách tiếp cận này có một lỗ hổng chung. Nó cụ thể hóa hai bảng liệt kê cho mỗi đoạn, để làm nổi bật vấn đề hãy thử chạy:

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

Để khắc phục điều này, chúng ta có thể thử cách tiếp cận của Cameron , vượt qua bài kiểm tra trên về màu sắc bay vì nó chỉ đi bộ liệt kê một lần.

Rắc rối là nó có một lỗ hổng khác nhau, nó cụ thể hóa mọi vật phẩm trong mỗi khối, rắc rối với cách tiếp cận đó là bạn chạy bộ nhớ cao.

Để minh họa rằng hãy thử chạy:

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

Cuối cùng, bất kỳ triển khai nào cũng có thể xử lý các vòng lặp theo thứ tự, ví dụ:

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

Nhiều giải pháp tối ưu cao như sửa đổi đầu tiên của tôi về câu trả lời này đã thất bại ở đó. Vấn đề tương tự có thể được nhìn thấy trong câu trả lời tối ưu hóa của casperOne .

Để giải quyết tất cả các vấn đề này, bạn có thể sử dụng như sau:

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


        }

    }
}

Ngoài ra còn có một loạt các tối ưu hóa mà bạn có thể giới thiệu cho các lần lặp không theo thứ tự của các khối, nằm ngoài phạm vi ở đây.

Bạn nên chọn phương pháp nào? Nó hoàn toàn phụ thuộc vào vấn đề bạn đang cố gắng giải quyết. Nếu bạn không quan tâm đến lỗ hổng đầu tiên, câu trả lời đơn giản là vô cùng hấp dẫn.

Lưu ý như với hầu hết các phương pháp, điều này không an toàn cho đa luồng, công cụ có thể trở nên kỳ lạ nếu bạn muốn làm cho luồng an toàn, bạn sẽ cần phải sửa đổi EnumeratorWrapper.


Lỗi sẽ là Enumerable.Range (0, 100) .Chunk (3) .Reverse (). ToArray () bị sai, hoặc Enumerable.Range (0, 100) .ToArray (). Chunk (3) .Reverse () .ToArray () ném ngoại lệ?
Cameron MacFarland

@SamSaffron Tôi đã cập nhật câu trả lời của mình và đơn giản hóa mã rất nhiều cho những gì tôi cảm thấy là trường hợp sử dụng nổi bật (và thừa nhận sự cẩn thận).
casperOne

Điều gì về việc cắt xén IQueryable <>? Tôi đoán là cách tiếp cận Take / Skip sẽ là tối ưu nếu chúng ta muốn ủy thác tối đa các hoạt động cho nhà cung cấp
Guillaume86

@ Guillaume86 Tôi đồng ý, nếu bạn có IList hoặc IQueryable, bạn có thể sử dụng tất cả các loại phím tắt để thực hiện việc này nhanh hơn nhiều (Linq thực hiện việc này trong tất cả các loại phương pháp khác)
Sam Saffron

1
Đây là câu trả lời tốt nhất cho hiệu quả. Tôi gặp sự cố khi sử dụng SqlBulkCopy với IEnumerable chạy các quy trình bổ sung trên mỗi cột, do đó, nó phải chạy hiệu quả chỉ với một lần vượt qua. Điều này sẽ cho phép tôi chia IEnumerable thành các phần có kích thước có thể quản lý. (Đối với những người thắc mắc, tôi đã kích hoạt chế độ phát trực tuyến của SqlBulkCopy, dường như bị hỏng).
Brain2000 30/03/2016

64

Bạn có thể sử dụng một số truy vấn sử dụng TakeSkip, nhưng điều đó sẽ thêm quá nhiều lần lặp vào danh sách ban đầu, tôi tin.

Thay vào đó, tôi nghĩ bạn nên tạo một trình vòng lặp của riêng bạn, như vậy:

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

Sau đó, bạn có thể gọi đây và nó được bật LINQ để bạn có thể thực hiện các thao tác khác trên chuỗi kết quả.


Trước câu trả lời của Sam , tôi cảm thấy có một cách dễ dàng hơn để làm điều này mà không cần:

  • Lặp lại danh sách một lần nữa (điều mà trước đây tôi không làm)
  • Vật chất hóa các vật phẩm trong nhóm trước khi phát hành khối (đối với khối vật phẩm lớn, sẽ có vấn đề về bộ nhớ)
  • Tất cả các mã mà Sam đã đăng

Điều đó nói rằng, đây là một đường chuyền khác, mà tôi đã mã hóa theo phương thức mở rộng để IEnumerable<T>gọi 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);
}

Không có gì đáng ngạc nhiên trên đó, chỉ cần kiểm tra lỗi cơ bản.

Chuyển sang 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);
}

Về cơ bản, nó được IEnumerator<T>lặp và thủ công lặp qua từng mục. Nó kiểm tra xem nếu có bất kỳ mục nào hiện đang được liệt kê. Sau khi mỗi đoạn được liệt kê thông qua, nếu không còn vật phẩm nào, nó sẽ vỡ ra.

Khi phát hiện có các mục trong chuỗi, nó ủy thác trách nhiệm IEnumerable<T>thực hiện bên trong để 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đã được gọi vào IEnumerator<T>được chuyển đến ChunkSequence, nó mang lại vật phẩm được trả về Currentvà sau đó tăng số đếm, đảm bảo không bao giờ trả lại nhiều hơn chunkSizevật phẩm và chuyển sang mục tiếp theo trong chuỗi sau mỗi lần lặp (nhưng bị ngắn mạch nếu số lượng các mặt hàng mang lại vượt quá kích thước chunk).

Nếu không còn mục nào, thì InternalChunkphương thức sẽ thực hiện một lần chuyển tiếp khác trong vòng lặp bên ngoài, nhưng khi MoveNextđược gọi lần thứ hai, nó vẫn trả về false, theo tài liệu (nhấn mạnh của tôi):

Nếu MoveNext vượt qua phần cuối của bộ sưu tập, bộ liệt kê được định vị sau phần tử cuối cùng trong bộ sưu tập và MoveNext trả về sai. Khi điều tra viên ở vị trí này, các cuộc gọi tiếp theo tới MoveNext cũng trả về false cho đến khi Reset được gọi.

Tại thời điểm này, vòng lặp sẽ bị phá vỡ và chuỗi các chuỗi sẽ chấm dứt.

Đây là một bài kiểm tra đơn giản:

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

Đầu ra:

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

Một lưu ý quan trọng, điều này sẽ không hoạt động nếu bạn không rút toàn bộ chuỗi con hoặc ngắt tại bất kỳ điểm nào trong chuỗi cha. Đây là một cảnh báo quan trọng, nhưng nếu trường hợp sử dụng của bạn là bạn sẽ tiêu thụ mọi yếu tố của chuỗi trình tự, thì điều này sẽ làm việc cho bạn.

Ngoài ra, nó sẽ làm những điều kỳ lạ nếu bạn chơi theo thứ tự, giống như Sam đã làm tại một thời điểm .


Tôi nghĩ rằng đây là giải pháp tốt nhất ... vấn đề duy nhất là danh sách đó không có Độ dài ... nó có Đếm. Nhưng điều đó dễ thay đổi. Chúng ta có thể làm cho điều này tốt hơn bằng cách thậm chí không xây dựng Danh sách mà trả về ienumerables có chứa các tham chiếu đến danh sách chính với sự kết hợp bù / độ dài. Vì vậy, nếu kích thước nhóm lớn, chúng ta sẽ không lãng phí bộ nhớ. Bình luận nếu bạn muốn tôi viết nó lên.
Amir

@Amir Tôi muốn xem bằng văn bản
samandmoore

Điều này rất hay và nhanh chóng - Cameron cũng đã đăng một cái rất giống với bạn, chỉ có một điều lưu ý là nó đệm các khối, điều này có thể dẫn đến tình trạng hết bộ nhớ nếu kích thước khối và vật phẩm lớn. Xem câu trả lời của tôi cho một thay thế, mặc dù nhiều hairier, câu trả lời.
Sam Saffron

@SamSaffron Vâng, nếu bạn có một số lượng lớn các mục trong đó List<T>, rõ ràng bạn sẽ gặp vấn đề về bộ nhớ vì bộ đệm. Nhìn lại, tôi nên lưu ý rằng trong câu trả lời, nhưng dường như tại thời điểm đó, trọng tâm là quá nhiều lần lặp lại. Điều đó nói rằng, giải pháp của bạn thực sự là hairier. Tôi đã không thử nó, nhưng bây giờ tôi tự hỏi liệu có một giải pháp ít lông hơn không.
casperOne

@casperOne yeah ... Google đã cho tôi trang này khi tôi đang tìm cách chia nhỏ số liệt kê, cho trường hợp sử dụng cụ thể của tôi, tôi đang chia một danh sách lớn các bản ghi được trả về từ db, nếu tôi cụ thể hóa chúng thành một liệt kê nó sẽ nổ tung (trên thực tế dapper có bộ đệm: tùy chọn sai chỉ dành cho trường hợp sử dụng này)
Sam Saffron

48

Ok, đây là của tôi về nó:

  • hoàn toàn lười biếng: làm việc trên vô số
  • không sao chép / đệm trung gian
  • Thời gian thực hiện O (n)
  • cũng hoạt động khi trình tự bên trong chỉ được tiêu thụ một phầ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());
}

Cách sử dụng ví dụ

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

Giải thích

Mã này hoạt động bằng cách lồng hai yieldvòng lặp dựa trên.

Trình lặp bên ngoài phải theo dõi xem có bao nhiêu phần tử đã được tiêu thụ hiệu quả bởi trình vòng lặp bên trong (chunk). Điều này được thực hiện bằng cách kết thúc remainingvới innerMoveNext(). Các phần tử không được phát hiện của một đoạn được loại bỏ trước khi đoạn tiếp theo được tạo ra bởi trình vòng lặp bên ngoài. Điều này là cần thiết bởi vì nếu không, bạn nhận được kết quả không nhất quán, khi các liệt kê bên trong không được tiêu thụ (hoàn toàn) (ví dụ c3.Count()sẽ trả về 6).

Lưu ý: Câu trả lời đã được cập nhật để giải quyết những thiếu sót được chỉ ra bởi @aolszowka.


2
Rất đẹp. Giải pháp "đúng" của tôi phức tạp hơn thế nhiều. Đây là câu trả lời số 1 IMHO.
CaseyB

Điều này bị hành vi bất ngờ (từ quan điểm API) khi ToArray () được gọi, nó cũng không an toàn cho chuỗi.
aolszowka

@aolszowka: bạn có thể giải thích rõ hơn không?
3dGrabber

@ 3dGrabber Có lẽ đó là cách tôi xác nhận lại mã của bạn (xin lỗi, quá lâu để vượt qua đây, về cơ bản thay vì một phương thức mở rộng mà tôi đã truyền trong sourceEnumerator). Trường hợp thử nghiệm mà tôi đã sử dụng là một cái gì đó cho hiệu ứng này: int [] ArrayToSort = new int [] {9, 7, 2, 6, 3, 4, 8, 5, 1, 10, 11, 12, 13}; var source = Chunkify <int> (mảngToSort, 3) .ToArray (); Kết quả trong Nguồn chỉ ra rằng có 13 khối (số phần tử). Điều này có ý nghĩa với tôi như trừ khi bạn truy vấn các bảng liệt kê bên trong, Trình liệt kê không được tăng lên.
aolszowka

1
@aolszowka: điểm rất hợp lệ. Tôi đã thêm một cảnh báo và một phần sử dụng. Mã giả định rằng bạn lặp đi lặp lại vô số bên trong. Với giải pháp của bạn, bạn mất đi sự lười biếng mặc dù. Tôi nghĩ rằng nó có thể có được tốt nhất của cả hai thế giới với một IEnumerator tùy chỉnh, bộ nhớ đệm. Nếu tôi tìm thấy giải pháp tôi sẽ đăng nó ở đây ...
3dGrabber

18

hoàn toàn lười biếng, không đếm hoặc sao chép:

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

Giải pháp này rất thanh lịch đến nỗi tôi xin lỗi vì tôi không thể đưa ra câu trả lời này nhiều lần.
Đánh dấu

3
Tôi không nghĩ rằng điều này sẽ thất bại, chính xác. Nhưng nó chắc chắn có thể có một số hành vi kỳ lạ. Nếu bạn có 100 mặt hàng và bạn chia thành 10 lô và bạn liệt kê tất cả các lô mà không liệt kê bất kỳ mặt hàng nào trong các lô đó, bạn sẽ kết thúc với 100 lô 1.
CaseyB

1
Như @CaseyB đã đề cập, điều này bị lỗi 3dGrabber tương tự được giải quyết ở đây stackoverflow.com/a/20953521/1037948 , nhưng con người nhanh quá!
drzaus

1
Đây là một giải pháp đẹp. Làm chính xác những gì nó hứa.
Rod Hartzell

Cho đến nay thanh lịch nhất và giải pháp điểm. Chỉ có điều, bạn nên thêm một kiểm tra cho các số âm và thay thế ArgumentNullException bằng một ArgumentException
Romain Vergnory

13

Tôi nghĩ rằng gợi ý sau đây sẽ là nhanh nhất. Tôi đang hy sinh sự lười biếng của nguồn Có thể sử dụng Array.Copy và biết trước thời lượng của mỗi danh sách con của tôi.

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

Không chỉ nhanh nhất, nó còn xử lý chính xác các thao tác vô số khác trên kết quả, tức là các mặt hàng.Chunk (5) .Reverse (). ChọnMany (x => x)
quá

9

Chúng tôi có thể cải thiện giải pháp của @ JaredPar để thực hiện đánh giá lười biếng thực sự. Chúng tôi sử dụng một GroupAdjacentByphương pháp mang lại các nhóm yếu tố liên tiếp có cùng khóa:

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

Bởi vì các nhóm được mang lại từng cái một, giải pháp này hoạt động hiệu quả với các chuỗi dài hoặc vô hạn.


8

Tôi đã viết một phương pháp mở rộng Clump vài năm trước. Hoạt động tuyệt vời, và là thực hiện nhanh nhất ở đây. : 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;
        }
    }
}

Nó nên hoạt động nhưng nó được đệm 100% các khối, tôi đã cố tránh điều đó ... nhưng hóa ra là lông cực kỳ.
Sam Saffron

@SamSaffron Yep. Đặc biệt là nếu bạn ném những thứ như plinq vào hỗn hợp, đó là những gì tôi thực hiện ban đầu.
Cameron MacFarland

mở rộng câu trả lời của tôi, cho tôi biết bạn nghĩ gì
Sam Saffron

@CameronMacFarland - bạn có thể giải thích lý do tại sao kiểm tra thứ hai cho số lượng == kích thước là cần thiết? Cảm ơn.
dugas

8

System.Interactive cung cấp Buffer()cho mục đích này. Một số thử nghiệm nhanh cho thấy hiệu suất tương tự như giải pháp của Sam.


1
Bạn có biết ngữ nghĩa đệm? ví dụ: nếu bạn có một bộ liệt kê phát ra các chuỗi lớn 300k và cố gắng chia nó thành 10.000 khối kích thước, bạn sẽ thoát khỏi bộ nhớ chứ?
Sam Saffron

Buffer()trả về IEnumerable<IList<T>>vì vậy, có lẽ bạn có vấn đề ở đó - nó không phát trực tuyến như của bạn.
dahlbyk

7

Đây là một thói quen chia tách danh sách tôi đã viết vài tháng trước:

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

Tôi thấy đoạn trích nhỏ này thực hiện công việc khá độc đáo.

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

Cái này thì sao?

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

Theo như tôi biết, GetRange () là tuyến tính về số lượng vật phẩm được lấy. Vì vậy, điều này nên thực hiện tốt.


5

Đây là một câu hỏi cũ nhưng đây là những gì tôi đã kết thúc; nó chỉ liệt kê vô số một lần, nhưng không tạo danh sách cho mỗi phân vùng. Nó không bị hành vi bất ngờ khi ToArray()được gọi là một số thực hiện:

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

Sẽ là tốt để chuyển đổi điều này thành một phương pháp mở rộng:public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int chunkSize)
krizzzn

+1 cho câu trả lời của bạn. Tuy nhiên tôi khuyên bạn nên hai điều 1. sử dụng foreach thay vì while và sử dụng block. 2. Truyền chunkSize trong hàm tạo của List để danh sách đó biết kích thước tối đa dự kiến ​​của nó.
Usman Zafar

4

Chúng tôi thấy giải pháp của David B hoạt động tốt nhất. Nhưng chúng tôi đã điều chỉnh nó thành một giải pháp tổng quát hơn:

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

3
Điều này là tốt, nhưng khá khác với những gì người hỏi ban đầu đã yêu cầu.
Amy B

4

Giải pháp sau đây nhỏ gọn nhất mà tôi có thể nghĩ ra đó là 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

Mã cũ, nhưng đây là những gì tôi đã sử dụng:

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

Sau khi đăng, tôi nhận ra điều này khá chính xác cùng mã casperOne đã đăng 6 năm trước với việc thay đổi sử dụng .Any () thay vì .Count () vì tôi không cần toàn bộ số, chỉ cần biết có tồn tại không .
Robert McKee

3

Nếu danh sách thuộc kiểu system.collections.generic, bạn có thể sử dụng phương thức "CopyTo" có sẵn để sao chép các phần tử của mảng sang các mảng phụ khác. Bạn chỉ định phần tử bắt đầu và số phần tử cần sao chép.

Bạn cũng có thể tạo 3 bản sao của danh sách ban đầu của mình và sử dụng "RemoveRange" trên mỗi danh sách để thu nhỏ danh sách theo kích thước bạn muốn.

Hoặc chỉ cần tạo một phương thức trợ giúp để làm điều đó cho bạn.


2

Đó là một giải pháp cũ nhưng tôi đã có một cách tiếp cận khác. Tôi sử dụng Skipđể di chuyển đến phần bù mong muốn và Takeđể trích xuất số phần tử mong muốn:

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
Rất giống với cách tiếp cận tôi đã sử dụng, nhưng tôi khuyên nguồn đó không phải là IEnumerable. Ví dụ: nếu nguồn là kết quả của truy vấn LINQ, thì Skip / Take sẽ kích hoạt liệt kê nbChunk của truy vấn. Có thể nhận được đắt tiền. Tốt hơn là sử dụng IList hoặc ICollection làm loại cho nguồn. Điều đó tránh được vấn đề hoàn toàn.
RB Davidson

2

Đối với bất kỳ ai quan tâm đến giải pháp đóng gói / bảo trì, thư viện MoreLINEQ cung cấp Batchphương thức mở rộng phù hợp với hành vi bạn yêu cầu:

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

Việc Batchthực hiện tương tự như câu trả lời của Cameron MacFarland , với việc bổ sung quá tải để chuyển đổi khối / lô trước khi quay lại và thực hiện khá tốt.


đây sẽ là câu trả lời được chấp nhận Thay vì phát minh lại bánh xe, nên sử dụng
morelinq

1

Sử dụng phân vùng mô-đun:

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

Chỉ cần đặt hai xu của tôi. Nếu bạn muốn "xô" danh sách (hiển thị từ trái sang phải), bạn có thể làm như sau:

 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

Một cách khác là sử dụng toán tử 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();

IMHO câu trả lời porper nhất.
Stanislav Berkov

1
public static List<List<T>> GetSplitItemsList<T>(List<T> originalItemsList, short number)
    {
        var listGroup = new List<List<T>>();
        int j = number;
        for (int i = 0; i < originalItemsList.Count; i += number)
        {
            var cList = originalItemsList.Take(j).Skip(i).ToList();
            j += number;
            listGroup.Add(cList);
        }
        return listGroup;
    }

0

Tôi lấy câu trả lời chính và biến nó thành một thùng chứa IOC để xác định nơi cần chia. ( Đối với những người thực sự đang tìm cách chỉ chia trên 3 mục, khi đọc bài đăng này trong khi tìm kiếm câu trả lời? )

Phương pháp này cho phép một người phân chia trên bất kỳ loại mặt hàng nào khi cần thiết.

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

Vì vậy, đối với OP, mã sẽ là

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

Thật tuyệt vời khi tiếp cận Sam Saffron .

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

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

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

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

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

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

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

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

}


0

Có thể làm việc với máy phát vô hạn:

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)

Mã trình diễn: 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

Nhưng thực sự tôi muốn viết phương thức tương ứng mà không có linq.


0

Kiểm tra này! Tôi có một danh sách các yếu tố với một bộ đếm chuỗi và ngày. Mỗi lần trình tự khởi động lại, tôi muốn tạo một danh sách mới.

Ví dụ. danh sách tin nhắn.

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

Tôi muốn chia danh sách thành các danh sách riêng khi bộ đếm khởi động lại. Đây là mã:

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

Để chèn hai xu của tôi ...

Bằng cách sử dụng loại danh sách cho nguồn được phân đoạn, tôi tìm thấy một giải pháp rất nhỏ gọn khác:

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;
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.