1. 入门
当你的程序比Hello, world!稍微复杂点时,你很快就会遇到这样的问题:不同类型的错误要用不同的方式处理。想象一下:你在操作文件、网络、数据库——每种出错的情况都得有不同的反应。比如,文件没找到可以让用户选别的文件,网络出错就可以重试或者弹个提示“检查下网线,是不是猫又把它咬断了”。
在C#里,这种情况就用多个catch块连着写。每个catch块专门处理自己那种异常(包括它的子类)。
多重catch的结构
try
{
// 这里是可能抛出各种异常的代码
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"文件没找到: {ex.Message}");
}
catch (IOException ex) // 捕获所有输入/输出错误,除了FileNotFoundException
{
Console.WriteLine($"输入输出错误: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"出问题了: {ex.Message}");
}
注意!编译器会从上往下依次检查catch块,遇到第一个类型匹配的就用它。如果碰到FileNotFoundException,后面的catch就不会再走了。
“链条”示意图
| 异常类型 | 会被哪个catch块抓住? |
|---|---|
| FileNotFoundException | 第一个catch |
| IOException(其他) | 第二个catch |
| ArgumentException | 第三个(通用)catch |
为什么不能混着来?
最“宽泛”的catch——比如catch (Exception)——一定要放最后,否则它会提前“吞掉”所有异常,后面的专用catch根本用不上。
这就像大楼里所有烟雾报警器都关掉了,只因为有一个报警器被烤面包机触发了:后面真着火了系统也发现不了。
实用例子
using System;
using System.IO;
class Program
{
static void Main()
{
try
{
double result = CalculateAverageAgeFromFile("users.txt");
Console.WriteLine($"平均年龄 — {result}");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("错误:文件没找到。请检查路径。");
}
catch (FormatException ex)
{
Console.WriteLine("数据错误:无法读取年龄。");
}
catch (Exception ex)
{
Console.WriteLine($"其他错误: {ex.Message}");
}
}
static double CalculateAverageAgeFromFile(string filePath)
{
// (实现:读取文件,解析年龄,计算平均值)
// ...
throw new NotImplementedException();
}
}
这里我们明确区分了:如果文件不存在(FileNotFoundException)怎么处理,如果文件里数据“有问题”(FormatException)又怎么处理。剩下的情况都交给第三个“兜底”块。
2. catch过滤器:捕捉细节
多个catch块很方便,但有时候还不够。有些情况下,同一种异常类型你还想根据具体情况做不同反应。
比如,网络不通如果是因为没流量了——可以试试重连。如果是服务器没响应——那就该弹别的提示了。
这时候就轮到catch过滤器登场了——C#的超级功能,可以不仅按类型,还能加条件来捕获异常。
when过滤器语法
catch (IOException ex) when (ex.Message.Contains("没有磁盘"))
{
Console.WriteLine("哎呀!看起来你把U盘拔了。");
}
catch (IOException ex)
{
Console.WriteLine("其他输入输出错误: " + ex.Message);
}
这里第一个catch块只会抓到那些IOException,而且消息里包含"没有磁盘"的——其他的都进第二个块。
实际应用
过滤器在处理网络错误时特别有用,比如你要根据异常的内部属性决定:要不要重试,还是直接报错。
再举个例子:假如我们有个解析文件的方法,想要单独处理“年龄”字段出错(比如年龄写成了"abc"而不是数字),而不是所有FormatException都一样处理。
catch (FormatException ex) when (ex.Message.Contains("年龄"))
{
Console.WriteLine("错误:无法读取年龄。请检查数据。");
}
catch (FormatException ex)
{
Console.WriteLine("数据格式错误: " + ex.Message);
}
过滤器和性能
过滤器很方便,但要记住,过滤器里的条件是在进入catch块之前计算的,如果条件不成立,catch块里的代码根本不会执行。
顺便说下,如果过滤器表达式里自己抛了异常(比如when里除以零),这个异常当前catch块永远不会捕获。要小心点。
3. 多个catch和过滤器一起用
想象一下,你的项目里不仅要按异常类型处理,还要看异常的内部状态。比如处理IOException时,如果是磁盘没空间和没有权限要分别处理。
try
{
File.AppendAllText("log.txt", "新日志\n");
}
catch (IOException ex) when (ex.Message.Contains("没有空间"))
{
Console.WriteLine("错误:磁盘空间用完了!");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("错误:没有写文件的权限。请用管理员身份运行。");
}
catch (IOException ex)
{
Console.WriteLine("其他磁盘错误: " + ex.Message);
}
这里我们既用了过滤器,也按错误类型分开处理。做复杂应用时,这种灵活性特别有用,可以更友好地应对各种常见故障。
4. 过滤器和多重catch的特点和“坑”
- catch过滤器可以用异常变量(ex),但不能修改它。
- 如果你在when里抛异常,这个catch块是不会捕获它的,异常会继续往上冒,就像过滤器不存在一样。
- 过滤器逻辑成了代码契约的一部分:你的同事要知道,并不是所有IOException都会被抓住——只有条件成立才行。
- 如果你整个方法只写了一个catch,但里面加了过滤器,有可能“多余”的异常根本没被捕获。所以不确定时,还是建议写多个明确的catch块。
- 在大型应用里,可以用过滤器做集中式逻辑,比如只记录严重错误的日志。
5. 面试和实际工作场景
会用过滤器和多重catch,不仅仅是为了代码漂亮或者clean code高分。这是真正的技能,面试时尤其是做大型、复杂、分布式应用的岗位,面试官很可能会问到。
常见面试题:
- 在C#里怎么处理文件读取的不同错误场景,让不同情况(没文件、磁盘满、数据坏了)显示不同提示?
- 如果用了通用catch (Exception),怎么避免“吞掉”重要错误?
- catch过滤器catch是干嘛用的?里面能不能有throw?
GO TO FULL VERSION