你可以使用多个查询来使用Take
和Skip
,但我认为这会在原始列表上增加太多迭代。
相反,我认为你应该创建自己的迭代器,如下所示:
public static IEnumerable<IEnumerable<T>> GetEnumerableOfEnumerables<T>(
IEnumerable<T> enumerable, int groupSize)
{
List<T> list = new List<T>(groupSize);
foreach (T item in enumerable)
{
list.Add(item);
if (list.Count == groupSize)
{
yield return list;
list = new List<T>(groupSize);
}
}
if (list.Count != 0)
{
yield return list;
}
}
然后可以调用它,它支持LINQ,因此您可以对生成的序列执行其他操作。
根据Sam的回答,我觉得有一种更简单的方法可以做到这一点,而不需要:
- 再次遍历列表(我最初没有这样做)
- 在释放块之前将项目分组材料化(对于大量项目的块,会出现内存问题)
- Sam发布的所有代码
话虽如此,这里是另一个版本,我已经将其编码为扩展方法,称为Chunk
,适用于IEnumerable<T>
:
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source,
int chunkSize)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (chunkSize <= 0) throw new ArgumentOutOfRangeException(nameof(chunkSize),
"The chunkSize parameter must be a positive value.");
return source.ChunkInternal(chunkSize);
}
上面没有什么意外的,只是基本的错误检查。
接下来是ChunkInternal
:
private static IEnumerable<IEnumerable<T>> ChunkInternal<T>(
this IEnumerable<T> source, int chunkSize)
{
Debug.Assert(source != null);
Debug.Assert(chunkSize > 0);
using (IEnumerator<T> enumerator = source.GetEnumerator())
do
{
if (!enumerator.MoveNext()) yield break;
yield return ChunkSequence(enumerator, chunkSize);
} while (true);
}
基本上,它获取
IEnumerator<T>
并手动迭代每个项。 它检查当前是否有要枚举的项。 在枚举完每个块后,如果没有剩余项,它就会中断。
一旦检测到序列中有项,它就将内部
IEnumerable<T>
实现的责任委托给
ChunkSequence
:
private static IEnumerable<T> ChunkSequence<T>(IEnumerator<T> enumerator,
int chunkSize)
{
Debug.Assert(enumerator != null);
Debug.Assert(chunkSize > 0);
int count = 0;
do
{
yield return enumerator.Current;
} while (++count < chunkSize && enumerator.MoveNext());
}
由于在传递给ChunkSequence
的IEnumerator<T>
上已经调用了MoveNext
,它会产生Current
返回的项,然后增加计数,确保不返回超过chunkSize
个项,并在每次迭代后移动到序列中的下一项(但如果产生的项数超过块大小,则短路)。
如果没有剩余项,则InternalChunk
方法将在外部循环中进行另一次遍历,但是当第二次调用MoveNext
时,它仍将根据文档返回falseas per the documentation(我强调):
如果MoveNext通过集合的末尾,则枚举器位于集合中的最后一个元素之后,并且MoveNext返回false。 当枚举器处于此位置时,对MoveNext的后续调用也将返回false,直到调用Reset为止。
此时,循环将中断,序列的序列将终止。
这是一个简单的测试:
static void Main()
{
string s = "agewpsqfxyimc";
int count = 0;
foreach (IEnumerable<char> g in s.Chunk(3))
{
Console.Write("Group: {0} - ", ++count);
foreach (char c in g)
{
Console.Write(c + ", ");
}
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一样。
[a,g,e]
。 - Colonel PanicGroupBy(x=>f(x)).First()
永远不会产生分组。虽然OP提到了列表,但如果我们编写代码以支持IEnumerable,并且仅进行一次迭代,我们就能获得更好的性能优势。 - Colonel Panic