C#中的generator不会被立即执行……

本文通过对比分析 Eric Lippert 博文中提供的 C# 扩展方法实例,深入探讨了 C# 中 generator 的工作原理及注意事项,特别是 yield 关键字的行为特征。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

读了[url=http://blogs.msdn.com/ericlippert/default.aspx]Eric Lippert[/url]的这帖:[url=http://blogs.msdn.com/ericlippert/archive/2008/09/08/high-maintenance.aspx]High maintenance[/url]之后,心里一阵寒——我自己写的代码里就有几乎一模一样的逻辑。

Eric的帖里举的不好的例子:
public static class StreamReaderExtensions
{
public static IEnumerable<string> Lines(this StreamReader reader)
{
if (reader== null)
throw new ArgumentNullException("reader");
reader.BaseStream.Seek(0, SeekOrigin.Begin);
string line;
while ((line = reader.ReadLine()) != null)
yield return line;
}
}


我的代码:
/// <summary>
/// Reads the stream as lines of strings from the current
/// position to the end of the stream.
/// </summary>
/// <param name="reader">the stream to read from</param>
/// <returns>An enumerator of the lines of strings.</returns>
public static IEnumerable<string> Lines( this StreamReader reader ) {
for ( var line = reader.ReadLine( ); null != line; line = reader.ReadLine( ) ) {
yield return line;
}
}

事实上我的还更糟糕一些,没做null检查。写扩展方法的时候我总是写着写着就忘了:扩展方法不是成员方法,作为“this”的参数需要做null检查。

Eric对他所举的例子指出了几个不足:
1、[b]null检查不是在方法调用时执行,而是在迭代器第一次移动的时候才执行的[/b];
2、while循环在一行塞了太多逻辑,可以分开来写;
3、通过BaseStream来改变了底下的流的位置,不符合习惯上对流的使用方式;
4、调用者需要知道太多细节,例如必须知道何时这个迭代器结束了,在结束后由调用者来关闭流;
……等。

第4点我在写我那个程序时也知道这里有问题,但没想出好的解决办法。假如我就是要特意把流设置到某个位置之后再调用Lines(),该怎么办呢?或者说假如原本就不存在一个实际文件(例如stdin),那就无法用文件名来指定参数了,又怎么办呢?
于是在Stream的ownership上我还是得再思考一下才行。这篇主要是要提醒自己C#的generator的一个重要特性:

带有yield关键字的方法(也就是generator),其方法体在generator被调用时是不会被执行的;只有当其返回的iterator的MoveNext()方法被初次调用时才会执行。

举例来说,这样:
using System;
using System.Collections.Generic;

static class Program {
static IEnumerable<int> FooGenerator( int max ) {
Console.WriteLine( "Starting enumerator" );
for ( var i = 0; i < max; ++i ) {
yield return i;
}
}

static void Main( string[ ] args ) {
var list = FooGenerator( 3 );
Console.WriteLine( "After Foo(), before foreach" );
foreach ( var i in list ) {
Console.WriteLine( i );
}
}
}

输出的结果会是:
[code]After Foo(), before foreach
Starting enumerator
0
1
2[/code]

了解C#的generator的实现方式就不难理解这个行为的来源。C#的generator实际上是个由编译器自动生成的实现了IEnumerable<T>接口的有限状态机。也就是说generator里实际上只有一个return new ...,或许还会有些参数赋值,却没有任何别的内容。源码里写在generator里的逻辑都生成到了那个有限状态机对象里,也就是外界看到的迭代器里。如果要对参数做检查,恐怕还就是Eric说的,提供一个公有方法作为接口,在里面检查参数的正确性,然后再调用一个私有的generator来完成实际迭代工作。

用FooGenerator的例子说,假如max小于0是不符合要求的,那么应该这样写:
public static IEnumerable<int> FooGenerator( int max ) {
if ( 0 > max ) {
throw new ArgumentException( "Number must be non-negative", "max" );
}
return FooGeneratorCore( max );
}

private static IEnumerable<int> FooGenerator( int max ) {
for ( var i = 0; i < max; ++i ) {
yield return i;
}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值