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. 特定IP访问远程桌面

    1. 新建IP安全策略 (远程端口没有修改情况下的设置) WIN+R打开运行对话框,输入“gpedit.msc”进入组策略编辑器. 依次打开:本地计算机策略--计算机配置--Windows设置--安全 ...

  2. UNIX系统的显示时间何时会到达尽头

    本文分为三个小块: 一.UNIX系统中时间的存储形式: 二. time_t 的最大值是多少: 三. 将time_t 的最大值转化为真实世界的时间: #---------------------# # ...

  3. 大数据系列(4)——Hadoop集群VSFTP和SecureCRT安装配置

    前言 经过前三篇文章的介绍,已经通过VMware安装了Hadoop的集群环境,当然,我相信安装的过程肯定遇到或多或少的问题,这些都需要自己解决,解决的过程就是学习的过程,本篇的来介绍几个Hadoop环 ...

  4. VI常用的命令

    vi filename : 打开或者新建一个文件夹,并将光标置于第一行首位 I : 表示光标在当前位置编辑文本 A : 表示光标进入下以恶字符位置编辑文件 X : 每按一次删除光标所在位置的前面一个字 ...

  5. 我是如何把VC6一直用到2016年的

    写下这个标题的时候,也是表明必须需要改变的时候了…… 黄山松 (Tom Huang) 发表于博客园http://www.cnblogs.com/tomview/ 最早从windows3.1使用vb3编 ...

  6. Nagios监控平台搭建

    Nagios是一款开源的免费网络监视工具,能有效监控Windows.Linux和Unix的主机状态,交换机路由器等网络设置,打印机等.在系统或服务状态异常时发出邮件或短信报警第一时间通知网站运维人员, ...

  7. JavaScript根据文件名后缀判断是否图片文件

    //JavaScript根据文件名后缀判断是否图片文件 //图片文件的后缀名 var imgExt = new Array(".png",".jpg",&quo ...

  8. Ubuntu14-04 MySQL-5.6.21通用二进制安装

    #卸载mysql /etc/init.d/mysqld stop &> /dev/null killall mysqld &> /dev/null sudo rm -rf ...

  9. JSCH通过密钥文件进行远程访问

    需求:WEB app 需要使用JSCH来通过密钥文件的方式进行SFTP/SSH访问远程LINUX机器 实现方式:假设远程机器都含有用户名为hadoop的用户,因为密码因为策略的要求密码会随时间发生变化 ...

  10. 第8章 Java类的三大特性之一:封装

    1.什么是封装 1.1概念 将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问.主要是类的属性 1.2好处 a.只能通过规定的方法访问数据b.隐藏类 ...