[C#.NET] 設計底層類別時,請勿過度使用例外攔截
不管是初學者,還是有多年經驗的程式設計師,常常會對於在什麼地方拋出例外感到疑惑。
MSDN的類別庫設計方針裡已有詳細的介紹,http://msdn.microsoft.com/zh-tw/library/ms229005,其中有一項是:
請勿過度使用攔截, 應該經常允許例外狀況散佈到呼叫堆疊上。
這是什麼意思呢?什麼叫做過度?什麼時候會發生?過度使用會怎樣?
如果你會有上述疑問,你可以繼續往下看。
我新增了一個類別庫名為RfidReader,並寫了 AlienReader 類別
namespace RfidReader
{
public class AlienReader : IReader
{
public event EventHandler<ConnectCompletedEventArgs> ConnectCompleted;
private bool _isConnected;
public bool IsConnected
{
get { return this._isConnected; }
set { _isConnected = value; }
}
public void Connect()
{
ConnectCompletedEventArgs e = new ConnectCompletedEventArgs();
this._isConnected = false;
try
{
//模擬例外
throw new NotSupportedException("No Support this Device!");
this._isConnected = true;
}
finally
{
e.IsConnected = this._isConnected;
this.OnConnect(e);
}
}
protected virtual void OnConnect(ConnectCompletedEventArgs e)
{
if (ConnectCompleted != null)
{
ConnectCompleted(this, e);
}
}
}
}
在此我模擬了一個例外發生,這是在 Reader.cs 的第24行
然後在另外一個 Middleware 類別裡引用 AlienReader 類別
using RfidReader;
namespace RfidReader
{
public class Middleware : INotify
{
private RfidReader.IReader reader = new AlienReader();
public event EventHandler<ConnectCompletedEventArgs> ConnectCompleted;
public Middleware()
{
reader.ConnectCompleted += reader_ConnectCompleted;
}
private void reader_ConnectCompleted(object sender, ConnectCompletedEventArgs e)
{
if (this.ConnectCompleted != null)
{
this.ConnectCompleted(sender, e);
}
}
public void Connect()
{
try
{
reader.Connect();
}
catch (Exception ex)
{
throw ex;
}
}
}
}
新增一個Winform專案,把RfidReader加入參考
我們來看看上述的Connect方法中的例外補捉寫法在UI端會造成什麼樣的結果
{
RfidReader.Middleware middleware = new Middleware();
try
{
middleware.Connect();
}
catch (Exception ex)
{
MessageBox.Show(ex.StackTrace);
}
}
按下button1後,我們得到了以下訊息,正確的例外應該是在 Reader.cs 的第24行,很明顯的例外堆疊被隱藏了(被重置),看不到Reader.cs 所拋出的例外,這會使你不知道真正發生錯誤的地方。
這也表示UI層將永遠不知道真正發生錯誤的地方。
at RfidReader.Middleware.Connect() in C:\Users\gy\Desktop\LogLibrary\LogLibrary\Middleware.cs:line 50
at Demo.Form1.button1_Click(Object sender, EventArgs e) in C:\Users\gy\Desktop\LogLibrary\Demo\Form1.cs:line 26
如果我把Middleware 類別的 Connect 方法改成以下
{
try
{
reader.Connect();
}
catch (Exception ex)
{
throw;
}
}
則會得到以下,在Middleware類別裡的例外,應該是第29行,因為重新拋出例外而變成了第33行,這讓堆疊追蹤指向重新擲回當做錯誤位置,但它仍保留了真正發生錯誤的地方。
這如同MSDN文件所述:
當攔截並重新擲回例外狀況時,最好使用空白擲回方式, 因為這是保留例外狀況呼叫堆疊的最好方式。
當攔截傳輸例外狀況的目的時,請不要排除任何特殊的例外狀況。
at RfidReader.AlienReader.Connect() in C:\Users\gy\Desktop\LogLibrary\LogLibrary\Reader.cs:line 24
at RfidReader.Middleware.Connect() in C:\Users\gy\Desktop\LogLibrary\LogLibrary\Middleware.cs:line 33
at Demo.Form1.button1_Click(Object sender, EventArgs e) in C:\Users\gy\Desktop\LogLibrary\Demo\Form1.cs:line 26
再把Middleware 類別的 Connect 方法改成以下
{
reader.Connect();
}
這樣堆疊追蹤指向就都完全正確了。
at RfidReader.AlienReader.Connect() in C:\Users\gy\Desktop\LogLibrary\LogLibrary\Reader.cs:line 24
at RfidReader.Middleware.Connect() in C:\Users\gy\Desktop\LogLibrary\LogLibrary\Middleware.cs:line 27
at Demo.Form1.button1_Click(Object sender, EventArgs e) in C:\Users\gy\Desktop\LogLibrary\Demo\Form1.cs:line 26
在這情況下,或許什麼都不做還比較好。
由上述簡單的實驗,我們可以知道,
應允許例外在調用堆疊往上傳遞,不要隨便使用 catch 然後再 throw (除非你瞭解你為何 catch 及 throw),不然將會:
- 讓程式碼更長,不斷的寫catch,還不知道 catch 裡的內容對還是不對,再對 catch 的內容做單元測試,怎麼寫都寫不完。
- 隱藏了例外堆疊的訊息,使得真正發生例外的被隱藏。
延伸閱讀:
[.NET] 使用 using 或 try/finally 清理資源
而在UI層捕捉例外時,我習慣這麼做,使用全域捕捉以及多執行緒捕捉,來捕捉漏網之魚。
[C#.NET][VB.NET] Winform 應用程式等級的例外捕捉 / Winform of Application Level wicth Exception Catch
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET