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

时间: 2024-11-03 22:28:34

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

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

对于yield关键字我们首先看一下msdn的解释: 如果你在语句中使用 yield 关键字,则意味着它在其中出现的方法.运算符或 get 访问器是迭代器. 通过使用 yield 定义迭代器,可在实现自定义集合类型的 IEnumerable 和 IEnumerator 模式时无需其他显式类(保留枚举状态的类,有关示例,请参阅 IEnumerator<T>). yield是一个语法糖 看msdn 的解释总是让人感觉生硬难懂.其实yield关键字很好理解.首先我们对于性质有个了解.yield是一个语

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

原文地址:http://www.cnblogs.com/santian/p/4389675.html 对于yield关键字我们首先看一下msdn的解释: 如果你在语句中使用 yield 关键字,则意味着它在其中出现的方法.运算符或 get 访问器是迭代器. 通过使用 yield 定义迭代器,可在实现自定义集合类型的 IEnumerable 和 IEnumerator 模式时无需其他显式类(保留枚举状态的类,有关示例,请参阅 IEnumerator<T>). yield是一个语法糖 看msdn

yield关键字的用法

在上一篇文章中,说了下foreach的用法,但是还是比较复杂的,要实现接口才能进行遍历,有没有简单些的方法呢?答案是肯定的.且看下面. yield关键字的用法: 1.为当前类型添加一个任意方法,但是要求该方法的返回值类型必须是IEnumerable:<代码1-1> 1 class Person 2 { 3 public string Name { get; set; } 4 public int Age { get; set; } 5 6 public string[] _Name = new

C#中yield关键字的使用个人总结

C#中yield关键字的使用个人总结: 1.首先,yield必须与return或者break组合在一起才能使用. 2.其次,必须在循环体中使用. 3.必须在迭代器块代码中使用. 在foreach语句中,in 后面跟随的对象,必须是IEnumerable“对象”(注:事实上,在C#里,接口是没有实例化的对象.但是,我们是可以创建“接口类型”的变量.然后可以把“继承了该接口的类”的对象赋给该变量.通过接口的多态性,使用这个变量.如果把接口理解成类,就很好理解了:若BClass继承于AClass,对于

c# yield关键字原理详解

c# yield关键字的用法 1.yield实现的功能 yield return: 先看下面的代码,通过yield return实现了类似用foreach遍历数组的功能,说明yield return也是用来实现迭代器的功能的. using static System.Console; using System.Collections.Generic; class Program { //一个返回类型为IEnumerable<int>,其中包含三个yield return public stat

yield关键字的使用

yield的中文是什么意思呢? 在金山词霸上面的翻译是: vt.屈服,投降: 生产: 获利: 不再反对 vi.放弃,屈服: 生利: 退让,退位 n.产量,产额: 投资的收益: 屈服,击穿: 产品 个人觉得比较符合yield关键字在C#中的意思是退让,退位,让权. yield关键字主要是在yield迭代器块中使用,使用yield的时候,C#编译器会创建一个状态机来保存当前我们迭代器块中的状态,返回当前需要返回的一个变量,然后暂时退出当前迭代器块.之后当下次需要调用迭代器块的时候,编译器会根据之前保

C#中yield关键字理解

yield关键字之前用得较少,但是在做项目开发的过程中也遇到了,当时有点迷惑,就顺便研究学习了一下,以下是个人理解,不到之处欢迎拍砖!废话就到这,上代码: class Program { static void Main(string[] args) { string[] strs = new string[] { "1", "2", "3", "4" }; foreach (var item in GetInts(strs)

python yield 关键字

最近看代码看到python里面的yield关键字,和我之前接触的语言好像都没有来着,我就查了查它的含义,大概理解如下: >>> def createGenerator(): ... mylist = range(3) ... for i in mylist: ... yield i*i ... >>> mygenerator = createGenerator() # create a generator >>> print(mygenerator)

使用yield关键字让自定义集合实现foreach遍历

一般来说当我们创建自定义集合的时候为了让其能支持foreach遍历,就只能让其实现IEnumerable接口(可能还要实现IEnumerator接口) 但是我们也可以通过使用yield关键字构建的迭代器方法来实现foreach的遍历,且自定义的集合不用实现IEnumerable接口 注:虽然不用实现IEnumerable接口 ,但是迭代器的方法必须命名为GetEnumerator() ,返回值也必须是IEnumerator类型 实例代码以及简单说明如下: 1 class Person 2 { 3