從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