导读:本期聚焦于俊华创作的《C#中如何自定义集合的遍历行为?迭代器模式实现方法详解》,敬请观看详情。迭代器模式在C#里并非一个孤立的设计模式,它直接对应着编译器对yield return关键字和IEnumerable接口的底层转换。当foreach循环遍历一个集合时,实际调用的是对象的GetEnumerator方法,拿到一个带有Current属性和MoveNext方法的状态机。C#编译器会把包含yield return的方法重写为一个实现了IEnumerator接口的嵌套类,用state字段记录执行到哪个yield位置,从而在多次MoveNext调用之间保留局部变量状态。理解了这一层机制,自定义集合的遍历行为就不再局限于顺序访问,可以轻松加入过滤、倒序、延迟加载等逻辑。本文从IEnumerable和IEnumerator接口出发,给出手动实现和yield简化的对比,再演示如何为一个业务集合增加按条件筛选、分页遍历等自定义行为,同时说明延迟执行和资源释放需要注意的细节。

foreach 循环在 C# 中能够遍历数组、列表、字典以及各种自定义类型,背后依赖的并不是什么神秘语法,而是两个接口:IEnumerable 和 IEnumerator。当编译器遇到 foreach 时,它会尝试在目标对象上调用 GetEnumerator 方法,拿到一个迭代器,然后反复调用 MoveNext 读取 Current。换句话说,只要一个类型能够提供符合约定的迭代器,foreach 就能正常工作。迭代器模式要解决的核心问题,就是把遍历逻辑从集合本身中剥离出来,让同一个集合可以支持多种遍历策略,而调用方依然只需要写一段 foreach 代码。

C#中如何自定义集合的遍历行为?迭代器模式实现方法详解

一、IEnumerable 与 IEnumerator:遍历的底层契约

在 C# 中,一个类型能否被 foreach 遍历,取决于它是否实现了 IEnumerable 或 IEnumerable<T>。泛型版本和非泛型版本的核心思路一致,泛型版本多了强类型支持,避免装箱拆箱。实现 IEnumerable<T> 时,编译器要求提供两个 GetEnumerator 方法:一个公开的泛型版本,一个显式接口实现的非泛型版本。非泛型版本通常直接调用泛型版本,保持行为一致。

IEnumerator<T> 则是迭代器本身,它负责记录当前遍历的位置。它有三个核心成员:Current 属性返回当前位置的元素,MoveNext 方法把游标向前移动并返回是否还有元素,Reset 方法把游标重置到初始状态。实际开发中 Reset 很少被调用,很多集合甚至会直接抛出 NotSupportedException。手动实现一套完整的迭代器需要额外管理游标状态,对于包含多个字段、嵌套结构的集合来说,代码量会迅速膨胀。

下面是一个手动实现迭代器的简单例子。虽然功能正常,但可以看到状态维护全部压在开发者身上,稍微复杂一点的遍历逻辑就容易出错。

public class SimpleCollection : IEnumerable<int>
{
    private int[] _items;

    public SimpleCollection(int[] items)
    {
        _items = items;
    }

    public IEnumerator<int> GetEnumerator()
    {
        return new SimpleEnumerator(_items);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    private class SimpleEnumerator : IEnumerator<int>
    {
        private int[] _items;
        private int _position = -1;

        public SimpleEnumerator(int[] items)
        {
            _items = items;
        }

        public int Current
        {
            get
            {
                if (_position < 0 || _position >= _items.Length)
                    throw new InvalidOperationException();
                return _items[_position];
            }
        }

        object IEnumerator.Current => Current;

        public bool MoveNext()
        {
            _position++;
            return _position < _items.Length;
        }

        public void Reset()
        {
            _position = -1;
        }

        public void Dispose() { }
    }
}

这段代码中,SimpleEnumerator 是一个嵌套类,专门负责保存 _position 游标,并在 MoveNext 中判断是否越界。它的逻辑已经很直白,但如果要支持反向遍历、跳过某些元素、或者根据条件过滤,就必须在 MoveNext 和 Current 里加入更多判断,同时还要考虑多线程下状态隔离。这种手写方式在简单场景可以接受,在复杂场景下就变成负担。

二、yield return 如何把状态机交给编译器

从 C# 2.0 开始引入的 yield return 关键字,让迭代器的实现发生了根本变化。你只需要在一个返回 IEnumerable<T> 或 IEnumerator<T> 的方法里逐个 yield return 元素,编译器就会自动生成一个完整的状态机类。这个类实现了 IEnumerator<T> 接口,并用一个整数状态字段记录当前执行到哪个 yield return 位置。每次 MoveNext 被调用时,状态机从上次暂停的位置继续执行,局部变量的值也会保留在生成类的字段中。

这一点是理解迭代器模式的关键。表面上你写的是一个普通方法,实际上它会被改写成类似前面手动实现的那套结构,只是状态管理由编译器完成。正因如此,yield return 不能出现在带有 ref 或 out 参数的方法中,也不能出现在匿名方法或不安全代码块里,因为生成的状态机无法安全地保存这些上下文。

用 yield return 改写前面的集合,代码会非常简洁:

public class SimpleCollection : IEnumerable<int>
{
    private int[] _items;

    public SimpleCollection(int[] items)
    {
        _items = items;
    }

    public IEnumerator<int> GetEnumerator()
    {
        for (int i = 0; i < _items.Length; i++)
        {
            yield return _items[i];
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

这里看不到任何游标字段、边界检查和 Dispose 实现,因为编译器已经替我们生成了这些代码。更重要的是,当方法包含多个 yield return 时,状态机可以保证每次 MoveNext 只执行到一个 yield 就暂停,后续调用继续往后执行。这种能力让编写复杂的遍历逻辑变得非常自然,比如先输出头部元素,再输出主体,最后输出尾部。

还需要注意 yield break 的作用。它可以在遍历过程中提前终止迭代,相当于生成的状态机直接进入结束状态。例如在找到满足条件的元素后停止后续判断,就可以使用 yield break,避免不必要的循环。

三、自定义集合遍历行为的典型场景

自定义集合的遍历行为,最常见的需求是让集合提供多个不同维度的遍历方法,而调用方仍然可以继续使用 foreach。例如一个订单集合,默认遍历返回全部订单,但业务上可能还需要只遍历有效订单、按页返回订单、或者按金额区间筛选。如果把所有过滤逻辑都塞进 GetEnumerator,显然不够灵活,而且无法同时满足多种需求。

合理的做法是保留默认遍历为返回全部元素,另外提供返回 IEnumerable<T> 的公开方法,内部使用 yield return 构建独立的遍历序列。下面的 OrderCollection 演示了这种模式:

public class Order
{
    public int Id { get; set; }
    public string Customer { get; set; }
    public bool IsActive { get; set; }
    public decimal Amount { get; set; }
}

public class OrderCollection : IEnumerable<Order>
{
    private List<Order> _orders = new List<Order>();

    public void Add(Order order)
    {
        _orders.Add(order);
    }

    public IEnumerator<Order> GetEnumerator()
    {
        foreach (var order in _orders)
        {
            yield return order;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public IEnumerable<Order> GetActiveOrders()
    {
        foreach (var order in _orders)
        {
            if (order.IsActive)
            {
                yield return order;
            }
        }
    }

    public IEnumerable<Order> GetPagedOrders(int pageIndex, int pageSize)
    {
        int start = (pageIndex - 1) * pageSize;
        int end = Math.Min(start + pageSize, _orders.Count);
        for (int i = start; i < end; i++)
        {
            yield return _orders[i];
        }
    }
}

GetActiveOrders 在遍历时逐项判断 IsActive,只返回满足条件的订单。GetPagedOrders 则根据页码和每页数量计算起始索引与结束索引,再按区间返回。这两个方法的返回值都是 IEnumerable<Order>,调用方可以直接写 foreach (var order in orders.GetActiveOrders()),并不需要关心内部如何筛选或分页。

这种设计还有一个好处:它与 LINQ 的延迟执行理念一致。遍历方法只是构建了一个查询序列,并不会立刻执行循环体。只有真正开始 foreach 或调用 ToList、Count 等操作时,才会触发迭代。因此,如果集合在遍历前被修改,最终结果会反映修改后的数据;如果在遍历过程中修改集合,则可能触发版本检测异常。

如果需要支持不同排序方式,也可以添加类似 GetOrdersSortedByAmount 的方法,但注意不要在 yield return 之前对底层列表直接排序,除非你能接受修改原始集合。更稳妥的做法是复制一份列表再排序,或者使用 LINQ 的 OrderBy 方法生成新的序列。

四、延迟执行与资源释放的注意事项

理解延迟执行对使用迭代器至关重要。一个包含 yield return 的方法在调用时并不会立即执行方法体,而是返回一个可迭代对象。只有当你开始枚举这个对象时,方法体才会从第一个 yield return 开始运行。下面的例子可以清楚地看到执行时机:

public IEnumerable<int> GenerateNumbers()
{
    Console.WriteLine("开始生成第一个数字");
    yield return 1;
    Console.WriteLine("生成第二个数字");
    yield return 2;
    Console.WriteLine("生成第三个数字");
    yield return 3;
}

如果只是执行 var numbers = GenerateNumbers();,控制台不会有任何输出。只有在 foreach (var n in numbers) 时,才会依次打印三段文字并返回数字。如果只遍历前两个元素就 break,那么第三个 Console.WriteLine 不会执行,因为状态机没有继续调用 MoveNext。

这种延迟特性在读取文件、访问数据库或调用远程接口时尤其需要留意。很多人习惯把读取逻辑写进返回 IEnumerable<T> 的方法里,认为资源已经在方法返回时处理完毕,实际上资源要等到枚举结束才会释放。正因如此,在迭代器方法内使用 try-finally 或 using 是安全释放资源的关键。下面是一个读取文件行的例子:

public IEnumerable<string> ReadLines(string path)
{
    using (var reader = new StreamReader(path))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

如果调用方只枚举了前几行就中断,using 块还是会执行 Dispose,因为编译器生成的状态机实现了 IDisposable,foreach 在结束或异常时会调用它。这也是为什么迭代器方法内部推荐使用 using 而不是手动打开资源后忘记释放。但要注意,如果迭代器对象没有被正确枚举,比如只拿到 IEnumerable<T> 而没有遍历,那么 Dispose 不会触发,资源也就不会释放。

另外,在 foreach 遍历期间修改底层集合会引发 InvalidOperationException。这是因为集合内部通常维护一个版本号,每次添加或删除元素都会递增。迭代器在 MoveNext 时会校验版本号是否与创建时一致,不一致就报错。这个机制能避免遍历到脏数据,但也限制了某些场景下的操作。如果确实需要在遍历时修改集合,可以先 ToList 复制一份再遍历副本,或者使用 for 循环倒序删除。

回到迭代器模式本身,C# 通过 IEnumerable、IEnumerator 和 yield return 提供了一套非常成熟的实现路径。自定义集合的遍历行为,本质上就是为不同的业务需求编写独立的迭代方法,让每个方法返回一个经过筛选、排序或分页处理的序列。掌握这套机制后,你可以在不改变集合存储结构的前提下,灵活扩展遍历方式,同时借助延迟执行减少不必要的计算和资源占用。

C#迭代器模式自定义集合遍历yield return修改时间:2026-09-18 03:46:58

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0918/58660.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。