Skip to Content
Week 02Day 5 - 异常处理

Day 5 - 异常处理

建议用时:180-210 分钟

你将学会什么

  • 异常是什么,和普通错误提示有什么区别
  • try / catch / finally 分别负责什么
  • 为什么用户输入错误优先用 TryParse,不是直接靠异常
  • 常见异常类型:FormatExceptionFileNotFoundExceptionIndexOutOfRangeExceptionArgumentException
  • throw 是什么,什么时候主动抛异常
  • 为什么不能写空 catch
  • 如何把错误提示写得清楚
  • 如何写一个安全读取文件的小程序

异常处理不是把错误藏起来。它的目标是:程序失败时不失控,能说明原因,并在合适的位置停止或恢复。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“泛型”,今天继续学习“异常处理”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 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); }

输出:

价格必须大于 0

catch 顺序要从具体到宽泛

先捕获具体异常,再捕获 Exception

try { string text = File.ReadAllText("data.txt"); Console.WriteLine(text); } catch (FileNotFoundException) { Console.WriteLine("文件不存在"); } catch (Exception ex) { Console.WriteLine($"其他错误: {ex.Message}"); }

如果把 catch (Exception) 放前面,后面的具体异常就没有机会执行。

今天写异常处理时按这个顺序

  1. 先判断这是输入错误、业务错误还是系统错误。
  2. 输入错误优先用 TryParse、空值判断。
  3. 业务错误优先明确判断,必要时 throw
  4. 系统错误用 try/catch
  5. catch 里必须给提示或记录信息。
  6. 需要收尾时写 finally
  7. 不要用异常代替所有判断。

第二部分:把知识组合成完整例子

前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个安全读取文件的程序:先判断文件名,再尝试读取,失败时给出明确提示。

最终你要写出这个程序

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("输入的不是整数"); }

分别输入 12abc

训练 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 仍然执行。

第四部分:作业完整答案

这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。

作业要求

写一个安全读取文件并统计行数的程序,要求:

  1. 输入文件名。
  2. 文件名不能为空。
  3. 文件不存在时提示“文件不存在”。
  4. 读取成功时输出文件行数。
  5. 其他读取错误输出错误信息。
  6. 无论成功失败,都输出“读取流程结束”。

作业拆解

按这个顺序写:

  1. 读取文件名。
  2. 用空值判断处理输入错误。
  3. try 中读取文件。
  4. File.ReadAllLines 得到所有行。
  5. 捕获 FileNotFoundException
  6. 捕获 Exception 处理其他错误。
  7. 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 会隐藏问题,不应该写。