使用 LINQ 将列表拆分为子列表

Posted

技术标签:

【中文标题】使用 LINQ 将列表拆分为子列表【英文标题】:Split List into Sublists with LINQ 【发布时间】:2010-09-29 23:03:25 【问题描述】:

有什么方法可以将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]

我还需要将结果列表大小作为此函数的参数。

【问题讨论】:

【参考方案1】:

试试下面的代码。

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 组的效果。然后将每个组转换为列表,并将 ListIEnumerable 转换为 ListLists

【讨论】:

GroupBy 进行隐式排序。这会影响性能。我们需要的是某种 SelectMany 的逆。 @Justice,GroupBy 可能通过散列实现。你怎么知道 GroupBy 的实现“会扼杀性能”? GroupBy 在枚举所有元素之前不会返回任何内容。这就是为什么它很慢。 OP 想要的列表是连续的,因此更好的方法可以在枚举更多原始列表之前产生第一个子列表 [a,g,e] 以无限 IEnumerable 为例。 GroupBy(x=&gt;f(x)).First() 永远不会产生一个组。 OP 询问了列表,但如果我们使用 IEnumerable 编写代码,只进行一次迭代,我们将获得性能优势。 @Nick 订单并没有按照您的方式保留。知道这仍然是一件好事,但您会将它们分组为(0,3,6,9,...),(1,4,7,10,...),(2,5,8 ,11,...)。如果顺序无关紧要,那很好,但在这种情况下,它听起来很重要。【参考方案2】:

我刚刚写了这个,我认为它比其他建议的解决方案更优雅一点:

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

【讨论】:

喜欢这个解决方案。我建议添加此完整性检查以防止无限循环:if (chunksize &lt;= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize"); 我喜欢这个,但效率不高 我喜欢这个,但时间效率是O(n²)。您可以遍历列表并获得O(n) 时间。 @hIpPy,n^2 怎么样?在我看来是线性的 @vivekmaharajh source 每次都被包裹的IEnumerable 替换。所以从source 中获取元素需要经过Skips 层【参考方案3】:

一般CaseyB 建议的方法可以正常工作,事实上,如果您传入List&lt;T&gt; 很难出错,也许我会将其更改为:

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;
   

这将避免大量的调用链。尽管如此,这种方法有一个普遍的缺陷。它实现了每个块的两个枚举,以突出尝试运行的问题:

foreach (var item in Enumerable.Range(1, int.MaxValue).Chunk(8).Skip(100000).First())

   Console.WriteLine(item);

// wait forever 

为了克服这个问题,我们可以尝试Cameron's 方法,它通过了上述测试,因为它只遍历了一次枚举。

问题在于它有一个不同的缺陷,它实现了每个块中的每个项目,这种方法的问题是你的内存很高。

为了说明尝试运行:

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]

许多高度优化的解决方案,比如我的第一个revision 这个答案在那里失败了。在casperOne's optimized 答案中可以看到同样的问题。

要解决所有这些问题,您可以使用以下方法:

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() 抛出异常? @SamSaffron 我已经更新了我的答案并极大地简化了代码,因为我认为这是突出的用例(并承认警告)。 如何分块 IQueryable?我的猜测是,如果我们想将最多的操作委托给提供者,那么 Take/Skip 方法将是最佳的 @Guillaume86 我同意,如果您有 IList 或 IQueryable,您可以采用各种捷径,这会加快速度(Linq 在内部为各种其他方法执行此操作) 这是迄今为止效率的最佳答案。我在使用带有 IEnumerable 的 SqlBulkCopy 时遇到问题,该 IEnumerable 在每一列上运行额外的进程,因此它必须只通过一次就可以有效地运行。这将允许我将 IEnumerable 分解为可管理大小的块。 (对于那些想知道的人,我确实启用了 SqlBulkCopy 的流模式,这似乎被破坏了)。【参考方案4】:

可以使用大量使用TakeSkip 的查询,但我相信这会在原始列表中添加太多迭代。

相反,我认为您应该创建自己的迭代器,如下所示:

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,因此您可以对生成的序列执行其他操作。


鉴于Sam's answer,我觉得有一种更简单的方法可以做到这一点:

再次遍历列表(我最初没有这样做) 在释放块之前按组实现项目(对于大块项目,会出现内存问题) Sam 发布的所有代码

也就是说,这是另一个传递,我已将其编码为 IEnumerable&lt;T&gt; 的扩展方法,称为 Chunk

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
    int chunkSize)

    // Validate parameters.
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (chunkSize <= 0) throw new ArgumentOutOfRangeException(nameof(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&lt;T&gt; 并手动迭代每个项目。它检查当前是否有要枚举的项目。每一个chunk被枚举完后,如果没有剩下的item,就会爆发出来。

一旦检测到序列中有项目,它会将内部IEnumerable&lt;T&gt; 实现的责任委托给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 已经在传递给ChunkSequenceIEnumerator&lt;T&gt; 上调用,它产生Current 返回的项目,然后增加计数,确保返回的项目永远不会超过chunkSize 并移动到每次迭代后序列中的下一项(但如果产生的项数超过块大小,则会短路)。

如果没有剩余项目,那么InternalChunk 方法将在外循环中再进行一次传递,但是当第二次调用MoveNext 时,它仍然会返回false,as per the documentation(强调我的):

如果 MoveNext 超过集合的末尾,则枚举数为 定位在集合中的最后一个元素和 MoveNext 之后 返回假。 当枚举器在这个位置时,后续 在调用 Reset 之前,对 MoveNext 的调用也会返回 false。

此时,循环将中断,序列序列将终止。

这是一个简单的测试:

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,

重要说明,如果您不耗尽整个子序列或在父序列中的任何点中断,这将不起作用。这是一个重要的警告,但如果您的用例是您将消耗序列序列中的每个元素,那么这对您有用。

另外,如果你按顺序玩,它会做一些奇怪的事情,就像Sam's did at one point。

【讨论】:

我认为这是最好的解决方案......唯一的问题是列表没有长度......它有计数。但这很容易改变。我们甚至可以通过不构造列表而是返回包含对主列表的引用以及偏移/长度组合的 ienumerables 来使这一点变得更好。那么,如果组大小很大,我们就不会浪费内存。如果你想让我写出来,请评论。 @Amir 我想看看写下来 这很好而且很快——Cameron 在你之后也发布了一个非常相似的,唯一需要注意的是它缓冲块,如果块和项目大小很大,这可能导致内存不足。请参阅我的答案以获得替代答案,尽管答案要复杂得多。 @SamSaffron 是的,如果您在List&lt;T&gt; 中有大量项目,那么您显然会因为缓冲而出现内存问题。回想起来,我应该在答案中注意到这一点,但当时的重点似乎是太多的迭代。也就是说,您的解决方案确实更棘手。我还没有测试过它,但现在它让我想知道是否有更简单的解决方案。 @casperOne 是的......当我在寻找一种拆分枚举的方法时,Google 给了我这个页面,对于我的特定用例,我正在拆分从db,如果我将它们具体化为一个列表,它会爆炸(事实上,dapper 有一个 buffer:false 选项仅适用于这个用例)【参考方案5】:

好的,这是我的看法:

完全懒惰:适用于无限枚举 没有中间复制/缓冲 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 的迭代器来工作。

外部迭代器必须跟踪内部(块)迭代器有效消耗了多少元素。这是通过用innerMoveNext() 关闭remaining 来完成的。在外部迭代器产生下一个块之前,块中未使用的元素被丢弃。 这是必要的,因为否则当内部枚举没有(完全)被消耗(例如c3.Count() 将返回 6)时,您会得到不一致的结果。

注意: 已更新答案以解决@aolszowka 指出的缺点。

【讨论】:

非常好。我的“正确”解决方案比这复杂得多。这是第一个答案恕我直言。 这会在调用 ToArray() 时出现意外(从 API 的角度来看)行为,它也不是线程安全的。 @aolszowka:你能详细说明一下吗? @aolszowka:非常有效的积分。我添加了一个警告和一个使用部分。该代码假定您遍历内部可枚举。使用您的解决方案,您将失去懒惰。我认为应该可以通过自定义的缓存 IEnumerator 获得两全其美的效果。如果我找到解决方案,我会在这里发布... @3dGrabber 我正在尝试将它(因为优雅)用于非惰性情况来拆分更大的复杂对象集合(基本上是 get 和 .ToList()),但似乎无法得到它返回的不仅仅是第一个块。没有自定义枚举器。意识到这是模糊的,知道为什么直接(非通用)副本会发生这种情况吗?【参考方案6】:

完全懒惰,不计算也不复制:

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;
     
  

【讨论】:

这个解决方案非常优雅,很抱歉我不能多次支持这个答案。 我认为这绝对不会失败。但它肯定会有一些奇怪的行为。如果您有 100 个项目,并且您分成 10 个批次,并且您枚举了所有批次而不枚举这些批次中的任何项目,那么您最终会得到 100 个批次的 1。 正如@CaseyB 所提到的,这也受到了此处***.com/a/20953521/1037948 的同样失败的 3dGrabber 的影响,但是伙计,它很快! 这是一个很好的解决方案。完全按照它的承诺行事。 迄今为止最优雅且切中要害的解决方案。唯一的事情是,您应该添加对负数的检查,并将 ArgumentNullException 替换为 ArgumentException【参考方案7】:

我认为以下建议是最快的。我牺牲了源 Enumerable 的惰性来使用 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;
    

【讨论】:

不仅最快,它还能正确处理对结果的进一步可枚举操作,即 items.Chunk(5).Reverse().SelectMany(x => x)【参考方案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% 的块,我试图避免这种情况......但结果却是令人难以置信的毛茸茸。 @SamSaffron 是的。特别是如果你把 plinq 之类的东西混在一起,这就是我的实现最初的目的。 扩展了我的答案,让我知道你的想法【参考方案9】:

我们可以改进@JaredPar 的解决方案来进行真正的惰性评估。我们使用GroupAdjacentBy 方法生成具有相同键的连续元素组:

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

由于组是一个接一个产生的,因此该解决方案可以有效地处理长序列或无限序列。

【讨论】:

【参考方案10】:

System.Interactive 为此目的提供Buffer()。一些快速测试表明性能与 Sam 的解决方案相似。

【讨论】:

你知道缓冲语义吗?例如:如果您有一个枚举器可以输出 300k 大的字符串并尝试将其拆分为 10,000 个大小的块,您会出现内存不足的情况吗? Buffer() 返回IEnumerable&lt;IList&lt;T&gt;&gt; 所以是的,你可能会遇到问题 - 它不像你的那样流式传输。 是的,但是如果你想要流式传输,那么在同一个 repo (Rx.NET) 中使用 Observable Buffer 方法【参考方案11】:

对于任何对打包/维护解决方案感兴趣的人,MoreLINQ 库提供了与您请求的行为相匹配的 Batch 扩展方法:

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

The Batch implementation 类似于Cameron MacFarland's answer,增加了一个重载,用于在返回之前转换块/批处理,并且性能相当好。

【讨论】:

这应该是公认的答案。与其重新发明***,不如使用morelinq 确实如此。检查了 github 上的源代码,它优于此页面上的任何内容。包括我的回答 :) 我最初确实检查了 moreLinq,但我正在寻找名称中带有“Chunk”的东西。【参考方案12】:

这是我几个月前写的一个列表拆分例程:

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;

【讨论】:

【参考方案13】:

我发现这个小 sn-p 做得很好。

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;
    

【讨论】:

【参考方案14】:

我们发现 David B 的解决方案效果最好。但我们将其调整为更通用的解决方案:

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

【讨论】:

这很好,但与最初的提问者要求的完全不同。【参考方案15】:

这个呢?

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() 与所取物品的数量呈线性关系。所以这应该表现良好。

【讨论】:

【参考方案16】:

这是一个老问题,但这是我最终得到的;它仅枚举一次可枚举,但确实为每个分区创建列表。当 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&lt;IEnumerable&lt;T&gt;&gt; Partition&lt;T&gt;(this IEnumerable&lt;T&gt; source, int chunkSize) +1 为您解答。但是我推荐两件事 1. 使用 foreach 而不是 while 和使用块。 2. 在List的构造函数中传递chunkSize,让list知道它的最大期望大小。【参考方案17】:

旧代码,但这是我一直在使用的:

    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 年前发布的代码几乎完全相同,只是使用 .Any() 而不是 .Count() 因为我不需要全部计数,只需要知道是否存在。【参考方案18】:

以下解决方案是我能想到的最紧凑的解决方案,即 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;
    

【讨论】:

【参考方案19】:

通过引入 .NET 6.0,向 System.Linq 命名空间添加了一个名为 Chunk 的新方法:

Enumerable.Chunk(IEnumerable, Int32) Method

public static System.Collections.Generic.IEnumerable<TSource[]> Chunk<TSource> (this System.Collections.Generic.IEnumerable<TSource> source, int size);

使用这个新方法,除了最后一个块之外的每个块的大小都是size。最后一个块将包含剩余的元素,并且可能会更小。

您可能在想,为什么不使用 Skip and Take 呢?确实如此,我认为这只是更简洁一些,并且使内容更具可读性。

这是一个例子:

var list = Enumerable.Range(1, 100);
var chunkSize = 10;
foreach(var chunk in list.Chunk(chunkSize)) //Returns a chunk with the correct size. 

    Parallel.ForEach(chunk, (item) =>
    
        //Do something Parallel here. 
        Console.WriteLine(item);
    );

【讨论】:

【参考方案20】:

如果列表是 system.collections.generic 类型,您可以使用可用的“CopyTo”方法将数组的元素复制到其他子数组。您指定要复制的起始元素和元素数。

您还可以复制原始列表的 3 个副本,并使用每个列表上的“RemoveRange”将列表缩小到您想要的大小。

或者只是创建一个帮助方法来为你做这件事。

【讨论】:

【参考方案21】:

这是一个旧的解决方案,但我有不同的方法。我使用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));

【讨论】:

与我使用的方法非常相似,但我建议源不是 IEnumerable。例如,如果 source 是 LINQ 查询的结果,则 Skip/Take 将触发查询的 nbChunk 枚举。可能会变得昂贵。最好使用 IList 或 ICollection 作为源的类型。这完全避免了这个问题。【参考方案22】:

另一种方法是使用Rx Buffer operator

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

【讨论】:

恕我直言,最合适的答案。 它也很简洁。感觉这应该在 common linq 库中,所有人都在关注它并进行大量测试。【参考方案23】:

使用模块化分区:

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

【讨论】:

【参考方案24】:

只需投入我的两分钱。如果您想“存储”列表(从左到右可视化),您可以执行以下操作:

 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;
    

【讨论】:

【参考方案25】:
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;
    

【讨论】:

【参考方案26】:

问题是如何“使用 LINQ 将列表拆分为子列表”,但有时您可能希望这些子列表引用原始列表,而不是副本。这允许您从子列表中修改原始列表。在这种情况下,这可能对您有用。

public static IEnumerable<Memory<T>> RefChunkBy<T>(this T[] array, int size)

    if (size < 1 || array is null)
    
        throw new ArgumentException("chunkSize must be positive");
    

    var index = 0;
    var counter = 0;

    for (int i = 0; i < array.Length; i++)
    
        if (counter == size)
        
            yield return new Memory<T>(array, index, size);
            index = i;
            counter = 0;
        
        counter++;

        if (i + 1 == array.Length)
        
            yield return new Memory<T>(array, index, array.Length - index);
        
    

用法:

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

var c3 = RefChunkBy(src, 3);      // 1, 2, 3, 4, 5, 6;
var c4 = RefChunkBy(src, 4);      // 1, 2, 3, 4, 5, 6;

// as extension method
var c3 = src.RefChunkBy(3);      // 1, 2, 3, 4, 5, 6;
var c4 = src.RefChunkBy(4);      // 1, 2, 3, 4, 5, 6;

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

请随意改进此代码。

【讨论】:

【参考方案27】:

插入我的两分钱......

通过对要分块的源使用列表类型,我找到了另一个非常紧凑的解决方案:

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;

【讨论】:

【参考方案28】:

我选择了主要答案,并将其作为 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();


所以对于 OP,代码是

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

【讨论】:

【参考方案29】:

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

【讨论】:

【参考方案30】:

可以使用无限生成器:

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的相应方法。

【讨论】:

以上是关于使用 LINQ 将列表拆分为子列表的主要内容,如果未能解决你的问题,请参考以下文章

如何将列表中的连续元素拆分为子列表

Python 在给定的开始/结束关键字处将列表拆分为子列表

python - 如何首先根据初始列表的单个元素将列表拆分为子列表,然后在python中将列表的连续部分拆分为子列表?

根据 NSDictionary 键值将 NSArray 拆分为子数组

使用 LINQ 拆分列表中的重复项

C#将文本拆分为子字符串