當我們不想要採用系統預設的例外狀況顯示訊息時,可以透過throw的方式來達到客製化顯示例外訊息的目的。
throw:拋出例外
當你在程式的某處需要引發例外來中斷正常流程時,便可以使用 throw 來拋出一個例外。
1
2
3
4
5
6
7
8
void Print(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
Console.WriteLine(name);
}
Print 方法會先檢查參數 name 是否為 null,如果是的話,便拋出一個 ArgumentNullException 類型的例外,讓呼叫端知道此函式堅決不接受傳入的參數為 null。
第 3~6 行程式碼也可以用一行解決:ArgumentNullException.ThrowIfNull(name);。
順便提及,當你需要拋出 NullReferenceException,除了用 throw new NullReferenceException(),也可以這樣寫:
1
throw null;
再度拋出例外
你也可以在 catch 區塊裡面使用 throw 來將目前的例外再度拋出:
1
2
3
4
5
6
7
8
9
try
{
DoSomething();
}
catch (Exception ex)
{
Log(ex); // 把當前的例外資訊寫入 log。
throw; // 再次拋出同一個例外。
}
這裡有個細節值得一提:在上述範例中,如果把倒數第二行寫成 throw ex 也可以通過編譯,但其作用稍微不同:
- 單單寫
throw會保留原本的堆疊追蹤資訊(stack trace)。也就是說,透過堆疊追蹤資訊,便能抓到原始的例外。 - 如果寫
throw ex,則會重設堆疊追蹤資訊,這將導致呼叫端無法透過例外物件的StackTrace屬性得知引發例外的源頭是哪一行程式碼。基於此緣故,通常不建議採用此寫法。
呼叫堆疊與堆疊追蹤
堆疊追蹤(
stack trace)是一個字串,裡面包含了當前呼叫堆疊(call stack)裡面的所有方法的名稱,以及方法所在的程式碼行號——如果編譯時有啟用除錯資訊的話。
呼叫堆疊指的是一個記憶體區塊,其中保存了某一條執行緒當時的所有方法呼叫的資訊,例如傳入方法的參數、方法的區域變數等等。
於 catch 區塊中再次拋出例外時,你也可以拋出一個新的、不同類型的例外:
1
2
3
4
5
6
7
8
9
10
11
DateTime StringToDate(string input)
{
try
{
return Convert.ToDateTime(input);
}
catch (FormatException ex)
{
throw new ArgumentException($"無效的引數: {nameof(input)}", ex);
}
}
請注意上述範例在拋出一個新的 ArgumentException 時,有把當前的例外 ex 傳入建構子的第二個參數,這會把當前的例外保存於新例外的 InnerException 屬性。也就是說,在拋出新例外的同時,依然保存原始的例外資訊(也許呼叫端在診斷錯誤的時候會用到)。
一般來說,以下幾種場合會在 catch 區塊中拋出不同的例外:
- 在處理當前的例外時,又發生了其他意外狀況。
- 讓外層接收到更一般的例外類型,以便隱藏底層的細節(不想讓外界知道太多、避免駭客攻擊)。
- 讓外層接收到更特殊的例外類型,以便呼叫端更明確知道發生錯誤的原因。