前幾天在寫using的時候,裡面再包一層try catch時,總覺得良心很過意不去,但是就當下的程式碼結構一定非要這樣寫不可(為了有條件的攔截錯誤而修正,不讓整個using結構因為某個錯誤立即崩潰)。於是想寫這一篇文章,讓面臨這樣抉擇的人可以重新的審視一下自己是否真的有需要在using裡面再包try catch。
話不多說,請先看一下程式碼:
using System; using static System.Console; namespace Simply { class Program { public static void Main() { try { MyMethod(); } catch (Exception oEx) { WriteLine("run Main() catch block."); WriteLine(oEx.Message); } finally { WriteLine("run Main() finally block."); } WriteLine("OK."); Read(); } public static void MyMethod() { int x = 10; int y = 0; try { x = x / y; } catch { WriteLine("run MyMethod() catch block."); throw new Exception("Catch It!"); } finally { WriteLine("run MyMethod() finally block."); } } } }
整段程式碼中,就是故意在MyMethod()中加入一個除零的錯誤,讓try被觸發到,那麼到底是catch段先執行,亦或是finally段被執行呢?答案請看輸出結果...
run MyMethod() catch block. run MyMethod() finally block. run Main() catch block. Catch It! run Main() finally block. OK.
很明顯的,當try捕捉到錯誤時,是先跑catch段,才再去跑finally段。且內部throw new Exception被觸發時,這個錯誤會被先押入堆疊中,然後程式碼還是會先把內層finally段跑完,再往層外拋錯誤。
差異點在於在Java中,是允許你在finally段中寫下「return;」,進行中斷跳出區塊的做法,但是這個在C#中是不被允許的。估計是C#語言希望你在finally段被執行完成後,還有機會去把在堆疊中的指令(throw new Exception)再接著跑下去。
如果你硬要在C#中的finally段裡面加入「return;」指令,你會被編譯器回應下列字眼:
編譯器錯誤 CS0157 控制項不可脫離finally子句的主體。 必須執行finally子句中的所有陳述式。 Control cannot leave the body of a finally clause. All of the statements in a finally clause must execute.
這邊順便紀錄一下Using區段在經過編譯器的編譯後,背後幫我們轉回try-catch的結構概念。
Using的寫法:
using (System.IO.StreamReader oSR = new System.IO.StreamReader(@"C:\test.txt")) { string s = null; while((s = oSR.ReadLine()) != null) { Console.WriteLine(s); } }
被編譯過後的寫法:
{ System.IO.StreamReader oSR = new System.IO.StreamReader(@"C:\test.txt"); try { string s = null; while((s = oSR.ReadLine()) != null) { Console.WriteLine(s); } } finally { if (oSR != null) { ((IDisposable)oSR).Dispose(); } } }
看到這邊大家應該要注意下列的幾點重點:
請特別注意,using在編譯器翻譯後,只有實作Try-Finally,並沒有包含catch區段、並沒有包含catch區段、並沒有包含catch區段(很重要所以說三次)。
在複雜的情況下,你或許應該在using區段中(或者在區段之外),自己再實作try-catch自己攔截與處理某些不需要整個中斷流程的錯誤。有需要的可以參考一下MSDN官方文件。以下就用官方文件中的程式碼再帶大家仔細的觀察一次using語法:
using語法:
using (Font font1 = new Font("Arial", 10.0f)) { byte charset = font1.GdiCharSet; }
經過compiler編譯過的using結構原型,很有趣的是請特別注意字型物件宣告的那行,竟然是在Try結構之前就大喇喇地放上了,也就是說如果在實例化這個字型物件的時期出錯了,事實上還是會出錯的:
{ Font font1 = new Font("Arial", 10.0f); try { byte charset = font1.GdiCharSet; } finally { if (font1 != null) ((IDisposable)font1).Dispose(); } }
文章最末附上System.Data.SqlClient.SqlConnection.Dispose方法的反組譯程式碼:
// System.Data.SqlClient.SqlConnection.Dispose disassemble protected override void Dispose(bool disposing) { if (disposing) { this._userConnectionOptions = null; this._poolGroup = null; this.Close(); //人家有呼叫Close();喔! } this.DisposeMe(disposing); base.Dispose(disposing); }
可以的話,請順便參考這篇文章:
Try-Catch Using 錯誤捕捉 誤解 Dispose IDisposable