⚠️ 例外處理與除錯
📌 什麼是例外(Exception)?
例外就像開車時遇到的「路障」。如果你沒有準備好應對方案,程式就會直接撞上去然後崩潰。例外處理就是幫你設計繞路方案。
📌 try / catch / finally
try // 嘗試執行可能出錯的程式碼(像是試著過馬路)
{
// 嘗試把字串轉成數字
string input = "abc";
int number = int.Parse(input); // 💥 這行會出錯,因為 "abc" 不是數字
Console.WriteLine(number); // 這行不會執行
}
catch (FormatException ex) // 捕捉「格式錯誤」的例外
{
// 印出錯誤訊息,告訴使用者輸入格式不對
Console.WriteLine($"格式錯誤:{ex.Message}");
}
catch (OverflowException ex) // 捕捉「數字溢位」的例外
{
// 數字太大或太小時會觸發
Console.WriteLine($"數字超出範圍:{ex.Message}");
}
finally // 不管有沒有出錯,這裡的程式碼都會執行
{
// 通常用來釋放資源(關閉檔案、資料庫連線等)
Console.WriteLine("不管成功失敗,我都會執行");
}
📌 多重 catch 與 when 子句
try // 嘗試執行程式碼
{
// 模擬根據錯誤碼拋出不同例外
int errorCode = 404;
throw new HttpRequestException($"HTTP 錯誤:{errorCode}"); // 手動拋出例外
}
catch (HttpRequestException ex) when (ex.Message.Contains("404")) // 只捕捉包含 404 的例外
{
// 處理找不到頁面的情況
Console.WriteLine("頁面不存在(404)");
}
catch (HttpRequestException ex) when (ex.Message.Contains("500")) // 只捕捉包含 500 的例外
{
// 處理伺服器錯誤的情況
Console.WriteLine("伺服器錯誤(500)");
}
catch (Exception ex) // 捕捉所有其他例外(最通用的放最後)
{
// 處理未預期的錯誤
Console.WriteLine($"未預期的錯誤:{ex.Message}");
}
重點: when 子句讓你可以對同一種例外類型做更細緻的分類處理,就像醫院的分級診療。
📌 Exception 階層架構
// Exception 的繼承關係(像家族樹)
// Exception ← 所有例外的祖先
// ├── SystemException ← 系統層級例外
// │ ├── NullReferenceException ← 物件為 null 時存取其成員
// │ ├── IndexOutOfRangeException ← 陣列索引超出範圍
// │ ├── InvalidOperationException ← 操作在目前狀態下無效
// │ └── ArgumentException ← 傳入的參數不合法
// │ └── ArgumentNullException ← 參數為 null
// └── ApplicationException ← 應用程式層級例外(較少用)
📌 自訂例外
// 建立自訂例外類別(繼承 Exception)
public class InsufficientBalanceException : Exception // 餘額不足例外
{
// 目前餘額
public decimal CurrentBalance { get; }
// 嘗試提領的金額
public decimal WithdrawAmount { get; }
// 建構函式,接收餘額和提領金額
public InsufficientBalanceException(decimal balance, decimal amount)
: base($"餘額不足!目前餘額:{balance},嘗試提領:{amount}") // 呼叫父類建構函式設定訊息
{
CurrentBalance = balance; // 儲存目前餘額
WithdrawAmount = amount; // 儲存提領金額
}
}
// 使用自訂例外
public class BankAccount // 銀行帳戶類別
{
// 帳戶餘額
public decimal Balance { get; private set; } = 1000m;
// 提款方法
public void Withdraw(decimal amount)
{
if (amount > Balance) // 如果提領金額超過餘額
{
// 拋出自訂例外,附帶餘額和金額資訊
throw new InsufficientBalanceException(Balance, amount);
}
Balance -= amount; // 扣除餘額
}
}
📌 throw vs throw ex 的差異
// ❌ 使用 throw ex — 會遺失原始堆疊追蹤
try
{
// 呼叫某個可能出錯的方法
SomeMethod();
}
catch (Exception ex) // 捕捉到例外
{
// 記錄錯誤日誌
Console.WriteLine(ex.Message);
throw ex; // ❌ 堆疊追蹤會從這裡重新開始,原始錯誤位置遺失
}
// ✅ 使用 throw — 保留完整的堆疊追蹤
try
{
// 呼叫某個可能出錯的方法
SomeMethod();
}
catch (Exception ex) // 捕捉到例外
{
// 記錄錯誤日誌
Console.WriteLine(ex.Message);
throw; // ✅ 保留原始堆疊追蹤,可以追溯到真正出錯的地方
}
比喻: throw ex 就像把犯罪現場的指紋擦掉再報警,警察就找不到原始線索了。throw 則是完整保留犯罪現場。
📌 除錯技巧
中斷點(Breakpoint)
在 Visual Studio 中,點擊程式碼左邊的灰色區域即可設定中斷點。程式執行到該行時會暫停,讓你檢查變數的值。
監看式(Watch)
在中斷點暫停時,可以把變數加入「監看式」視窗,即時觀察變數的變化。
即時運算視窗(Immediate Window)
在除錯暫停時,可以在即時運算視窗輸入 C# 運算式來測試:
// 在 Immediate Window 中可以這樣輸入:
// ? myVariable ← 查看變數的值
// ? myList.Count ← 查看集合的元素數量
// ? myObject.ToString() ← 呼叫物件的方法
// myVariable = 42 ← 即時修改變數的值(測試用)
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:捕捉範圍太廣
// ❌ 錯誤寫法:捕捉所有 Exception,吞掉所有錯誤
try
{
// 執行某個操作
ProcessData();
}
catch (Exception) // 捕捉所有例外,但什麼都不做
{
// 空的 catch 區塊!錯誤被默默吞掉,你永遠不知道出了什麼問題
}
// ✅ 正確寫法:捕捉具體的例外,並適當處理
try
{
// 執行某個操作
ProcessData();
}
catch (FileNotFoundException ex) // 只捕捉檔案不存在的例外
{
// 記錄錯誤並通知使用者
Console.WriteLine($"找不到檔案:{ex.FileName}");
}
catch (Exception ex) // 其他未預期的例外
{
// 至少要記錄錯誤日誌
Console.WriteLine($"未預期錯誤:{ex}");
throw; // 重新拋出,讓上層處理
}
解釋: 空的 catch 區塊就像把所有警報都關掉,表面上一切正常,但問題其實在暗處惡化。
❌ 錯誤 2:throw new Exception() 包裝錯誤
// ❌ 錯誤寫法:用新的 Exception 包裝,遺失原始資訊
try
{
// 執行可能出錯的操作
ConnectToDatabase();
}
catch (Exception ex) // 捕捉到例外
{
// 建立全新的例外,原始的例外類型和堆疊追蹤全部遺失
throw new Exception("連線失敗"); // ❌ 遺失原始例外的所有資訊
}
// ✅ 正確寫法:用 inner exception 保留原始例外
try
{
// 執行可能出錯的操作
ConnectToDatabase();
}
catch (Exception ex) // 捕捉到例外
{
// 建立新例外時,把原始例外作為 inner exception 傳入
throw new InvalidOperationException("資料庫連線失敗", ex); // ✅ 保留原始例外
}
解釋: 就像轉述別人的話時,不只說結論,也要說明原始來源,這樣才能追溯問題根源。
❌ 錯誤 3:在 finally 中 return
// ❌ 錯誤寫法:在 finally 中使用 return
static int GetValue()
{
try
{
return 1; // 嘗試回傳 1
}
finally
{
return 2; // ❌ finally 中的 return 會覆蓋 try 中的 return(C# 不允許此寫法)
}
}
// ✅ 正確寫法:finally 只做清理工作
static int GetValue()
{
int result = 0; // 宣告結果變數
try
{
result = 1; // 設定結果
return result; // 回傳結果
}
finally
{
// finally 只做清理,不要 return
Console.WriteLine("清理完成"); // 釋放資源等操作
}
}
解釋: finally 的職責是清理資源(像是打掃戰場),不應該用來改變程式的回傳值。