如何在C#中生成枚举器的枚举器?.NET学习者技术问询
Hey there! 作为常年跟.NET打交道的开发者,我太懂从Python转过来时对迭代器实现的困惑了——两种语言的迭代逻辑确实有不少差异。针对你提到的分段迭代需求,我来拆解下C#里的标准解法,顺便解决你担心的实例过多问题。
先解决你最关心的:避免大量Segment实例的方案
你担心用new Segment会在大数据量下产生太多实例,其实完全可以通过延迟迭代的方式绕开这个问题,不需要提前创建任何分段对象,而是在迭代过程中动态生成每一段的元素。
这里有个通用的扩展方法实现,适用于所有IEnumerable<T>类型:
public static IEnumerable<IEnumerable<T>> Slice<T>(this IEnumerable<T> source, int sliceSize) { if (source == null) throw new ArgumentNullException(nameof(source)); if (sliceSize <= 0) throw new ArgumentOutOfRangeException(nameof(sliceSize)); using var enumerator = source.GetEnumerator(); while (enumerator.MoveNext()) { yield return GetNextSlice(enumerator, sliceSize); } } private static IEnumerable<T> GetNextSlice<T>(IEnumerator<T> enumerator, int sliceSize) { int currentCount = 0; do { yield return enumerator.Current; currentCount++; } while (currentCount < sliceSize && enumerator.MoveNext()); }
这个实现的核心是延迟执行:
- 不会一次性把整个序列拆分成所有分段,而是每次迭代到下一段时才生成对应的元素;
- 不需要任何自定义的
Segment类,完全靠枚举器的状态控制分段,从根源上避免了大量实例的产生。
回应你的两个核心诉求
分段大小与最后一段截断:
上面的代码会自动处理最后一段的截断——当枚举器走到序列末尾时,即使没凑够sliceSize个元素,也会停止并返回当前已有的元素。如果需要明确获取每段的大小,可以稍微修改返回类型,比如返回IEnumerable<(int SliceSize, IEnumerable<T> Elements)>,这样既不用额外类,又能拿到分段信息:public static IEnumerable<(int SliceSize, IEnumerable<T> Elements)> SliceWithSize<T>(this IEnumerable<T> source, int sliceSize) { using var enumerator = source.GetEnumerator(); while (enumerator.MoveNext()) { var slice = GetNextSlice(enumerator, sliceSize).ToList(); yield return (slice.Count, slice.AsEnumerable()); } }能否直接yield枚举器:
在C#里你不能直接yield return一个IEnumerator<T>,但可以yield return一个IEnumerable<T>(就像上面的GetNextSlice方法)。因为GetNextSlice本身就是一个延迟执行的迭代器,当外层迭代器遍历到它时,才会开始枚举里面的元素,完全符合你想要的迭代器行为。
针对有索引的集合的优化方案
如果你的输入是数组、List<T>这种支持索引访问的集合,还有更轻量的选择——用ArraySegment<T>(值类型,堆上无额外开销):
public static IEnumerable<ArraySegment<T>> Slice<T>(this T[] source, int sliceSize) { if (source == null) throw new ArgumentNullException(nameof(source)); if (sliceSize <= 0) throw new ArgumentOutOfRangeException(nameof(sliceSize)); for (int i = 0; i < source.Length; i += sliceSize) { int currentSliceSize = Math.Min(sliceSize, source.Length - i); yield return new ArraySegment<T>(source, i, currentSliceSize); } }
ArraySegment<T>只是对原数组的一个“视图”,不会复制元素,性能非常好,就算处理超大数组也不用担心实例开销。
总结一下最优实践
- 对于通用
IEnumerable<T>:优先用第一个延迟迭代的方案,无额外实例,全延迟执行; - 对于数组/
List<T>:用ArraySegment<T>,轻量高效,还能直接拿到分段的索引和大小; - 除非有特殊业务逻辑需要自定义分段属性,否则尽量避免自己写
Segment类——C#已经提供了足够好用的工具。
内容的提问来源于stack exchange,提问作者RandomB

