練習 Linq 原始碼的時候,遇到了迭代器的錯誤處理問題,由迭代器產生的錯誤沒有被外層 try-catch 正確的捕捉,所以這篇會介紹問題並解答。

問題:在 yield returnyield 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("獲得錯誤");
}
}

IteratorErrorGetError()yield return 0 變成了 yield break,而 NormalErrorGetError()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