[C#.NET] 設計底層類別時,請勿過度使用例外攔截

  • 13298
  • 0
  • C#
  • 2012-08-14

[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行
image
然後在另外一個 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;
            }
        }
    }
}
image
新增一個Winform專案,把RfidReader加入參考
我們來看看上述的Connect方法中的例外補捉寫法在UI端會造成什麼樣的結果

{
    RfidReader.Middleware middleware = new Middleware();
    try
    {
        middleware.Connect();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.StackTrace);
    }
}
image
按下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

image

如果我把Middleware 類別的 Connect 方法改成以下


{
    try
    {
        reader.Connect();
    }
    catch (Exception ex)
    {
        throw;
    }
}
image
則會得到以下,在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

image

再把Middleware 類別的 Connect 方法改成以下


{
    reader.Connect();
}

image

這樣堆疊追蹤指向就都完全正確了。

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

image

 

在這情況下,或許什麼都不做還比較好。


由上述簡單的實驗,我們可以知道,

應允許例外在調用堆疊往上傳遞,不要隨便使用 catch 然後再 throw (除非你瞭解你為何 catch 及 throw),不然將會:

  1. 讓程式碼更長,不斷的寫catch,還不知道 catch 裡的內容對還是不對,再對 catch 的內容做單元測試,怎麼寫都寫不完。
  2. 隱藏了例外堆疊的訊息,使得真正發生例外的被隱藏。

延伸閱讀:

[.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

Image result for microsoft+mvp+logo