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

  • 相关阅读:
    整理牙刷
    color 圆盘染色
    数论の一波流[长期更新]
    生成树
    一维黑白棋
    Factorials
    平面分割问题
    poj1183 反正切函数
    烽火传递
    校门外的树
  • 原文地址:https://www.cnblogs.com/artech/p/yield.html
Copyright © 2011-2022 走看看