C# Yield关键字方法 C#如何使用yield return实现迭代器

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

yield return 是什么,它不是返回值而是挂起点

yield return
不是普通函数的
return
,它不会终止方法执行,而是暂停当前迭代器状态,把值“交出去”,等下一次调用
MoveNext()
时从暂停处继续。这意味着方法体实际被编译成一个实现了
IEnumerable<t></t>
IEnumerator<t></t>
的状态机类,你写的代码只是语法糖。

常见误解是把它当“逐个 return”,结果在循环里写了

return
混用,导致后续
yield return
永远不执行 —— 这种写法直接报错或逻辑中断。

方法返回类型必须是
IEnumerable<t></t>
IEnumerable
IAsyncEnumerable<t></t>
(C# 8+)之一
方法体内不能有普通
return
语句(除了
return;
用于提前退出迭代)
不能在
try
块中有
yield return
,除非
catch
finally
中没有
yield return
(编译器限制)

怎么写一个基础 yield return 迭代器方法

最典型场景:把一个计算过程或数据流封装成可枚举对象,避免一次性加载全部数据到内存。

public static IEnumerable<int> GetEvenNumbers(int max)
{
    for (int i = 0; i <= max; i += 2)
    {
        yield return i;
    }
}

调用时:

foreach (int n in GetEvenNumbers(10))
{
    Console.WriteLine(n); // 输出 0, 2, 4, 6, 8, 10
}

注意:

GetEvenNumbers(10)
调用本身不执行循环,只返回一个未启动的迭代器;真正执行从
foreach
第一次调用
MoveNext()
开始。

每次
yield return
后,方法暂停,局部变量(如
i
)状态被保留
如果想中途退出,用
yield break;
,它相当于“迭代结束”,不是异常
不要在
yield return
后写任何代码(除非是
yield break;
或空语句),编译器会报错

yield return 和 List.Add 的性能与内存差异

对比两种实现方式:

// ❌ 先构造完整列表再返回
public static List<int> GetEvenNumbersList(int max)
{
    var list = new List<int>();
    for (int i = 0; i <= max; i += 2)
    {
        list.Add(i);
    }
    return list;
}
// ✅ yield return 流式生成
public static IEnumerable<int> GetEvenNumbers(int max)
{
    for (int i = 0; i <= max; i += 2)
    {
        yield return i;
    }
}

关键区别:

List<t></t>
版本必须分配足够内存容纳所有元素(比如
max = 1000000
就要存 50 万整数),且全部算完才返回
yield return
版本按需计算,内存占用恒定(仅保存当前状态),适合大数据流、IO 边界(如逐行读文件)、或消费者可能提前退出的场景(如
.FirstOrDefault()
但无法重复遍历(除非显式调用
.ToList()
),因为每次调用都新建迭代器

容易踩的坑:闭包捕获和延迟执行陷阱

下面这段代码很常见,但结果不符合直觉:

public static IEnumerable<Func<int>> GetDelegates()
{
    var actions = new List<Func<int>>();
    for (int i = 0; i < 3; i++)
    {
        yield return () => i; // ❌ 所有委托都返回 3
    }
}

原因:所有

yield return
返回的 lambda 共享同一个变量
i
,而迭代器直到遍历时才执行,此时循环早已结束,
i == 3

修复方式:在循环内创建局部副本:

for (int i = 0; i < 3; i++)
{
    int localI = i; // ✅ 每次迭代独立副本
    yield return () => localI;
}

另一个坑是误以为

yield return
方法“立即执行”——它其实完全惰性。如果你在
yield return
方法里打开文件、数据库连接或 HTTP 请求,这些资源会在第一次
MoveNext()
时才初始化,且若没正确释放(比如没用
using
包裹),容易造成资源泄漏或并发问题。

相关推荐