Day 5 - 异常处理
建议用时:180-210 分钟
你将学会什么
- 异常是什么,和普通错误提示有什么区别
try / catch / finally分别负责什么- 为什么用户输入错误优先用
TryParse,不是直接靠异常 - 常见异常类型:
FormatException、FileNotFoundException、IndexOutOfRangeException、ArgumentException throw是什么,什么时候主动抛异常- 为什么不能写空
catch - 如何把错误提示写得清楚
- 如何写一个安全读取文件的小程序
异常处理不是把错误藏起来。它的目标是:程序失败时不失控,能说明原因,并在合适的位置停止或恢复。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“泛型”,今天继续学习“异常处理”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“异常处理”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分先解决“这是什么、为什么要这样写”。每看到一个短例子就亲手敲一遍并运行,不要先背完整程序。
今天先抓住什么
程序失败大概分三类:
输入错误:用户输入 abc 当数量
业务错误:价格不能小于 0
系统错误:文件不存在、文件没有权限、网络失败处理方式也不同:
| 错误类型 | 优先处理方式 |
|---|---|
| 输入错误 | TryParse、空值判断 |
| 业务错误 | 明确判断,必要时 throw |
| 系统错误 | try/catch 捕获并提示 |
名词先讲清楚
| 名词 | 直接解释 | 例子 |
|---|---|---|
| 异常 | 程序无法按正常路线继续时产生的错误对象 | 文件不存在 |
try | 放可能失败的代码 | 读取文件 |
catch | 捕获并处理异常 | 提示“文件不存在” |
finally | 无论成功失败都会执行 | 收尾、释放资源 |
throw | 主动抛出异常 | 参数不合法 |
Exception | 所有异常的基础类型 | catch (Exception ex) |
ex.Message | 异常里的错误信息 | 输出失败原因 |
异常和普通判断有什么区别
普通判断适合预期内的情况:
Console.Write("年龄: ");
string? input = Console.ReadLine();
if (!int.TryParse(input, out int age))
{
Console.WriteLine("年龄格式错误");
return;
}
Console.WriteLine(age);用户输入错很常见,所以用判断处理。
异常适合正常流程无法继续的情况:
try
{
string text = File.ReadAllText("data.txt");
Console.WriteLine(text);
}
catch (FileNotFoundException)
{
Console.WriteLine("文件不存在");
}文件不一定存在,读取可能失败,所以用 try/catch 保护。
try/catch/finally 怎么执行
try
{
Console.WriteLine("开始");
int number = int.Parse("abc");
Console.WriteLine(number);
}
catch (FormatException)
{
Console.WriteLine("格式错误");
}
finally
{
Console.WriteLine("结束");
}输出:
开始
格式错误
结束int.Parse("abc") 失败后,后面的 Console.WriteLine(number) 不会执行,程序会跳到 catch。
常见异常类型
| 异常类型 | 常见原因 |
|---|---|
FormatException | 字符串格式不对,例如 int.Parse("abc") |
FileNotFoundException | 文件不存在 |
IndexOutOfRangeException | 数组下标越界 |
ArgumentException | 参数不符合要求 |
InvalidOperationException | 当前状态不允许执行这个操作 |
异常处理常用写法速查
| 场景 | 推荐写法 | 原因 |
|---|---|---|
| 用户输入数字 | TryParse | 输入错误很常见,不需要异常控制流程 |
| 必填文本为空 | IsNullOrWhiteSpace | 直接判断更清楚 |
| 参数违反业务规则 | throw new ArgumentException(...) | 调用方传错参数 |
| 文件不存在 | catch (FileNotFoundException) | 给出明确文件提示 |
| 其他文件错误 | catch (IOException ex) | 文件占用、权限等 |
| 下标可能越界 | 先判断范围 | 比捕获越界异常更直接 |
| 必须收尾 | finally | 无论成功失败都执行 |
| 兜底记录 | catch (Exception ex) | 放最后,避免程序失控 |
模板 1:输入转换不用异常
Console.Write("数量: ");
string? input = Console.ReadLine();
if (!int.TryParse(input, out int count))
{
Console.WriteLine("数量必须是整数");
return;
}模板 2:业务规则主动抛异常
void ChangePrice(decimal price)
{
if (price <= 0)
{
throw new ArgumentException("价格必须大于 0");
}
Console.WriteLine($"新价格: {price:F2}");
}模板 3:系统操作用 try/catch
try
{
string text = File.ReadAllText("data.txt");
Console.WriteLine(text);
}
catch (FileNotFoundException)
{
Console.WriteLine("文件不存在");
}
catch (IOException ex)
{
Console.WriteLine($"文件读取失败: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"未知错误: {ex.Message}");
}
finally
{
Console.WriteLine("读取流程结束");
}模板 4:下标访问先判断范围
var names = new List<string> { "Keyboard", "Mouse" };
int index = 2;
if (index < 0 || index >= names.Count)
{
Console.WriteLine("下标超出范围");
return;
}
Console.WriteLine(names[index]);能先判断的错误,就先判断。try/catch 主要保护文件、网络、数据库这类外部操作。
为什么不要空 catch
不要这样写:
try
{
string text = File.ReadAllText("data.txt");
Console.WriteLine(text);
}
catch
{
}这会把错误吞掉。程序表面没报错,但你也不知道发生了什么。
至少要提示或记录:
try
{
string text = File.ReadAllText("data.txt");
Console.WriteLine(text);
}
catch (Exception ex)
{
Console.WriteLine($"读取失败: {ex.Message}");
}throw 是主动抛出异常
当方法收到不合理参数,并且无法继续执行时,可以主动 throw。
try
{
void ChangePrice(decimal price)
{
if (price <= 0)
{
throw new ArgumentException("价格必须大于 0");
}
Console.WriteLine($"新价格: {price:F2}");
}
ChangePrice(-1m);
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
输出:
价格必须大于 0catch 顺序要从具体到宽泛
先捕获具体异常,再捕获 Exception。
try
{
string text = File.ReadAllText("data.txt");
Console.WriteLine(text);
}
catch (FileNotFoundException)
{
Console.WriteLine("文件不存在");
}
catch (Exception ex)
{
Console.WriteLine($"其他错误: {ex.Message}");
}如果把 catch (Exception) 放前面,后面的具体异常就没有机会执行。
今天写异常处理时按这个顺序
- 先判断这是输入错误、业务错误还是系统错误。
- 输入错误优先用
TryParse、空值判断。 - 业务错误优先明确判断,必要时
throw。 - 系统错误用
try/catch。 catch里必须给提示或记录信息。- 需要收尾时写
finally。 - 不要用异常代替所有判断。
第二部分:把知识组合成完整例子
前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个安全读取文件的程序:先判断文件名,再尝试读取,失败时给出明确提示。
最终你要写出这个程序
Console.Write("请输入文件名: ");
string? fileName = Console.ReadLine();
try
{
if (string.IsNullOrWhiteSpace(fileName))
{
throw new ArgumentException("文件名不能为空");
}
string text = File.ReadAllText(fileName);
Console.WriteLine("文件内容:");
Console.WriteLine(text);
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
catch (FileNotFoundException)
{
Console.WriteLine("文件不存在");
}
catch (Exception ex)
{
Console.WriteLine($"读取失败: {ex.Message}");
}
finally
{
Console.WriteLine("读取流程结束");
}输入空内容:
请输入文件名:
文件名不能为空
读取流程结束输入不存在的文件:
请输入文件名: data.txt
文件不存在
读取流程结束这个例子里,try 负责执行可能失败的读取,catch 负责处理失败,finally 负责收尾。
第三部分:跟着敲代码
从这里开始动手。每个例子都可以直接放进 Program.cs 运行。
例子 1:输入错误用 TryParse
Console.Write("数量: ");
string? input = Console.ReadLine();
if (!int.TryParse(input, out int count))
{
Console.WriteLine("数量格式不正确");
return;
}
Console.WriteLine($"数量是 {count}");输入 abc 时,程序不会崩溃,会输出“数量格式不正确”。
例子 2:用 try/catch 捕获格式异常
Console.Write("数量: ");
string? input = Console.ReadLine();
try
{
int count = int.Parse(input ?? "");
Console.WriteLine($"数量是 {count}");
}
catch (FormatException)
{
Console.WriteLine("数量必须是整数");
}这个例子能运行,但用户输入这种场景更推荐例子 1 的 TryParse。
例子 3:读取文件失败
try
{
string text = File.ReadAllText("data.txt");
Console.WriteLine(text);
}
catch (FileNotFoundException)
{
Console.WriteLine("文件不存在");
}
catch (Exception ex)
{
Console.WriteLine($"读取失败: {ex.Message}");
}如果当前目录没有 data.txt,会输出“文件不存在”。
例子 4:finally 一定会执行
try
{
Console.WriteLine("开始读取");
string text = File.ReadAllText("missing.txt");
Console.WriteLine(text);
}
catch (FileNotFoundException)
{
Console.WriteLine("文件不存在");
}
finally
{
Console.WriteLine("读取流程结束");
}无论读取成功还是失败,finally 都会执行。
例子 5:业务规则主动 throw
try
{
decimal CalculateTotal(decimal price, int count)
{
if (price < 0)
{
throw new ArgumentException("单价不能小于 0");
}
if (count <= 0)
{
throw new ArgumentException("数量必须大于 0");
}
return price * count;
}
decimal total = CalculateTotal(100m, 0);
Console.WriteLine(total);
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
输出:
数量必须大于 0例子 6:先校验,再捕获意外错误
Console.Write("请输入下标: ");
string? input = Console.ReadLine();
if (!int.TryParse(input, out int index))
{
Console.WriteLine("下标格式不正确");
return;
}
try
{
int[] numbers = { 10, 20, 30 };
Console.WriteLine(numbers[index]);
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("下标超出了数组范围");
}格式问题用 TryParse,越界问题用 try/catch 保护。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 用异常处理普通输入错误 | 输入错误很常见,不应该靠异常控制流程 | 用 TryParse 和条件判断 |
catch 捕获后什么都不做 | 错误原因丢失 | 至少输出或记录 ex.Message |
| 捕获范围太大 | 不同错误混在一起 | 先捕获具体异常,再兜底捕获 Exception |
| 抛异常没有消息 | 调用方不知道规则是什么 | throw new ArgumentException("价格必须大于 0") |
| 异常后资源不收尾 | 文件或状态可能残留 | 用 finally 做清理 |
小白重复敲写训练
异常处理必须故意制造错误来练,不能只看正常结果。
训练 1:捕获数字格式错误
try
{
Console.Write("请输入整数: ");
int number = int.Parse(Console.ReadLine()!);
Console.WriteLine(number * 2);
}
catch (FormatException)
{
Console.WriteLine("输入的不是整数");
}分别输入 12 和 abc。
训练 2:捕获除以零
try
{
int a = 10;
int b = 0;
Console.WriteLine(a / b);
}
catch (DivideByZeroException)
{
Console.WriteLine("除数不能是 0");
}改动任务:把 b 改成 2,确认 catch 不再执行。
训练 3:观察 finally
try
{
Console.WriteLine("开始处理");
throw new InvalidOperationException("模拟失败");
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
finally
{
Console.WriteLine("无论成功失败都执行清理");
}第二遍删除 throw,观察 finally 是否还执行。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:异常处理。今天至少要掌握的 3 个点是:
1. 异常是什么,和普通错误提示有什么区别
2. `try / catch / finally` 分别负责什么
3. 为什么用户输入错误优先用 `TryParse`,不是直接靠异常答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:异常是什么,和普通错误提示有什么区别。
上位机专项练习
通信配置、文件读取和设备连接都可能失败。异常处理要让程序给出明确提示,而不是直接崩溃。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:处理错误端口号
try
{
int port = int.Parse("abc");
Console.WriteLine(port);
}
catch (FormatException)
{
Console.WriteLine("端口号必须是整数");
}运行结果或界面效果:
端口号必须是整数改动任务: 把 abc 改成 502,确认不会进入 catch。
专项例子 2:主动拒绝非法采集周期
void ValidateInterval(int interval)
{
if (interval <= 0)
{
throw new ArgumentException("采集周期必须大于 0");
}
}
try
{
ValidateInterval(0);
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}运行结果或界面效果:
采集周期必须大于 0改动任务: 分别测试 -1 和 1000。
专项例子 3:finally 保证执行清理
try
{
Console.WriteLine("开始读取设备");
throw new TimeoutException("设备响应超时");
}
catch (TimeoutException ex)
{
Console.WriteLine(ex.Message);
}
finally
{
Console.WriteLine("释放通信资源");
}运行结果或界面效果:
开始读取设备
设备响应超时
释放通信资源改动任务: 删除 throw,确认 finally 仍然执行。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。
作业要求
写一个安全读取文件并统计行数的程序,要求:
- 输入文件名。
- 文件名不能为空。
- 文件不存在时提示“文件不存在”。
- 读取成功时输出文件行数。
- 其他读取错误输出错误信息。
- 无论成功失败,都输出“读取流程结束”。
作业拆解
按这个顺序写:
- 读取文件名。
- 用空值判断处理输入错误。
- 在
try中读取文件。 - 用
File.ReadAllLines得到所有行。 - 捕获
FileNotFoundException。 - 捕获
Exception处理其他错误。 - 在
finally中输出收尾信息。
完整代码
Console.Write("请输入文件名: ");
string? fileName = Console.ReadLine();
try
{
if (string.IsNullOrWhiteSpace(fileName))
{
throw new ArgumentException("文件名不能为空");
}
string[] lines = File.ReadAllLines(fileName);
Console.WriteLine($"文件行数: {lines.Length}");
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
catch (FileNotFoundException)
{
Console.WriteLine("文件不存在");
}
catch (Exception ex)
{
Console.WriteLine($"读取失败: {ex.Message}");
}
finally
{
Console.WriteLine("读取流程结束");
}正确运行结果
空文件名:
请输入文件名:
文件名不能为空
读取流程结束不存在的文件:
请输入文件名: missing.txt
文件不存在
读取流程结束存在的文件:
请输入文件名: data.txt
文件行数: 3
读取流程结束你要能说清楚这 6 件事
- 输入错误优先用判断或
TryParse。 try放可能失败的代码。catch处理已经发生的异常。finally无论成功失败都会执行。throw是主动抛出异常。- 空
catch会隐藏问题,不应该写。