如何无临时存储将单向遍历的有状态IEnumerable<T>转为IEnumerable<IEnumerable<T>>
流式预排序IEnumerable的内存友好分组实现
场景与需求
处理流式IEnumerable数据(示例为元组,实际场景是从DataReader流式读取DataRecords),数据已按左字段预排序,要求:
- 按左字段连续相同的规则分组,返回每组的
IEnumerable - 不使用临时集合存储分组数据,避免内存占用过高
- 全程仅单次遍历源数据,实现单向流式处理
原尝试用TakeWhile实现,但出现迭代器从头重启的问题,相关代码如下:
测试数据
var tuples = new(int, int)[] { (0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (2, 0), (2, 1), (2, 2), };
错误实现代码
private int currentGroup; public IEnumerator<IEnumerable<Tuple<int, int>>> GetEnumerator() { var tg = TupleGenerator(); foreach (Tuple<int, int> item in tg) { currentGroup = item.Item1; yield return tg.TakeWhile((x) => x.Item1 == currentGroup); } } static IEnumerable<Tuple<int, int>> TupleGenerator() { for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { yield return new Tuple<int, int>(i,j); } } }
问题根源
IEnumerable每次遍历(包括foreach和TakeWhile)都会创建独立的枚举器,原代码中TupleGenerator()返回的序列被多次遍历,导致TakeWhile总是从头读取数据,无法共享遍历进度。
正确实现
通过共享单个枚举器,手动控制遍历进度,实现流式分组:
扩展方法实现
public static class EnumerableExtensions { public static IEnumerable<IEnumerable<T>> GroupByContiguousKey<T, TKey>( this IEnumerable<T> source, Func<T, TKey> keySelector, IEqualityComparer<TKey> comparer = null) { comparer ??= EqualityComparer<TKey>.Default; using var enumerator = source.GetEnumerator(); if (!enumerator.MoveNext()) yield break; T currentItem = enumerator.Current; TKey currentKey = keySelector(currentItem); while (true) { yield return YieldContiguousGroup(enumerator, ref currentItem, ref currentKey, keySelector, comparer); if (!enumerator.MoveNext()) yield break; currentItem = enumerator.Current; currentKey = keySelector(currentItem); } } private static IEnumerable<T> YieldContiguousGroup<T, TKey>( IEnumerator<T> enumerator, ref T currentItem, ref TKey currentKey, Func<T, TKey> keySelector, IEqualityComparer<TKey> comparer) { // 返回当前分组的第一个元素 yield return currentItem; while (enumerator.MoveNext()) { T nextItem = enumerator.Current; TKey nextKey = keySelector(nextItem); if (comparer.Equals(currentKey, nextKey)) { yield return nextItem; } else { // 保存下一组的起始元素与键,终止当前分组 currentItem = nextItem; currentKey = nextKey; yield break; } } } }
使用示例
// 流式数据生成器(模拟DataReader流式读取) static IEnumerable<(int, int)> TupleGenerator() { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { yield return (i, j); } } } // 调用分组方法 foreach (var group in TupleGenerator().GroupByContiguousKey(item => item.Item1)) { Console.WriteLine($"分组键: {group.First().Item1}"); foreach (var item in group) { Console.WriteLine($" {item}"); } }
特性说明
- 仅适用于已按分组键预排序的数据源,基于连续相同键进行分组
- 全程共享单个枚举器,仅单次遍历源数据,内存占用极低
- 分组返回的
IEnumerable为延迟加载,仅在遍历分组时才会继续读取源数据
内容的提问来源于stack exchange,提问作者amonroejj
相关产品推荐
相关产品推荐

