从yield关键字看IEnumerable和Collection的区别
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的区别的更多相关文章
- [原译]实现IEnumerable接口&理解yield关键字
原文:[原译]实现IEnumerable接口&理解yield关键字 著作权声明:本文由http://leaver.me 翻译,欢迎转载分享.请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢 ...
- C# 基础小知识之yield 关键字 语法糖
原文地址:http://www.cnblogs.com/santian/p/4389675.html 对于yield关键字我们首先看一下msdn的解释: 如果你在语句中使用 yield 关键字,则意味 ...
- C# 基础小知识之yield 关键字
对于yield关键字我们首先看一下msdn的解释: 如果你在语句中使用 yield 关键字,则意味着它在其中出现的方法.运算符或 get 访问器是迭代器. 通过使用 yield 定义迭代器,可在实现自 ...
- 转载yield关键字理解
实现IEnumerable接口及理解yield关键字 [摘要]本文介绍实现IEnumerable接口及理解yield关键字,并讨论IEnumerable接口如何使得foreach语句可以使用. 本 ...
- yield关键字的用法
在上一篇文章中,说了下foreach的用法,但是还是比较复杂的,要实现接口才能进行遍历,有没有简单些的方法呢?答案是肯定的.且看下面. yield关键字的用法: 1.为当前类型添加一个任意方法,但是要 ...
- yield 关键字和迭代器
一般使用方法 yield 关键字向编译器指示它所在的方法是迭代器块 在迭代器块中,yield 关键字与 return 关键字结合使用,向枚举器对象提供值. 这是一个返回值,例如,在 forea ...
- C#2.0中使用yield关键字简化枚举器的实现
我们知道要使用foreach语句从客户端代码中调用迭代器,必需实现IEnumerable接口来公开枚举器,IEnumerable是用来公开枚举器的,它并不实现枚举器,要实现枚举器必需实现IEnumer ...
- c# yield关键字原理详解
c# yield关键字的用法 1.yield实现的功能 yield return: 先看下面的代码,通过yield return实现了类似用foreach遍历数组的功能,说明yield return也 ...
- yield 关键字
yield 关键字向编译器指示它所在的方法是迭代器块.编译器生成一个类来实现迭代器块中表示的行为.在迭代器块中,yield 关键字与 return 关键字结合使用,向枚举器对象提供值.这是一个返回值, ...
随机推荐
- 用tpcc测试对比 innodb 和 tokudb
测试环境 1台IBM Intel(R) Xeon(R) CPU E5606 @ 2.13GHz,内存12G cd tpcc/tpcc-mysql/src # make cc lo ...
- java实现REST方式的webService
一. 简介 WebService有两种方式,一是SOAP方式,二是REST方式.SOAP是基于XML的交互,WSDL也是一个XML文档, 可以使用WSDL作为SOAP的描述文件:REST是基于HTTP ...
- 《Inside UE4》目录
<Inside UE4>目录 InsideUE4 UE4无疑是非常优秀的世界上最顶尖的引擎之一,性能和效果都非常出众,编辑器工作流也非常的出色,更难得宝贵的是完全的开源让我们有机会去从中吸 ...
- python 聊天室
server端程序 # -*- coding: utf-8 -*- #!/usr/bin/python """ """ import soc ...
- 电脑控制Android设备的软件——Total Control
最早开始搞Android开发时,为了调试方便,想找一个Android下的远程控制软件,支持在电脑端远程控制和同步显示Android设备.先后试了360手机助手.Mobizen.Vysor和Mirror ...
- Struts2 使用jQuery实现Ajax
在jQuery中将Ajax相关的操作进行封装,使用时只需在合适的地方调用Ajax相关的方法即可,相比而言,使用jQuery实现Ajax更加简洁,方便 1.$.Ajax()可以通过发送Http请求加载远 ...
- python generator: next , sent(msg)区别
对于普通的生成器,第一个next调用,相当于启动生成器,会从生成器函数的第一行代码开始执行,直到第一次执行完yield语句(第4行)后,跳出生成器函数. 然后第二个next调用,进入生成器函数后,从y ...
- Eclipse: 提示 Toolchain "MinGW GCC" is not detected
解决办法: 1. 把 D:\MinGW\bin, 设置到 PATH 路径. 2. 重启 eclipse
- java多线程系类:JUC集合:01之框架
概要 之前,在"Java 集合系列目录(Category)"中,讲解了Java集合包中的各个类.接下来,将展开对JUC包中的集合进行学习.在学习之前,先温习一下"Java ...
- 关于NODE NPM 输入命令后没反应的问题
输入NPM 命令 如 install config help都没有反应,光标在下面一直闪,只有 -v 有反应,查了下,是npm config set prefix 改包的路径出问题了 解决办法就是删 ...