yield关键字
------------本文导读A,B,C,D_Point-----------
C#的yield关键字由来以久,如果我没有记错的话,应该是在C# 2.0中被引入的。相信大家此关键字的用法已经了然于胸,很多人也了解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就是运用了延迟计算的思想。看下面的代码:
目录
一、一个很简单的例子
二、简单谈谈“延迟赋值”
三、从反射的代码帮助我们更加直接的了解yield导致的延迟赋值
四、如果需要“立即赋值”怎么办?
后记
public class List { //A_Point using System.Collections; public static IEnumerable Power(int number, int exponent) { int counter = 0; int result = 1; while (counter++ < exponent) { result = result * number;
//B_Point 返回到IEnumerable中 yield return result; } } static void Main() { // Display powers of 2 up to the exponent 8: foreach (int i in Power(2, 8)) { Console.Write("{0} ", i); } } } /* Output: 4 8 16 32 64 128 256 */
一、一个很简单的例子
下面是一个很简单的例子:Vector为自定义表示二维向量的类型,Program的静态方法GetVetors方法获取以类型为IEnumerable<Vector> 表示的Vector列表,而方法通过yield关键字返回三个Vectior对象。在Main方法中,将GetVetors方法的返回值赋值给一个变量,然后对每一个Vector对象的X和Y进行重新赋值,最后将每一个Vector的信息输出来。从最后的输出我们不难看出,我们对Vector的重新赋值无效,最终的每一个Vector元素依旧“保持”着初始值。
class Program { static void Main(string[] args) {
//C_Point可以理解为IEnumerable与List类似 IEnumerable<Vector> vectors = GetVectors(); foreach (var vector in vectors) { vector.X = 4; vector.Y = 4; } foreach (var vector in vectors) { Console.WriteLine(vector); } }
//D_Point yield return的用法 static IEnumerable<Vector> GetVectors() { yield return new Vector(1, 1); yield return new Vector(2, 3); yield return new Vector(3, 3); } } public class Vector { public double X { get; set; } public double Y { get; set; } public Vector(double x, double y) { this.X = x; this.Y = y; } public override string ToString() { return string.Format("X = {0}, Y = {1}", this.X, this.Y); } }
输出结果:
1: X = 1, Y = 1
2: X = 2, Y = 3
3: X = 3, Y = 3
二、简单谈谈“延迟赋值”
对于上面的现象,很多人一眼就可以看出这是由于yield背后的“延迟赋值”机制导致,但是不可否认我们会不经意间犯这种错误。为了让大家对这个问题有稍微深刻的认识,我们还是简单来谈谈“延迟赋值”。延迟赋值(Delay|Lazy Evaluation)又被称为延迟计算。为了避免不必要的计算导致的性能损失,和LINQ查询一样,yield关键字并不会导致后值语句的立即执行,而是转换成一个“表达式”。只有等到需要的那一刻(进行迭代)的时候,表达式被才被执行。
针对上面这个例子,我们对其进行简单的修改来验证“延迟赋值”的存在。我们只需要在Vector的构造函数中添加一行语句:Console.WriteLine("Vector object is instantiated.");。从运行后的结过我们可以看出,Vector对象被创建了6次,来自于两次迭代。一次是对Vector元素的重新赋值,另一次源自对Vector元素的输出。由于两次迭代造作的并不是同一批对象,才会导致X和Y属性依然“保持”着原始的值。
public class Vector { //..... public Vector(double x, double y) { Console.WriteLine("Vector object is instantiated."); this.X = x; this.Y = y; } }
输出结果:
1: Vector object is instantiated. 2: Vector object is instantiated. 3: Vector object is instantiated. 4: Vector object is instantiated. 5: X = 1, Y = 1 6: Vector object is instantiated. 7: X = 2, Y = 3 8: Vector object is instantiated. 9: X = 3, Y = 3
三、从反射的代码帮助我们更加直接的了解yield导致的延迟赋值
通过Reflector对编译后的代码进行发射,可以为我们更加“赤裸”地揭示yield导致的延迟赋值,下面的代码片断是对Program类型的“本质”反映。
1: internal class Program 2: { 3: private static IEnumerable<Vector> GetVectors() 4: { 5: return new <GetVectors>d__0(-2); 6: } 7: 8: private static void Main(string[] args) 9: { 10: IEnumerable<Vector> vectors = GetVectors(); 11: foreach (Vector vector in vectors) 12: { 13: vector.X = 4.0; 14: vector.Y = 4.0; 15: } 16: foreach (Vector vector in vectors) 17: { 18: Console.WriteLine(vector); 19: } 20: } 21: }
从上面的代码我们可以看到,通过yield关键字实现的GetVectors方法最终返回值是一个<GetVectors>d__0 类型的对象,该对象定义如下:
1: [CompilerGenerated]
2: private sealed class <GetVectors>d__0 : IEnumerable<Vector>, IEnumerable, IEnumerator<Vector>, IEnumerator, IDisposable
3: {
4: private int <>1__state;
5: private Vector <>2__current;
6: private int <>l__initialThreadId;
7:
8: [DebuggerHidden]
9: public <GetVectors>d__0(int <>1__state);
10: private bool MoveNext();
11: [DebuggerHidden]
12: IEnumerator<Vector> IEnumerable<Vector>.GetEnumerator();
13: [DebuggerHidden]
14: IEnumerator IEnumerable.GetEnumerator();
15: [DebuggerHidden]
16: void IEnumerator.Reset();
17: void IDisposable.Dispose();
18:
19: Vector IEnumerator<Vector>.Current { [DebuggerHidden] get; }
20: object IEnumerator.Current { [DebuggerHidden] get; }
21: }
这是一个实现了众多接口的类型,实现的接口包括:IEnumerable<Vector>, IEnumerable, IEnumerator<Vector>, IEnumerator, IDisposable。<GetVectors>d__0 类大部分成员都没有复杂的逻辑,唯一值得一提的就是MoveNext方法。从中我们清楚地但到,对Vector对象的创建发生在每一个迭代中。
1: private bool MoveNext()
2: {
3: switch (this.<>1__state)
4: {
5: case 0:
6: this.<>1__state = -1;
7: this.<>2__current = new Vector(1.0, 1.0);
8: this.<>1__state = 1;
9: return true;
10:
11: case 1:
12: this.<>1__state = -1;
13: this.<>2__current = new Vector(2.0, 3.0);
14: this.<>1__state = 2;
15: return true;
16:
17: case 2:
18: this.<>1__state = -1;
19: this.<>2__current = new Vector(3.0, 3.0);
20: this.<>1__state = 3;
21: return true;
22:
23: case 3:
24: this.<>1__state = -1;
25: break;
26: }
27: return false;
28: }
29:
四、如果需要“立即赋值”怎么办?
有时候我们不需要“延迟赋值”,而需要“立即赋值”,因为调用着需要维护它们的状态,那该怎么办呢?有人说,不用yield不久得到吗?但是有的情况下,我们需要调用别人提供的API来获取IEnumerable<T>对象,我们不清楚对方有没有使用yield关键字。在这种情况我个人常用的做法就是调用ToArray或者ToList将其转换成T[]或者List<T>,进而进行强制赋值。由于它们也实现了接口IEnumerable<T>,所以不会存在什么问题。同样是对于我们的例子,我们在对GetVectors方法的返回值进行变量赋值的时候的调用ToArray或者ToList方法,我们就能对元素进行有效赋值。
1: class Program 2: { 3: //...... 4: static void Main(string[] args) 5: { 6: IEnumerable<Vector> vectors = GetVectors().ToList(); 7: foreach (var vector in vectors) 8: { 9: vector.X = 4; 10: vector.Y = 4; 11: } 13: foreach (var vector in vectors) 14: { 15: Console.WriteLine(vector); 16: } 17: } 18: }
或者:
1: class Program 2: { 3: //...... 4: static void Main(string[] args) 5: { 6: IEnumerable<Vector> vectors = GetVectors().ToArray(); 7: foreach (var vector in vectors) 8: { 9: vector.X = 4; 10: vector.Y = 4; 11: } 12: 13: foreach (var vector in vectors) 14: { 15: Console.WriteLine(vector); 16: } 17: } 18: }
输出结果:
1: X = 4, Y = 4
2: X = 4, Y = 4
3: X = 4, Y = 4
后记
其实本篇文章的意图并不在于yield这个关键字如何如何,因为不止是yield,我们一般的LINQ查询也会导致这个问题,而是借此说明IEnumerable对象和Array、List这样的集合类型的区别。IEnumerable这个接口和集合没有本质的联系,只是提供“枚举”的功能。甚至说,我们应该将IEnumerable对象当成“只读”的,如果我们需要“可写”的功能,你应该使用数组或者集合类型。至于本文提到的“延迟赋值”或者“延迟计算”,如果就“枚举”功能而言,也不是很准确,因为“枚举”不承诺“赋值”。
浙公网安备 33010602011771号