前言

寫程式時,幾乎一定會遇到需要使用 try & catch 的情境。

它看起來只是把程式碼包起來,但實際上是在幫我們處理「程式執行時可能出現的例外狀況」。這篇就用幾個簡單範例,整理 try & catch 的用途、常見寫法,以及幾個容易混淆的小觀念。

try & catch 的作用是什麼?

簡單來說,try & catch 通常有兩個主要使用契機:

避免程式在執行時,因為非預期錯誤而直接中斷。

針對已預期可能發生的例外情境,設計對應的處理流程。

大多數情況下,我們使用 try & catch ,都是為了避免程式發生 崩潰(crash),導致後續功能無法繼續執行。

只要程式沒有直接 crash,通常就還有機會繼續運作,或至少能留下錯誤資訊,方便後續追查。至於功能是否完全正確,那就是另一個故事了

1
2
3
4
5
6
7
8
9
// 程式發生例外錯誤的簡單範例
static void Main(string[] args)
{
string a = null;
a = a.ToString();

Console.WriteLine(a);
Console.ReadKey();
}

上面的例子就是一個很典型的程式崩潰情境。

anull,卻又呼叫 .ToString() 時,就會拋出 NullReferenceException。如果沒有任何例外處理,程式就會直接 crash 給你看。(命令視窗可能會直接關掉不見)

而在程式碼越多、流程越複雜的情況下,沒有人能保證完全不會發生例外錯誤

所以,我們通常會把可能發生例外的程式碼放進 try & catch 裡,避免錯誤一路往外拋,最後導致整個程式中斷。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用 try & catch 的簡單範例
try
{
string a = null;
a = a.ToString();

Console.WriteLine(a);
}
catch(Exception ex)
{
Console.WriteLine(ex);
}
Console.ReadKey();

同樣的程式碼,在使用 try & catch 包起來之後,就不會因為例外而直接中斷。

try { … } 區段裡發生例外狀況時,catch { … } 區段就會接住這個例外,讓我們可以決定後續要怎麼處理。

也就是說,catch 的重點不是「讓錯誤消失」,而是定義當例外發生時,程式應該做什麼處理

try & catch 的規則與用法

一組完整的 try…catch 中,try 區段只能有一個,但 catch 可以有多個,用來處理不同類型的例外。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try
{
// 主程式碼內容
// ...
}
catch(MyException myex)
{
// 捕捉自訂例外時執行的程式碼
// ...
}
catch(Exception ex)
{
// 捕捉其他例外時執行的程式碼
// ...
}
finally
{
// 不論 try 或 catch 的結果如何,最後都會嘗試執行的程式碼
// ...
}

我們可以在 catch 中指定要捕捉的例外類型。下面整理幾個常見的例外類型:

例外類型 說明
System.IO.IOException 處理 I/O 相關錯誤。
System.IndexOutOfRangeException 處理索引超出範圍的錯誤。
System.ArrayTypeMismatchException 處理陣列型別不匹配的錯誤。
System.NullReferenceException 處理操作 null 物件時發生的錯誤。
System.DivideByZeroException 處理除以 0 的錯誤。
System.InvalidCastException 處理型別轉換失敗的錯誤。
System.OutOfMemoryException 處理記憶體不足時發生的錯誤。
System.StackOverflowException 處理執行堆疊溢位錯誤。

以上只是幾個常見的例外類型。有些人也會省略例外參數,直接寫成 catch { … }

當然,也可以自訂例外類別,讓程式針對特定情境做更準確、也更符合需求的處理。

1
2
3
4
5
// 記得要繼承 Exception 類別,才能在 catch 中使用
public class MyException : Exception
{
// ...
}

至於 finally { … },它代表的是不論 try 或 catch 的結果如何,最後都會嘗試執行的區段。常見用途像是釋放資源、關閉連線、清理暫存狀態等等。

整理這些內容,一方面是幫自己複習;另一方面,也希望讓剛開始寫程式的新手,在遇到例外狀況時,可以更清楚知道自己正在處理什麼。

複習考題

既然已經看過 try & catch 的基本用法,接著就用幾題小測驗確認一下觀念吧。

問題 1:請問輸出到命令視窗的 c 會是多少?

A: 3
B: 12
C: 123
D: 6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Program.cs
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine(tt.QnA());
}
}

// tt.QnA
public static string QnA()
{
string c = "";
try
{
Employee emp = null;
c += "1";
emp.getEmp();
}
catch (Exception ex)
{
c += "2";
return c;
}
finally
{
c += "3";
}
return c;
}

答案:B

如果第一次就答對,代表你真的有注意到這題的小細節

但如果你回答 AD,可能要先回頭看清楚 c 是什麼型別啦!

解答

這題有一個容易混淆的地方。

因為 finally 裡的程式碼不管如何都會嘗試被執行,所以 c 在流程中確實會被加到 "123"

但是,在 catch { … } 裡已經先執行了 return c

因此,真正回傳出去的結果會是 catch 當下準備回傳的值,再加上 finally 執行前後對回傳流程的影響。以這段程式來說,命令視窗最後輸出的結果是 "12"

你答對了嗎~

覺得答錯不甘心,想再試一次嗎?沒問題,那我再出一題!

問題 2:請問 c 的值是多少?

這次題目可要看清楚唷

A: 1245
B: 12345
C: 134
D: 1234

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Program.cs
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine(tt.QnA2());
}
}

// tt.QnA2
public static string QnA2()
{
string c = "";
try
{
Employee emp = null;
c += "1";
emp.getEmp();
}
catch (NullReferenceException ex)
{
c += "2";
}
catch(Exception ex2)
{
c += "3";
return c;
}
finally
{
c += "4";
}
c += "5";
return c;
}

答案:A

這次你答對了嗎?

為什麼會是 A 呢?因為:

當例外已經被某一個 catch { … } 捕捉並處理後,後面的其他 catch 就不會再被執行。

這題拋出的例外是 NullReferenceException,所以程式會進入第一個 catch(NullReferenceException ex) { … }

後面的 catch(Exception ex) { … } 會被跳過。

接著,因為第一個 catch 裡沒有 return,程式會繼續執行 finally,最後再往下執行 c += "5"return c

所以最終答案就是:

不論是字串 c 的內容,還是命令視窗輸出的結果,都會是 1245

結語

  • 當程式有可能發生不可預期的錯誤時,可以利用 try & catch 避免程式直接崩潰。
  • 一組完整的 try & catch 中,try 只能有一個,catch 則可以有多個;但只要例外被其中一個 catch 接住,後面的 catch 就不會再執行。
  • finally { … } 是不論 try { … }catch { … } 結果如何,都會嘗試執行的區段。不過,如果在進入 finally 前遇到 returnthrow,就要特別留意流程與回傳值的變化。

以上淺談,希望能對你有所幫助