You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何无临时存储将单向遍历的有状态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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 22:50:28