閱讀625 返回首頁    go 阿裏雲 go 技術社區[雲棲]


從yield關鍵字看IEnumerable和Collection的區別

C#的關鍵字由來以久,如果我沒有記錯的話,應該是在C# 2.0中被引入的。相信大家此關鍵字的用法已經了然於胸,很多人也了解yield背後的“”機製。但是即使你知道這個機製,你也很容易在不經意間掉入它製造的陷阱。

目錄
一、一個很簡單的例子
二、簡單談談“延遲賦值”
三、從反射的代碼幫助我們更加直接的了解yield導致的延遲賦值
四、如果需要“立即賦值”怎麼辦?
後記

下麵是一個很簡單的例子:Vector為自定義表示二維向量的類型,Program的靜態方法GetVetors方法獲取以類型為IEnumerable<Vector> 表示的Vector列表,而方法通過yield關鍵字返回三個Vectior對象。在Main方法中,將GetVetors方法的返回值賦值給一個變量,然後對每一個Vector對象的X和Y進行重新賦值,最後將每一個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的構造函數中添加一行語句:。從運行後的結過我們可以看出,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

通過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方法最終返回值是一個 類型的對象,該對象定義如下:

   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 類大部分成員都沒有複雜的邏輯,唯一值得一提的就是方法。從中我們清楚地但到,對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關鍵字。在這種情況我個人常用的做法就是調用或者將其轉換成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這個接口和集合沒有本質的聯係,隻是提供“枚舉”的功能。甚至說,我們應該將IEnumerable對象當成“”的,如果我們需要“可寫”的功能,你應該使用數組或者集合類型。至於本文提到的“延遲賦值”或者“延遲計算”,如果就“枚舉”功能而言,也不是很準確,因為。


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-27 14:04:30

  上一篇:go  創建代碼生成器可以很簡單:如何通過T4模板生成代碼?[下篇]
  下一篇:go  完整複現何愷明ICCV獲獎論文結果並開源 !(附論文&開源代碼)