當我們想要使用實作 IDisposable
的型別時,using
關鍵字通常是不二人選,但是其中卻包含一些小陷阱 (嚴格來說不是陷阱,是實作 IDisposable
的時候沒做好)。
背景知識:
using
關鍵字就是 try-finally 加上呼叫IDisposable.Dispose()
的語法糖,沒把握的話找個反組譯工具確認一下就知道了。
有可能的問題
捕捉不到的例外
首先,用下面的範例程式來說明:1
2
3
4
5
6
7public class Foo : IDisposable
{
public void Dispose()
{
throw new Exception("a");
}
}
1 | // caller |
我們有一個實作 IDisposable
的類別,這個類別模擬在 Dispose
方法中拋出例外,而上面的程式碼因為 Foo.Dispose()
拋出例外而使得看起來能正常捕捉例外的程式碼其實是會有漏洞的。
拋出預期外的例外
那如果把 try-catch 區塊移到 using 外呢?以下方程式來說明:
1 | public class Foo : IDisposable |
1 | // caller |
一樣的 Foo
,不一樣的呼叫端,但是這樣使用會造成呼叫端的 catch 區塊中捕捉到的例外其實是 Foo.Dispose()
拋出的例外,這意味著當呼叫端程式發生例外時,錯誤根本不會被捕捉到,也就代表當 Production Issue 發生時,會完全看不到呼叫端程式真正的例外,在有時間壓力下發生這種事是很可怕的。
問題總結
這件事的根本原因是因為在 using
的 finally 區塊中拋出例外使得呼叫端誤以為自己有考慮到所有例外,或是呼叫端誤以為自己能捕捉到 try 區塊中的例外,但其實不然。
解決方案
服務提供方確保 IDisposable.Dispose()
不拋出例外
如果要實作 IDisposable
,必須確保 IDisposable.Dispose()
方法中不會拋出例外。
(不推薦) 呼叫端避免使用 using
就是用手動釋放資源取代 using
,如下:
1 | var f = new Foo(); |
一般來說,這個問題應該是服務提供方應該要注意的,所以除非確定服務提供方有這個缺陷且沒辦法要求改正,不然不推薦將這種作法作為預設選項。
(很不推薦) 呼叫端用多個 try-catch 暴力解
很不推薦的做法,雖然簡單但太過暴力,很醜且維護的人很容易覺得這是多餘的而拆掉其中一個 try-catch 區塊。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18try
{
using (var f = new Foo())
{
try
{
throw new Exception("a");
}
catch (Exception ex)
{
// do something
}
}
}
catch (Exception ex)
{
// do something else
}
結論
會想寫這篇是源於這個已知的問題,但我覺得這是微軟的鍋,不應該因噎廢食而放棄 using
,但萬一遇到了,還是要知道有這個現象來避免鬼打牆找不到問題,所以需要紀錄一下來加強印象。
參考
Close and Abort release resources safely when network connections have dropped