C# 试题 29:迭代器与异步流
0561 以下代码输出什么?
难度: 实战
static IEnumerable<int> M()
{
Console.Write("X");
yield return 1;
}
var query = M();
Console.Write("Y");
Console.WriteLine(query.First());
- A.
XY1 - B.
YX1,调用 M() 只构建状态机,枚举时才执行方法体 - C.
Y1 - D.
YX
查看答案与解析
正确答案B
解析: 含 yield 的方法被编译成状态机,调用 M() 只返回枚举器、不执行任何方法体;First() 首次 MoveNext 才打印 X 并产出 1。输出为 YX1。
关键点: 迭代器方法体的执行被推迟到首次枚举。
0562 以下代码输出什么?
难度: 进阶
static IEnumerable<int> M()
{
Console.Write("X");
yield break;
}
var query = M();
Console.Write("Y");
Console.WriteLine(query.Count());
- A.
YX0,yield break 前的语句也在首次 MoveNext 时才执行 - B.
XY0 - C.
Y0 - D.
YX1
查看答案与解析
正确答案A
解析: 只要方法里存在 yield,整个方法体(包括 yield break 之前的语句)都延迟到首次 MoveNext 执行。因此 Y 先打印,随后枚举打印 X 并得到 Count=0,输出 YX0。
关键点: 延迟的是“整个方法体”,不只是 yield 之后的代码。
0563 以下代码输出什么?
难度: 实战
static IEnumerable<int> M()
{
int i = 0;
yield return i++;
yield return i++;
}
var a = M().GetEnumerator();
var b = M().GetEnumerator();
a.MoveNext(); a.MoveNext();
b.MoveNext();
Console.WriteLine($"{a.Current} {b.Current}");
- A.
1 1 - B.
0 0 - C.
1 0,每次 GetEnumerator 创建独立状态机 - D.
0 1
查看答案与解析
正确答案C
解析: 每次调用 M()/GetEnumerator 都生成独立的状态机实例,局部变量 i 互不相干:a 已推进两次(Current=1),b 只推进一次(Current=0)。
关键点: 迭代器状态属于单个枚举器,枚举器之间不共享。
0564 以下代码输出什么?
难度: 实战
static IEnumerable<int> M()
{
try { yield return 1; yield return 2; }
finally { Console.Write("F"); }
}
foreach (var x in M())
{
Console.Write(x);
break;
}
- A.
1 - B.
12F - C.
1F2 - D.
1F,break 会 Dispose 枚举器,finally 同步执行
查看答案与解析
正确答案D
解析: foreach 在提前 break 时会对枚举器调用 Dispose,迭代器状态的 finally 块随之执行。第一轮输出 1,break 触发 finally 输出 F,结果为 1F。
关键点: 迭代器的清理不依赖 GC,提前退出也会走 finally。
0565 以下代码输出什么?
难度: 进阶
static IEnumerable<int> M()
{
try { yield return 1; }
finally { Console.Write("F"); }
}
Console.WriteLine(string.Join(",", M()));
- A.
1F - B.
F1,MoveNext 返回 false(触发 finally)先于 Join 拼接结果 - C.
1 - D. 编译失败,yield 不能出现在 try/finally 中
查看答案与解析
正确答案B
解析: string.Join 完整枚举后,最后一次 MoveNext 返回 false 时会执行 finally(打印 F),之后 Join 才完成拼接并输出 1。因此控制台先出现 F 再出现 1。
关键点: finally 在枚举自然结束时执行,且发生在 Join 产出结果之前。
0566 以下代码输出什么?
难度: 进阶
static IEnumerable<int> M()
{
try { yield return 1; }
finally { Console.Write("F"); }
}
var e = M().GetEnumerator();
e.MoveNext();
Console.Write("X");
- A.
X,未 Dispose 枚举器,finally 不执行 - B.
XF - C.
FX - D.
F
查看答案与解析
正确答案A
解析: 手动 GetEnumerator 不会自动释放:没有 using/foreach,枚举器从未被 Dispose,finally 不会执行,只输出 X。未释放的迭代器会一直占着 try/finally 内的资源直到 GC 兜底。
关键点: 手动枚举必须显式 Dispose(using 或 foreach),清理不是自动的。
0567 以下代码输出什么?
难度: 实战
class Resource : IDisposable
{
public void Dispose() => Console.Write("D");
}
static IEnumerable<int> M()
{
using var r = new Resource();
yield return 1;
}
foreach (var x in M()) break;
- A. 无输出,using 资源在 GC 时才释放
- B. 输出
1D - C. 输出
D,foreach 提前 break 触发枚举器 Dispose,using 立即释放 - D. 输出
1
查看答案与解析
正确答案C
解析: 迭代器方法里的 using 会被编译进状态机的 Dispose 路径:foreach 取到第一个元素后 break,枚举器被 Dispose,r.Dispose() 同步执行输出 D。资源释放不依赖 GC。
关键点: 迭代器内 using 的生命周期 = 枚举器生命周期,提前退出也会释放。
0568 以下代码结果是什么?
难度: 实战
static IEnumerable<int> M()
{
try { yield return 1; }
catch (Exception) { }
}
- A. 正常运行
- B. 运行抛
InvalidOperationException - C. 运行抛
CompilerException - D. 编译失败(CS1626):yield 不能出现在带 catch 的 try 块中
查看答案与解析
正确答案D
解析: 编译器限制:yield return 不能出现在带 catch 子句的 try 块中(CS1626)。因为异常跨越状态机恢复点时无法可靠映射 catch 语义。try/finally 中允许 yield,try/catch 中不允许。
关键点: yield 与 try/catch 互斥,与 try/finally 兼容。
0569 以下代码结果是什么?
难度: 进阶
Func<IEnumerable<int>> f = () => { yield return 1; };
- A. 正常运行
- B. 编译失败(CS1621):yield 不能出现在 lambda 或匿名方法中
- C. 运行抛
NotSupportedException - D. 返回空序列
查看答案与解析
正确答案B
解析: yield 只能出现在普通(非匿名)方法中,lambda 与匿名方法内使用会报 CS1621。需要迭代器逻辑时只能抽成具名方法。
关键点: 别试图在 lambda 里 yield,编译器不允许。
0570 两个线程各自用 foreach 枚举同一个迭代器方法返回的实例,哪项正确?
难度: 进阶
- A. 每次 foreach 都创建独立枚举器与独立状态机,互不干扰;只有并发调用同一个枚举器的 MoveNext 才不安全
- B. 同一 IEnumerable 不允许并发枚举,会抛 InvalidOperationException
- C. 两个线程共享同一状态,输出会交错
- D. IEnumerable 是线程安全的,同一枚举器可并发 MoveNext
查看答案与解析
正确答案A
解析: 迭代器返回的 IEnumerable 每次 GetEnumerator 都新建状态机,两个 foreach 互不影响。真正不安全的做法是并发调用同一个枚举器的 MoveNext——那会破坏共享状态,属于未定义行为。
关键点: “IEnumerable 可并发枚举”与“IEnumerator 可并发推进”是两回事。
0571 以下代码输出什么?
难度: 实战
static async IAsyncEnumerable<int> M()
{
Console.Write("S");
await Task.Yield();
yield return 1;
}
var query = M();
Console.Write("E");
await foreach (var x in query) Console.Write(x);
- A.
ES - B.
SE1 - C.
ES1,async 迭代器同样延迟,调用时不执行方法体 - D.
E1S
查看答案与解析
正确答案C
解析: async 迭代器与同步迭代器一样延迟:调用 M() 只创建异步枚举器,await foreach 首次 MoveNextAsync 才执行方法体(打印 S、await、产出 1),输出 ES1。
关键点: 异步流同样惰性,调用不等于开始执行。
0572 以下代码输出什么?
难度: 进阶
static async IAsyncEnumerable<int> M()
{
try { yield return 1; yield return 2; }
finally { Console.Write("F"); }
}
await foreach (var x in M())
{
Console.Write(x);
break;
}
- A.
12F - B.
1 - C.
1F2 - D.
1F,break 触发异步 Dispose,finally 执行
查看答案与解析
正确答案D
解析: await foreach 提前 break 时会对异步枚举器调用 DisposeAsync,async 迭代器中的 finally 随之执行。第一轮输出 1,break 后输出 F,结果为 1F。
关键点: 异步流的清理同样在提前退出时发生,通过 DisposeAsync 触发。
0573 关于 await foreach (var x in seq.ConfigureAwait(false)) 与 await foreach (var x in seq) 的区别,哪项正确?
难度: 进阶
- A. ConfigureAwait(false) 让迭代器完全同步执行
- B. 前者使迭代器内部的 await 不再恢复到捕获的 SynchronizationContext 上,避免上下文切换;无同步上下文时两者无差别
- C. 两者行为完全相同,ConfigureAwait 对异步流无效
- D. ConfigureAwait(false) 会取消迭代
查看答案与解析
正确答案B
解析: 对异步流调用 ConfigureAwait(false) 影响的是迭代器体内每个 await 的恢复方式:默认恢复回捕获的 SynchronizationContext(如 UI 线程),false 则在线程池继续,避免无谓的上下文切换。控制台等无上下文场景二者等价。
关键点: ConfigureAwait 作用在迭代器的内部 await 上,不是外层循环。
0574 以下代码运行结果是什么?
难度: 进阶
using System.Runtime.CompilerServices;
static async IAsyncEnumerable<int> M([EnumeratorCancellation] CancellationToken ct = default)
{
for (int i = 0; i < 5; i++)
{
ct.ThrowIfCancellationRequested();
await Task.Delay(1, ct);
yield return i;
}
}
using var cts = new CancellationTokenSource();
cts.Cancel();
await foreach (var x in M().WithCancellation(cts.Token)) { }
Console.Write("done");
- A. 抛
OperationCanceledException,token 通过 WithCancellation 传入 [EnumeratorCancellation] 参数 - B. 正常输出 done
- C. 编译失败,WithCancellation 只能用于同步迭代器
- D. 抛
TaskCanceledException,与 OperationCanceledException 不同
查看答案与解析
正确答案A
解析: WithCancellation(cts.Token) 把 token 注入标有 [EnumeratorCancellation] 的参数,首次 MoveNextAsync 就执行 ThrowIfCancellationRequested 抛 OperationCanceledException,“done”不会打印。(TaskCanceledException 是 OCE 的子类,这里抛出的是 OCE。)
关键点: 异步流取消的标准通道是 WithCancellation + [EnumeratorCancellation]。
0575 编写“异步生成序列”的方法,正确声明是?
难度: 实战
- A.
async Task<IEnumerable<int>> M(),用 yield 返回序列 - B.
Task<IAsyncEnumerable<int>> M(),内部 await + yield - C.
async IAsyncEnumerable<int> M(),可用 await 与 yield,配合 await foreach 消费 - D.
IAsyncEnumerable<int> M()内可直接 await,无需 async
查看答案与解析
正确答案C
解析: 异步流必须声明为 async IAsyncEnumerable<T>,才能同时使用 await 与 yield。async Task<IEnumerable<T>> 里不能用 yield;Task<IAsyncEnumerable<T>> 包裹异步流是反模式。消费端用 await foreach。
关键点: 异步流的“async”与“迭代器”必须在同一个返回类型上。
0576 以下代码输出什么?
难度: 进阶
int n = 0;
IEnumerable<int> Gen()
{
while (n < 3) yield return n++;
}
var query = Gen();
Console.WriteLine(query.Count());
Console.WriteLine(query.Count());
- A.
3/3 - B.
3/ 抛异常 - C.
0/0 - D.
3/0,每次枚举重新执行方法体,但捕获的外部变量跨枚举共享
查看答案与解析
正确答案D
解析: 迭代器方法体在每次枚举时重新执行,但闭包捕获的外部变量 n 是共享的:第一次 Count() 把 n 推到 3,第二次枚举时 n < 3 已不成立,结果为 0。这正说明“重新执行”不等于“状态重置”。
关键点: 捕获外部状态的迭代器,多次枚举之间状态会延续。
0577 以下代码输出什么?
难度: 进阶
static IEnumerable<int> M()
{
int i = 0;
while (i < 3) yield return i++;
}
var query = M();
Console.WriteLine(query.Sum());
Console.WriteLine(query.Sum());
- A.
3/0 - B.
3/3,局部变量属于每次枚举新建的状态机,两次枚举互不影响 - C.
6/0 - D.
0/0
查看答案与解析
正确答案B
解析: 与 0576 对照:i 是迭代器方法内的局部变量,位于每次枚举新建的状态机中,两次 Sum() 各自从 0 开始,都得到 0+1+2=3。
关键点: 局部状态随枚举器新建而重置,外部捕获状态则共享。
0578 以下代码输出什么?
难度: 实战
static async IAsyncEnumerable<int> M()
{
try { yield return 1; }
finally { Console.Write("F"); }
}
await foreach (var x in M()) { }
Console.Write("E");
- A.
FE,完整消费结束时 finally 执行 - B.
EF - C.
E - D.
F
查看答案与解析
正确答案A
解析: 循环体为空但会完整消费:第一次 MoveNextAsync 产出 1,第二次返回 false 时 finally 执行(打印 F),循环结束后打印 E,输出 FE。异步流的 finally 在枚举自然结束时同样生效。
关键点: 完整消费结束也会触发 finally,异步同步皆如此。
0579 以下代码输出什么?
难度: 进阶
static IEnumerable<int> M(int x)
{
Console.Write(x);
yield return x;
}
int n = 1;
var query = M(n);
n = 2;
Console.WriteLine(query.First());
- A.
21 - B.
22 - C.
11,参数在调用时求值(1),方法体在枚举时执行(打印 1)并产出 1 - D.
12
查看答案与解析
正确答案C
解析: 实参在调用 M(n) 时求值,状态机记住的是 1;方法体延迟到 First() 枚举时执行,打印 1 并产出 1。n=2 的修改不影响已捕获的参数。输出 11。
关键点: 参数按调用时快照,方法体按枚举时执行。
0580 两个任务并发 await foreach 同一个 IAsyncEnumerable 实例,哪项正确?
难度: 进阶
- A. 会抛异常,异步流不支持并发
- B. 两个消费者共享同一状态机,元素均分
- C. 同一枚举器可被并发 MoveNextAsync
- D. 每次 await foreach 创建独立枚举器;方法体若只使用局部状态则互不干扰,共享外部状态才需同步
查看答案与解析
正确答案D
解析: 与同步迭代器一致,每次 await foreach 通过 GetAsyncEnumerator 得到独立状态机,并发消费同一异步流是安全的——前提是迭代器方法体不依赖共享外部可变状态。同一个枚举器并发 MoveNextAsync 仍不安全。
关键点: 并发“消费”安全,并发“推进同一枚举器”不安全。