linq 如何对可枚举对象进行分块?

snvhrwxg  于 2022-12-06  发布在  其他
关注(0)|答案(6)|浏览(109)

我需要一个优雅的方法,它接受一个可枚举对象,并获取可枚举对象的可枚举对象,其中每个可枚举对象包含相同数量的元素,但最后一个元素除外:

public static IEnumerable<IEnumerable<TValue>> Chunk<TValue>(this IEnumerable<TValue> values, Int32 chunkSize)
{
    // TODO: code that chunks
}

这就是我所尝试的:

public static IEnumerable<IEnumerable<TValue>> Chunk<TValue>(this IEnumerable<TValue> values, Int32 chunkSize)
    {
        var count = values.Count();
        var numberOfFullChunks = count / chunkSize;
        var lastChunkSize = count % chunkSize;
        for (var chunkIndex = 0; chunkSize < numberOfFullChunks; chunkSize++)
        {
            yield return values.Skip(chunkSize * chunkIndex).Take(chunkSize);
        }
        if (lastChunkSize > 0)
        {
            yield return values.Skip(chunkSize * count).Take(lastChunkSize);
        }
    }

UPDATE刚发现有一个关于拆分列表的类似主题Split List into Sublists with LINQ

0yg35tkg

0yg35tkg1#

如果内存消耗不是问题,那么像这样?

static class Ex
{
    public static IEnumerable<IEnumerable<TValue>> Chunk<TValue>(
        this IEnumerable<TValue> values, 
        int chunkSize)
    {
        return values
               .Select((v, i) => new {v, groupIndex = i / chunkSize})
               .GroupBy(x => x.groupIndex)
               .Select(g => g.Select(x => x.v));
    }
}

否则,您可以使用yield关键字来发挥创意,如下所示:

static class Ex
{
    public static IEnumerable<IEnumerable<TValue>> Chunk<TValue>(
                    this IEnumerable<TValue> values, 
                    int chunkSize)
    {
        using(var enumerator = values.GetEnumerator())
        {
            while(enumerator.MoveNext())
            {
                yield return GetChunk(enumerator, chunkSize).ToList();
            }
        }
    }

    private static IEnumerable<T> GetChunk<T>(
                     IEnumerator<T> enumerator,
                     int chunkSize)
    {
        do
        {
            yield return enumerator.Current;
        } while(--chunkSize > 0 && enumerator.MoveNext());
    }
}
bq8i3lrv

bq8i3lrv2#

〉= .Net 6

内置Enumerable.Chunk方法:

// Giving an enumerable
var e = Enumerable.Range(1, 999);

// Here it is. Enjoy :)
var chunks = e.Chunk(29);

// Sample, iterating over chunks
foreach(var chunk in chunks) // for each chunk
{
    foreach(var item in chunk) // for each item in a chunk
    {
        Console.WriteLine(item);
    }
}
muk1a3rh

muk1a3rh3#

下面是使用TakeSkip的扩展方法:

public static IList<IList<T>> Chunk<T>(this IList<T> source, int chunksize)
{
    while (source.Any())
    {
        yield return source.Take(chunksize);
        source = source.Skip(chunksize);
    }
}

(更新为使用IList而不是IEnumerable

x9ybnkn6

x9ybnkn64#

如果你没有.net 6,你可能会选择把the Chunk method from it补丁到你的项目中,你可能需要做的唯一的调整是与.net源代码使用的异常助手有关,因为你自己的项目可能不会有ThrowHelper
他们的代号:

ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);

可能更像是:

throw new ArgumentNullException(nameof(source));

以下代码块已应用了这些调整;您可以创建一个名为Chunk.cs的新文件并将以下代码放入其中:

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;

namespace System.Linq
{
    public static partial class Enumerable
    {
        /// <summary>
        /// Split the elements of a sequence into chunks of size at most <paramref name="size"/>.
        /// </summary>
        /// <remarks>
        /// Every chunk except the last will be of size <paramref name="size"/>.
        /// The last chunk will contain the remaining elements and may be of a smaller size.
        /// </remarks>
        /// <param name="source">
        /// An <see cref="IEnumerable{T}"/> whose elements to chunk.
        /// </param>
        /// <param name="size">
        /// Maximum size of each chunk.
        /// </param>
        /// <typeparam name="TSource">
        /// The type of the elements of source.
        /// </typeparam>
        /// <returns>
        /// An <see cref="IEnumerable{T}"/> that contains the elements the input sequence split into chunks of size <paramref name="size"/>.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="source"/> is null.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="size"/> is below 1.
        /// </exception>
        public static IEnumerable<TSource[]> Chunk<TSource>(this IEnumerable<TSource> source, int size)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            if (size < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(size));
            }

            return ChunkIterator(source, size);
        }

        private static IEnumerable<TSource[]> ChunkIterator<TSource>(IEnumerable<TSource> source, int size)
        {
            using IEnumerator<TSource> e = source.GetEnumerator();
            while (e.MoveNext())
            {
                TSource[] chunk = new TSource[size];
                chunk[0] = e.Current;

                int i = 1;
                for (; i < chunk.Length && e.MoveNext(); i++)
                {
                    chunk[i] = e.Current;
                }

                if (i == chunk.Length)
                {
                    yield return chunk;
                }
                else
                {
                    Array.Resize(ref chunk, i);
                    yield return chunk;
                    yield break;
                }
            }
        }
    }
}

您应该验证将他们的MIT许可代码合并到您的项目中不会过度影响您自己的许可意图

hs1rzwqc

hs1rzwqc5#

仅进行了一些快速测试,但这似乎有效:

public static IEnumerable<IEnumerable<TValue>> Chunk<TValue>(this IEnumerable<TValue> values, Int32 chunkSize)
{
    var valuesList = values.ToList();
    var count = valuesList.Count();        
    for (var i = 0; i < (count / chunkSize) + (count % chunkSize == 0 ? 0 : 1); i++)
    {
        yield return valuesList.Skip(i * chunkSize).Take(chunkSize);
    }
}
deikduxw

deikduxw6#

正如其他答案已经指出的那样,从.NET 6开始,存在Enumerable.Chunk扩展方法。
不幸的是(在我看来),该方法返回IEnumerable<T[]>,这削弱了一次处理一个IEnumerable<T>元素所带来的节省内存的好处:

public IEnumerable<HugeObject> CreateHugeObjects(int count) {
  for (var i = 0; i < count; ++i) {
    yield return new HugeObject(i);
  }
}

public static int AggregateSomehow(IEnumerable<HugeObject> chunk) {
  return 0;
}

public void Consume() {
  var source = CreateHugeObjects(1000);
  var chunks = source.Chunk(100);
  var result = chunks.Select(AggregateSomehow);
}

在这个范例中,AggregateSomehowchunk的基础型别将会是HugeObject[100],表示必须同时将HugeObject的100个执行严修载入内存,才能执行方法呼叫。
Enumerable.Chunk可用之前,我曾经编写自己的扩展名为Partition,如下所示:

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size) {
  if (source == null) {
    throw new ArgumentNullException(nameof(source));
  }

  if (size < 1) {
    throw new ArgumentOutOfRangeException(nameof(size));
  }

  using var e = source.GetEnumerator();
  while (e.MoveNext()) {
    yield return new Partitioner<T>(e, size);
  }
}

private class Partitioner<T> : IEnumerable<T>
{
  private class PartitionerEnumerator : IEnumerator<T>
  {

    private readonly IEnumerator<T> m_Source;
    private readonly int m_Size;

    private int m_Index = -1;
    private bool m_Disposed = false;

    public PartitionerEnumerator(IEnumerator<T> source, int size) {
      m_Source = source;
      m_Size = size;
    }

    public T Current => m_Source.Current;

    object IEnumerator.Current => Current;

    public void Dispose() {
      if (!m_Disposed) {
        m_Disposed = true;
        while (++m_Index < m_Size && m_Source.MoveNext()) { }
      }
    }

    public bool MoveNext() {
      if (m_Index == -1) {
        ++m_Index;
        return true;
      } else {
        return ++m_Index < m_Size && m_Source.MoveNext();
      }
    }

    public void Reset() => throw new NotImplementedException();
  }

  private readonly PartitionerEnumerator m_Enumerator;

  public Partitioner(IEnumerator<T> source, int size) {
    m_Enumerator = new PartitionerEnumerator(source, size);
  }

  public IEnumerator<T> GetEnumerator() => m_Enumerator;
  
  IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

这种方法考虑到三个因素:
1.原始source只枚举一次(Skip/Take实现经常会遗漏)
1.在一般的巢状/链接LINQ运算式中,内存中一次只能有一个元素(目前的实作会忽略这个元素)
1.当任何分区只处理了一部分,然后过早地被释放时,PartitionerEnumerator.Dispose方法确保底层枚举数仍然转发计数的剩余部分(嵌套循环方法经常会错过这一点:)

public static IEnumerable<IEnumerable<T>> PartitionWrong<T>(this IEnumerable<T> source, int size) {
  if (source == null) {
    throw new ArgumentNullException(nameof(source));
  }

  if (size < 1) {
    throw new ArgumentOutOfRangeException(nameof(size));
  }

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

  using (var e = source.GetEnumerator()) {
    while (e.MoveNext()) {
      yield return EnumeratePartition(e, size);
    }
  }
}

如果所有的子序列都被完全枚举,例如通过在它们上调用CountSum,这种方法将工作,但是对于部分枚举,例如在它们上调用First,它将失败:

var source = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
source.PartitionWrong(3).Select(c => c.Count()); // 3, 3, 3
source.PartitionWrong(3).Select(c => c.Sum()); // 6, 15, 24
source.PartitionWrong(3).Select(c => c.First()); // 1, 2, 3, 4, 5, 6, 7, 8, 9 but should be 1, 4, 7

我的实现可以满足上述所有要求,但仍有几个缺点,这些缺点与我的应用程序无关,但前两个缺点可能就是.NET团队选择“简单方法”并使用立即填充的数组的原因:
1.如果你先保存所有的Partition对象,然后循环处理它们,一次一个元素,原始IEnumerable的顺序在分区中不被保留,也就是说,第一个分区不保证包含前三个元素。作为一个副作用,如果元素的数量没有均匀地划分到分区大小中,哪个分区比size短是“随机的”。甚至不一定保证只有一个分区更短。
1.在并行环境中使用它会遇到与(1)相同的问题,但TBH我甚至从来没有考虑过我的代码的线程安全性。
1.过早枚举中止的好处(如在子序列上调用AnyAll)将不会阻止创建当前枚举的Partion的其余元素(尽管对于Chunk显然也是如此,其中所有元素在进入块时被创建)
因此,简而言之,如果您不打算使用并行化或不依赖于有序处理,并且在使用.NET 6的Chunk时遇到内存问题,我的旧代码可能会对您有所帮助。

相关问题