第 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;接住它并打印消息。 - 然后读报错解码器里的日常失败——它们是这里一切东西的编译期表亲。