第 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);
    }
    // ……存起来
}

早抛,消息写清楚——另一个选项(默默存下胡说八道)会变成别人凌晨两点的调试现场。

现在就练