練習 Linq 原始碼的時候,遇到了迭代器的錯誤處理問題,由迭代器產生的錯誤沒有被外層 try-catch
正確的捕捉,所以這篇會介紹問題並解答。
問題:在 yield return
或 yield break
前直接產生錯誤,如以下程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13
| static void Main(string[] args) { try { GetError(); } catch (Exception ex) { } }
static IEnumerable<int> GetError() { throw new Exception("獲得錯誤"); yield break; }
|
這一個方法並不會讓程式直接停止運作,讓我想了很久,我的觀念以為錯誤會優先傳回,而這方法也因為沒有經過 yield
所以不會包裝成迭代器,所以我發了問題在C# 在迭代器中錯誤捕捉的問題 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天,不久就有好心的大大幫忙解決,詳細在連結裡,這邊就不多講了。
結論:使用關鍵字 yield
方法就會變成 lazy evaluation,就算沒有經過該段程式碼,並且在 lazy evaluation 中錯誤也需查詢才會發生。
後記:我也嘗試使用反編譯工具(ILSpy)來看,看兩者之間到底差在哪裡,程式碼如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class IteratorError { public IEnumerable<int> GetError() { throw new Exception("獲得錯誤"); yield return 0; } } class NormalError { public IEnumerable<int> GetError() { throw new Exception("獲得錯誤"); return null; } }
|
ILSpy 看到的原始碼為
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| internal class IteratorError { public IEnumerable<int> GetError() { throw new Exception("獲得錯誤"); yield break; } }
internal class NormalError { public IEnumerable<int> GetError() { throw new Exception("獲得錯誤"); } }
|
IteratorError
的 GetError()
的 yield return 0
變成了 yield break
,而 NormalError
的 GetError()
的 return
直接消失。
我認為是 IDE 產生的幻覺,因為程式碼跑不到的地方會有提示告訴開發者,但編譯後的樣子卻能發現兩者的差異,所以不經過 yield
也會變成 lazy evaluation,只是你以為編譯後會一樣。
再更,用 Ildasm.exe (IL 反組譯工具)來查看中繼碼,而不是像上面的還原成 C# 碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| .method public hidebysig instance class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> GetError() cil managed { .custom instance void [mscorlib]System.Runtime.CompilerServices.IteratorStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 28 43 6F 6E 73 6F 6C 65 41 70 70 32 2E 49 // ..(ConsoleApp2.I 74 65 72 61 74 6F 72 45 72 72 6F 72 2B 3C 47 65 // teratorError+<Ge 74 45 72 72 6F 72 3E 64 5F 5F 30 00 00 ) // tError>d__0.. // 程式碼大小 15 (0xf) .maxstack 8 IL_0000: ldc.i4.s -2 IL_0002: newobj instance void ConsoleApp2.IteratorError/'<GetError>d__0'::.ctor(int32) IL_0007: dup IL_0008: ldarg.0 IL_0009: stfld class ConsoleApp2.IteratorError ConsoleApp2.IteratorError/'<GetError>d__0'::'<>4__this' IL_000e: ret } // end of method IteratorError::GetError
|
1 2 3 4 5 6 7 8 9 10 11
| .method public hidebysig instance class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> GetError() cil managed { // 程式碼大小 12 (0xc) .maxstack 1 .locals init ([0] class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> V_0) IL_0000: nop IL_0001: ldstr bytearray (72 73 97 5F 2F 93 A4 8A ) // rs._/... IL_0006: newobj instance void [mscorlib]System.Exception::.ctor(string) IL_000b: throw } // end of method NormalError::GetError
|
兩段程式碼長得更不一樣了,當然不只 GetError 方法不一樣,迭代器版本的類別也多了一個屬於 GetError 的迭代器類別,可以很清楚區分兩者差異。
參考資料
mrkt 的程式學習筆記: .NET反組譯工具:ILSpy, Telerik JustDecompile
Ildasm.exe (IL 反組譯工具) Microsoft Docs