浅谈yield

c#1.0使用foreach 语句可以轻松地迭代集合。在c#1.0中,创建枚举器仍需要做大量的工作。c#2.0添加了yield语句,以便于创建枚举器。下面我们浅谈下yield的使用:

1、包含yield语句的方法或属性称为迭代块迭代块必须声明为返回IEnumerator或IEnumerable接口。这个块可以包含多个yield return语句或yield break语句,但不能包含return语句

      yield return语句返回集合的一个元素,并移动到下一个元素上。

      yield break语句:停止迭代

yield 语句只能出现在 iterator 块中,该块可用作方法、运算符或访问器的体。这类方法、运算符或访问器的体受以下约束的控制:

不允许不安全块。

方法、运算符或访问器的参数不能是 ref out

yield
语句不能出现在匿名方法中。有关更多信息,请参见匿名方法(C# 编程指南)。

当和 expression 一起使用时,yield return 语句不能出现在 catch 块中或含有一个或多个 catch 子句的 try 块中。有关更多信息,请参见异常处理语句(C# 参考)。

 2、yield语句从本质上讲是运用了延迟计算(Lazy evaluation或delayed evaluation)的思想。在Wiki上可以找到延迟计算的解释:将计算延迟,直到需要这个计算的结果的时候才计算,这样就可以因为避免一些不必要的计算而改进性能,在合成一些表达式时候还可以避免一些不必要的条件,因为这个时候其他计算都已经完成了,所有的条件都已经明确了,有的根本不可达的条件可以不用管了。

      延迟计算来源自函数式编程,在函数式编程里,将函数作为参数来传递,你想呀,如果这个函数一传递就被计算了,那还搞什么搞,如果你使用了延迟计算,表达式在没有使用的时候是不会被计算的,比如有这样一个应用:x=expression,将这个表达式赋给x变量,但是如果x没有在别的地方使用的话这个表达式是不会被计算的,在这之前x里装的是这个表达式。举个例子,linq就是运用了延迟计算的思想。看下面的代码:

var result = from book in books
    
where
 book.Title.StartWiths(“t”)
    select book
if(state > 0
)
{
    
foreach(var item in
 result)
    {
        
//….
   
}
}

result是一个实现了IEnumerable接口的类(Linq里,所有实现了IEnumerable接口的类都被称作sequence),对它的foreach或者while的访问必须通过它对应的IEnumeratorMoveNext()方法,如果我们把一些耗时的或者需要延迟的操作放在MoveNext()里面,那么只有等到MoveNext()被访问,也就是result被使用的时候那些操作才会执行,而给result赋值啊,传递啊,什么的,那些耗时的操作都没有被执行。

如果上面这段代码,最后由于state小于0,而对result没有任何需求了,在Linq里返回的结果都是IEnumerable的,如果这里没有使用延迟计算,那那个Linq表达式不就白运算了么?如果是Linq to Objects还稍微好点,如果是Linq to SQL,而且那个数据库表又很大,真是得不偿失啊,所以微软想到了这点,这里使用了延迟计算,只有等到程序别的地方使用了result才会计算这里的Linq表达式的值的,这样Linq的性能也比以前提高了不少,而且Linq to SQL最后还是要生成SQL语句的,对于SQL语句的生成来说,如果将生成延迟,那么一些条件就先确定好了,生成SQL语句的时候就可以更精练了。还有,由于MoveNext()是一步步执行的,循环一次执行一次,所以如果有这种情况:我们遍历一次判断一下,不满足我们的条件了我们就退出,如果有一万个元素需要遍历,当遍历到第二个的时候就不满足条件了,这个时候我们就可就此退出,后面那么多元素实际上都没处理呢,那些元素也没有被加载到内存中来。

延迟计算还有很多惟妙惟肖的特质,也许以后你也可以按照这种方式来编程了呢。写到这里我突然想到了Command模式,Command模式将方法封装成类,Command对象在传递等时候是不会执行任何东西的,只有调用它内部那个方法他才会执行,这样我们就可以把命令到处发,还可以压栈啊等等而不担心在传递过程中Command被处理了,也许这也算是一种延迟计算吧。

 

讲了yield的一些基础,觉得有必要讲下IEnumeratorIEnumerable接口区别:

 

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}
 
public interface IEnumerator
{
    bool MoveNext();
    void Reset();
 
    Object Current { get; }
}


 1、一个Collection要支持foreach方式的遍历,必须实现IEnumerable接口(亦即,必须以某种方式返回IEnumerator object)。
 
2
IEnumerator object具体实现了iterator(通过MoveNext()Reset()Current)。
 
3
、从这两个接口的用词选择上,也可以看出其不同:IEnumerable是一个声明式的接口,声明实现该接口的class可枚举(enumerable的,但并没有说明如何实现枚举器(iterator);IEnumerator是一个实现式的接口IEnumerator object就是一个iterator
 
4
IEnumerableIEnumerator通过IEnumerableGetEnumerator()方法建立了连接,client可以通过IEnumerableGetEnumerator()得到IEnumerator object,在这个意义上,将GetEnumerator()看作IEnumerator objectfactory method也未尝不可。


IEnumerator 是所有枚举数的基接口。  
   
 
枚举数只允许读取集合中的数据。枚举数无法用于修改基础集合。 这也是为什么说“不要在foreach循环中修改元素的原因
“.
   
 
最初,枚举数被定位于集合中第一个元素的前面。Reset 也将枚举数返回到此位置。在此位置,调用   Current 会引发异常。因此,在读取 Current 的值之前,必须调用 MoveNext 将枚举数提前到集合的第一个元素。
  
   
 
在调用   MoveNext      Reset   之前,Current   返回同一对象。MoveNext      Current   设置为下一个元素。  

   
 
在传递到集合的末尾之后,枚举数放在集合中最后一个元素后面,且调用   MoveNext   会返回   false。如果最后一次调用   MoveNext   返回   false,则调用   Current   会引发异常。若要再次将   Current   设置为集合的第一个元素,可以调用   Reset,然后再调用   MoveNext  
   
 
只要集合保持不变,枚举数就将保持有效。如果对集合进行了更改(例如添加、修改或删除元素),则该枚举数将失效且不可恢复,并且下一次对   MoveNext      Reset   的调用将引发   InvalidOperationException如果在   MoveNext      Current   之间修改集合,那么即使枚举数已经无效,Current   也将返回它所设置成的元素。
  
   
 
枚举数没有对集合的独占访问权;因此,枚举一个集合在本质上不是一个线程安全的过程。甚至在对集合进行同步处理时,其他线程仍可以修改该集合,这会导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合,或者捕捉由于其他线程进行的更改而引发的异常。  

 

 

代码下载:/Files/qlb5626267/YieldDemo.rar

参考文献:你必须知道的.net

你可能感兴趣的:(yield)