C#的yield关键字由来以久,如果我没有记错的话,应该是在C# 2.0中被引入的。相信大家此关键字的用法已经了然于胸,很多人也了解yield背后的“延迟赋值”机制。但是即使你知道这个机制,你也很容易在不经意间掉入它制造的陷阱。

目录
一、一个很简单的例子
二、简单谈谈“延迟赋值”
三、从反射的代码帮助我们更加直接的了解yield导致的延迟赋值
四、如果需要“立即赋值”怎么办?
后记

一、一个很简单的例子

下面是一个很简单的例子:Vector为自定义表示二维向量的类型,Program的静态方法GetVetors方法获取以类型为IEnumerable<Vector> 表示的Vector列表,而方法通过yield关键字返回三个Vectior对象。在Main方法中,将GetVetors方法的返回值赋值给一个变量,然后对每一个Vector对象的X和Y进行重新赋值,最后将每一个Vector的信息输出来。从最后的输出我们不难看出,我们对Vector的重新赋值无效,最终的每一个Vector元素依旧“保持”着初始值。

class Program

{

    static void Main(string[] args)

    {

        IEnumerable<Vector> vectors = GetVectors();

        foreach (var vector in vectors)

        {

            vector.X = 4;

            vector.Y = 4;

        }

 

        foreach (var vector in vectors)

        {

            Console.WriteLine(vector);

        }            

    }

 

    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对象被创建了次,来自于两次迭代。一次是对Vector元素的重新赋值,另一次源自对Vector元素的输出。由于两次迭代造作的并不是同一批对象,才会导致X和Y属性依然“保持”着原始的值。

   1: public class Vector

   2: {

   3:     //.....

   4:     public Vector(double x, double y)

   5:     {

   6:         Console.WriteLine("Vector object is instantiated.");

   7:         this.X = x;

   8:         this.Y = y;

   9:     }

  10: }

输出结果:

   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: }

  22:  

  23:  

从上面的代码我们可以看到,通过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:         }

  12:  

  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对象当成“只读”的,如果我们需要“可写”的功能,你应该使用数组或者集合类型。至于本文提到的“延迟赋值”或者“延迟计算”,如果就“枚举”功能而言,也不是很准确,因为“枚举”不承诺“赋值”。

从yield关键字看IEnumerable和Collection的区别的更多相关文章

  1. [原译]实现IEnumerable接口&理解yield关键字

    原文:[原译]实现IEnumerable接口&理解yield关键字 著作权声明:本文由http://leaver.me 翻译,欢迎转载分享.请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢 ...

  2. C# 基础小知识之yield 关键字 语法糖

    原文地址:http://www.cnblogs.com/santian/p/4389675.html 对于yield关键字我们首先看一下msdn的解释: 如果你在语句中使用 yield 关键字,则意味 ...

  3. C# 基础小知识之yield 关键字

    对于yield关键字我们首先看一下msdn的解释: 如果你在语句中使用 yield 关键字,则意味着它在其中出现的方法.运算符或 get 访问器是迭代器. 通过使用 yield 定义迭代器,可在实现自 ...

  4. 转载yield关键字理解

    实现IEnumerable接口及理解yield关键字   [摘要]本文介绍实现IEnumerable接口及理解yield关键字,并讨论IEnumerable接口如何使得foreach语句可以使用. 本 ...

  5. yield关键字的用法

    在上一篇文章中,说了下foreach的用法,但是还是比较复杂的,要实现接口才能进行遍历,有没有简单些的方法呢?答案是肯定的.且看下面. yield关键字的用法: 1.为当前类型添加一个任意方法,但是要 ...

  6. yield 关键字和迭代器

    一般使用方法     yield 关键字向编译器指示它所在的方法是迭代器块 在迭代器块中,yield 关键字与 return 关键字结合使用,向枚举器对象提供值. 这是一个返回值,例如,在 forea ...

  7. C#2.0中使用yield关键字简化枚举器的实现

    我们知道要使用foreach语句从客户端代码中调用迭代器,必需实现IEnumerable接口来公开枚举器,IEnumerable是用来公开枚举器的,它并不实现枚举器,要实现枚举器必需实现IEnumer ...

  8. c# yield关键字原理详解

    c# yield关键字的用法 1.yield实现的功能 yield return: 先看下面的代码,通过yield return实现了类似用foreach遍历数组的功能,说明yield return也 ...

  9. yield 关键字

    yield 关键字向编译器指示它所在的方法是迭代器块.编译器生成一个类来实现迭代器块中表示的行为.在迭代器块中,yield 关键字与 return 关键字结合使用,向枚举器对象提供值.这是一个返回值, ...

随机推荐

  1. MongoDB学习笔记~MongoDBRepository仓储的实现

    回到目录 仓储大叔,只要是持久化的东西,都要把它和仓储撤上关系,为啥,为的是开发人员在使用时统一,高可用及方便在各种方式之间实现动态的切换,如ef与redis和mongoDB的切换,你完成可以通过IR ...

  2. 设置共享,实现Linux和Windows之间的共享

    设置共享,实现Linux和Windows之间的共享 前提: 安装虚拟机.可以参考:在Windows上安装虚拟机详细图文教程 安装Linux.可以参考:在VMware Workstation里的Linu ...

  3. DIV+CSS规范命名大全

    网页制作中规范使用DIV+CSS命名规则,可以改善优化功效特别是团队合作时候可以提供合作制作效率,具体DIV CSS命名规则CSS命名大全内容篇. 常用DIV+CSS命名大全集合,即CSS命名规则 D ...

  4. 使用Jackson解析Json示例

    原文http://blog.csdn.net/gebitan505/article/details/17005735 custom.json: {     "country":&q ...

  5. 淘宝技术牛p博客整理

    淘宝的技术牛人的博客http://blog.csdn.net/zdp072/article/details/19574793

  6. 如何用python在Windows系统下,生成UNIX格式文件

    平时测试工作中,少不了制造测试数据.最近一个项目,我就需要制造一批可在UNIX下正确读取的文件.为确保这批文件能从FTP下载成功,开发叮嘱我:“文件中凡是遇到换行,换行符必须是UNIX下的LF,而不是 ...

  7. POJ2001Shortest Prefixes[Trie]

    Shortest Prefixes Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 17683   Accepted: 768 ...

  8. Server Tomcat v6.0 Server at localhost was unable to start within 45 seconds...

    仰天长啸   Server Tomcat v6.0 Server at localhost was unable to start within 45 seconds... 当启动tomcat时候出现 ...

  9. 3942: [Usaco2015 Feb]Censoring [KMP]

    3942: [Usaco2015 Feb]Censoring Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 375  Solved: 206[Subm ...

  10. SharePoint 2013常用开发工具分享

    众所周知,一款好的开发工具不仅能提高项目开发效率,而且能够协助开发人员简化开发流程.本文汇总几款SharePoint 2013开发常用开发工具,希望能够对大家有所帮助.如果您有更好的工具,没有包含在本 ...