第 9 課 · 約 12 分鐘 · ← 第 8 課
異常:執行時出問題怎麼辦
編譯錯誤在釋出前攔你;異常是執行中才冒頭的問題——壞輸入、不存在的鍵、除以零。C# 的答案:try 包住有風險的部分,catch 接住失敗,finally 負責清理。
try / catch:把爆炸圈住
string[] inputs = { "42", "abc" };
foreach (string s in inputs)
{
try
{
Console.WriteLine("parsed: " + int.Parse(s));
}
catch (FormatException) // 按型別接——只接這一種失敗
{
Console.WriteLine("not a number: " + s);
}
}
沒有 try/catch,第二個 int.Parse 會殺掉整個程式。有了它,失敗被圈進一輪迴圈,程式繼續跑。儘量接最具體的型別——catch (Exception) 什麼都能接,也常常把 bug 一起藏了。
異常物件:出了什麼事,白紙黑字
int Divide(int a, int b)
{
if (b == 0)
{
throw new Exception("division by zero is undefined"); // 自己拋
}
return a / b;
}
try
{
Console.WriteLine(Divide(10, 2));
Console.WriteLine(Divide(1, 0));
}
catch (Exception e) // e 就是那個異常——讀它的 Message
{
Console.WriteLine("caught: " + e.Message);
}
finally
{
Console.WriteLine("always runs"); // 成功失敗都跑——清理程式碼放這
}
finally 無論如何都執行:成功、被接住的失敗、沒人接的異常。關檔案、關連線的程式碼住這裡——引擎裡沒有檔案,但形狀是通用的。
型別是一個家族——接你想接的那支
Exception // 根——所有異常都是它
└── SystemException
├── FormatException // int.Parse("abc")
├── NullReferenceException // 用了 null 物件
├── IndexOutOfRangeException // 陣列下標越界
├── DivideByZeroException // 整數除以零
└── ArgumentException // 你傳了無效的東西
└── ArgumentNullException // ……具體地:傳了 null
接住父類等於接住它所有子類:catch (Exception e) 全接;catch (SystemException e) 接系統家族。多個 catch 塊按最具體在前排序——第一個命中者贏。
能避免的就別 try/catch
地道的分工:異常留給異常性的問題,TryParse 留給預期內的失敗:
string[] inputs = { "42", "xyz" };
foreach (string s in inputs)
{
if (int.TryParse(s, out int n)) // 失敗是正常分支
{
Console.WriteLine("got " + n);
}
else
{
Console.WriteLine("ignored " + s);
}
}
經驗法則:"失敗"是你邏輯裡的正常分支(使用者輸入!)就用 Try 系方法;失敗意味著真的出了毛病,讓異常飛,在能真正處理它的那一層接住。
拋自己的異常
當你的方法收到了無法工作的引數,C# 慣例是拋 ArgumentException(或其更具體的子類),訊息說清哪裡不對:
void SetAge(int age)
{
if (age < 0)
{
throw new ArgumentException("age cannot be negative: " + age);
}
// ……存起來
}
早拋,訊息寫清楚——另一個選項(默默存下胡說八道)會變成別人凌晨兩點的除錯現場。
現在就練
- 把
int.Parse(Console.ReadLine())包進 try/catch;迴圈直到輸入(或輸入框)給出合法數字。 - 在
SetScore(int)裡對負數拋ArgumentException;接住它並列印訊息。 - 然後讀報錯解碼器裡的日常失敗——它們是這裡一切東西的編譯期表親。