昨天寫了《yield在WCF中的錯誤使用——99%的開發人員都有可能犯的錯誤[上篇]》,引起了一些討論。關於yield關鍵字這個語法糖背后的原理(C#編譯器將它翻譯成什么)其實挺簡單,雖然有時候因為誤用它會導致一些問題,但是它本無過錯。接下來,我們通過這篇短文簡單地談談我所理解的yield。
目錄
一、先看一個簡單的例子
二、了解本質,只需要看看yield最終編譯成什么
三、回到WCF的例子
一、先看一個簡單的例子
我們現在看一個簡單的例子。我們在一個Console應用中編寫了如下一段簡單的程序:返回類型為IEnumerable<string>的方法GetItems以yield return的方式返回一個包含三個字符串的集合,而在方法開始的時候我們打印一段文字表明定義在方法中的操作開始執行。在Main方法中,我們先調用GetItems方法將“集合對象”返回,然后調用其ToArray方法。在調用該方法之前我們打印一段文字表明對集合對象進行迭代。
static void Main(string[] args) { IEnumerable<string> items = GetItems(); Console.WriteLine("Begin to iterate the collection."); items.ToArray(); } static IEnumerable<string> GetItems() { Console.WriteLine("Begin to invoke GetItems() method"); yield return "Foo"; yield return "Bar"; yield return "Baz"; }
對於上面這段代碼,我想肯定有人會認為得到的結果應該是這樣:
Begin to invoke GetItems() method Begin to iterate the collection.
但是下面才是真正的執行結果。也就是說,一旦我們在一個返回類型為IEnumerable或者IEnumerable<T>的方式中通過yield return返回集合元素,意味着這個定義在方法中操作會被“延后執行”——操作的真正執行不是發生在方法調用的時候,而是延后到對返回的集合進行迭代的時候。我們大體可以以這樣的方式來“解釋”這個現象:一旦我們使用了yield return,返回元素的操作會被封裝成“可執行的表達式”的方式返回,一旦我們對集合進行迭代的時候,這些表達式才會被執行。
Begin to iterate the collection.
Begin to invoke GetItems() method
二、了解本質,只需要看看yield最終編譯成什么
上面我們通過“延遲執行”和“可執行表達式”的形式來解釋yield return,僅僅是為了比較好地理解它所體現出來的效果而已,實際上並沒有這回事,這與LINQ的延遲加載更不是一回事。yield return僅僅是C#的一個語法糖而已,是編譯器玩的一個小花招。如何透過這一層“糖紙”看到本質的東西,只需要看看編譯器最終編譯后的與之等效的代碼是什么樣子就可以了。對於上面這個例子來說,不管GetItems方法中以何種方式返回需要的對象,返回值總歸是一個實現了IEnumerable <string>接口的某個類型的對象,我們只需要看看這個類型具有怎樣的定義就知道C#編譯器如果來“解釋”yield return。
我們可以直接利用Reflector打開編譯后的程序集,然后將.NET Framework的版本調成1.0(不支持C#針對后續版本提供的語法糖),這樣就可以以“本質”的方式查看我們編寫的代碼了。如下面的代碼片段所示,GetItems方法中沒有發現我們定義的代碼,而是直接返回一個類型為<GetItems>d__0的對象,看到這里相信讀者朋友們知道為什么執行GetItems方法的時候並沒有文字輸出的真正原因了吧。
internal class Program { private static IEnumerable<string> GetItems() { return new <GetItems>d__0(-2); } private sealed class <GetItems>d__0 : IEnumerable<string>, IEnumerable, IEnumerator<string>, IEnumerator, IDisposable }
<GetItems>d__0是自動生成的類型,它實現了IEnumerable<string>接口,也實現了IEnumerator<string>,其 GetEnumerator()方法返回的實際上就是他自己。至於對<GetItems>d__0對象的進行迭代的時候如何返回具體元素,只要看看該類型的定義就一目了然了。如下面的代碼片段所示,集合元素的返回實現在MoveNext()方法中,方法開始的操作(Console.WriteLine("Begin to invoke GetItems() method"))發生在第一次迭代的時候。
private sealed class <GetItems>d__0 : IEnumerable<string>, IEnumerable, IEnumerator<string>, IEnumerator, IDisposable { private int <>1__state; private string <>2__current; private bool MoveNext() { switch (this.<>1__state) { case 0: this.<>1__state = -1; Console.WriteLine("Begin to invoke GetItems() method"); this.<>2__current = "Foo"; this.<>1__state = 1; return true; case 1: this.<>1__state = -1; this.<>2__current = "Bar"; this.<>1__state = 2; return true; case 2: this.<>1__state = -1; this.<>2__current = "Baz"; this.<>1__state = 3; return true; case 3: this.<>1__state = -1; break; } return false; } string IEnumerator<string>.Current { [DebuggerHidden] get { return this.<>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return this.<>2__current; } } }
三、回到WCF的例子
再次回到《yield在WCF中的錯誤使用——99%的開發人員都有可能犯的錯誤[上篇]》中提到的例子,現在來解釋為什么針對如下兩段代碼,前者拋出的異常不能被WCF正常處理,而后者可以。原因很簡單——兩段代碼拋出異常的時機是不一樣的。對於后者,異常在執行GetItems方法的時候會立即拋出來,WCF會捕獲這個異常並作為應用級別的異常進行正常處理;對於前者,通過上面的分析我們知道異常實際上發生在對返回“集合對象”進行迭代的時候。具體是什么時候呢?其實就是對返回對象進行序列化的時候,此時拋出的異常將將會視為系統異常來處理。
public class DemoService : IDemoService { public IEnumerable<string> GetItems(string categoty) { if (string.IsNullOrEmpty(categoty)) { throw new FaultException("Invalid category"); } yield return "Foo"; yield return "Bar"; yield return "Baz"; } } public class DemoService : IDemoService { public IEnumerable<string> GetItems(string categoty) { if (string.IsNullOrEmpty(categoty)) { throw new FaultException("Invalid category"); } return new string[] { "Foo", "Bar", "Baz" }; } }
我個人覺得這是WCF值得改進的地方,但是目前來說為了避免這樣的問題,我推薦將WCF契約接口操作方法中的返回類型定義成數組,而不是IEnumerable或者IEnumerable<T>(順便說一下,WCF針對Array、List以及其他集合類型的序列化/反序列化行為是一致的),但是我個人對IEnumerable或者IEnumerable<T>不排斥。
yield在WCF中的錯誤使用——99%的開發人員都有可能犯的錯誤[上篇]
yield在WCF中的錯誤使用——99%的開發人員都有可能犯的錯誤[下篇]