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

来源:这里教程网 时间:2026-02-21 17:42:31 作者:

为什么
GetEnumerator()
返回的必须是
IEnumerator
IEnumerable

因为 C# 的

foreach
语句在编译期会查找类型是否公开实现了
GetEnumerator()
方法,且该方法返回类型需满足“有
MoveNext()
Current
Reset()
(可选)”这一契约。编译器不关心你是否真继承自
IEnumerator
,但必须能静态解析出这些成员——所以直接返回匿名类型或普通类会失败。

常见错误现象:

foreach
报错 “
foreach
statement cannot operate on variables of type ‘X’ because ‘X’ does not contain a public instance definition for ‘GetEnumerator’”。

最稳妥做法:让集合类实现
IEnumerable<t></t>
,并返回一个实现
IEnumerator<t></t>
的类型(可以是内部类、本地函数
yield return
,或结构体)
若只实现非泛型
IEnumerable
,则
Current
object
,会触发装箱,性能差且无类型安全
.NET 6+ 支持
ref struct
迭代器(如
Span<t>.GetEnumerator()</t>
),但不能跨方法返回,只能用于栈上遍历场景

yield return
和手动写
IEnumerator
类,选哪个?

yield return
是编译器帮你生成状态机类,代码简洁、不易出错;手动实现则完全可控,适合需要复用状态、提前终止、或嵌套遍历逻辑的场景。

使用场景举例:你要实现一个树结构的“按层遍历”迭代器,每次

MoveNext()
都要维护队列,这时手写类更清晰;但若只是对数组做筛选后遍历,
yield return
一行
if (x.IsValid) yield return x;
就够了。

yield return
生成的类是
IEnumerator<t></t>
,但无法被继承或扩展,调试时看到的是编译器生成的
<mymethod>d__5</mymethod>
类名
手动实现时,别忘了在
Dispose()
中释放资源(比如打开的文件流、数据库连接),而
yield
生成的迭代器默认只支持空
Dispose()
性能上,
yield
有少量状态机开销(字段读写 + 分支跳转),但对绝大多数业务逻辑可忽略;手动实现可零分配(用
struct
实现
IEnumerator<t></t>
),但要注意不能捕获局部引用类型变量

如何让自定义集合支持
foreach
同时还支持 LINQ 查询?

关键不是“额外加什么”,而是别破坏已有契约:只要你的集合实现了

IEnumerable<t></t>
,所有标准 LINQ 扩展方法(
Where
Select
OrderBy
等)就自动可用——因为它们参数类型就是
IEnumerable<t></t>

容易踩的坑:

别在
GetEnumerator()
里每次都新建一个集合副本(比如
return list.ToList().GetEnumerator()
),这会导致每次
foreach
都复制数据,内存和性能双崩
如果集合本身是只读或线程敏感的,应在迭代器中做快照(如用
ToArray()
)或加锁,否则可能抛出
InvalidOperationException
:“Collection was modified”
想支持
Count
属性或
ElementAt
等随机访问操作?那就得额外实现
ICollection<t></t>
或提供自己的索引器,LINQ 不会自动优化这些

结构体迭代器(
struct
实现
IEnumerator<t></t>
)有什么限制?

结构体迭代器能避免堆分配,提升高频遍历场景的性能,但它要求所有状态都存于值类型字段中,且不能引用外部局部变量(即不能闭包)。

典型错误:在结构体迭代器里捕获方法参数或局部变量,编译器会报错 “variable is referenced from scope but not defined in scope”。

必须把所有依赖的数据(如源集合引用、当前索引、临时缓冲区)作为
struct
字段显式声明
结构体迭代器不能实现
IDisposable
(C# 规定
struct
不能实现该接口),所以无法用
using
语法确保清理;如有资源需释放,只能靠调用方显式调用
Dispose()
(但没人会这么干)
它天然不支持
yield return
—— 因为
yield
必须生成类,而结构体无法被编译器重写为状态机

真正需要结构体迭代器的场景极少,除非你在写高性能基础库(如

Memory<t></t>
或游戏引擎容器),否则优先用
yield return
或类实现。

相关推荐