异步与序列化
8 / 11
LINQ 与数据
自在学
首页课程创意工坊价格
首页课程创意工坊价格
编程C#错误处理

错误处理

在软件开发过程中,错误与异常无处不在,例如参数非法、网络中断、磁盘故障或外部依赖不可用等。专业的错误处理不仅能够提升程序的健壮性与用户体验,也是工程质量的重要保障。 这节课我们介绍如何在 C# 中进行有效的错误处理,包括如何预防和检测异常、如何规范报告错误、如何优雅恢复以及如何进行彻底的资源清理。

错误处理


try/catch/finally

在 C# 里,try/catch/finally 是我们处理错误的“基本功”,就像船上的护栏和安全绳。我们写代码时,总会遇到一些“意外的浪花”——比如文件没找到、网络突然断开、用户输入了奇怪的数据。 如果我们什么都不做,程序一遇到这些问题就会“翻船”崩溃,用户体验很糟糕。

try/catch/finally 就像是给我们的程序加上一层保护。try 代码块里放的是“可能出错”的操作,比如读文件、访问网络。 catch 就像是“救生员”,一旦 try 里出错,catch 会立刻跳出来“接住”这个错误,让我们有机会优雅地处理它,比如给用户一个友好的提示,或者记录日志方便排查。 finally 则像是“收尾小队”,无论前面有没有出错,finally 里的代码都会被执行,非常适合用来做一些善后工作,比如关闭文件、释放资源、擦干净甲板。

我们可以通过 try/catch/finally,让程序在风浪中依然稳稳当当,不会因为一个小小的意外就全线崩溃。

|
using System; using System.IO; class Program { static void Main() { try { string text = File.ReadAllText("data.txt"); // 可能抛出 IOException/UnauthorizedAccessException 等 Console.WriteLine(text); } catch (FileNotFoundException ex) { Console.WriteLine("找不到文件: " + ex.FileName); } catch (IOException ex) { Console.WriteLine("IO 出错: " + ex.Message); } finally { Console.WriteLine("无论如何都会执行:可用于收尾工作"); } } }

从具体到抽象捕获;finally 总会运行(即便 catch 未命中),适合做关闭资源、释放锁等收尾动作。


抛出异常:throw 与自定义消息

在 C# 里,抛出异常就像是我们拉响了“警报”,让程序知道“这里出事了”。当我们发现某个操作可能出错时,就可以用 throw 来抛出一个异常。

|
using System; class Calculator { public int Divide(int a, int b) { if (b == 0) throw new DivideByZeroException("分母不能为 0"); // 迅速失败,信息明确 return a / b; } }

我们可以把抛出异常想象成在工厂里拉响了紧急警报:如果机器出现了故障,工人们会立刻拉下警铃,让大家都知道哪里出了问题。 同样地,在 C# 里,当我们用 throw 抛出异常时,最好把“警报内容”写得清楚明白。 比如说,如果是分母为零导致的错误,我们就直接告诉大家“分母不能为 0”,这样看到异常的人一眼就能明白发生了什么。 异常消息越具体、越友好,后面排查和修复问题就越轻松。


重新抛出:throw; vs throw ex;

|
try { MightFail(); } catch (Exception ex) { Console.WriteLine("记录日志: " + ex.Message); throw; // 保留原堆栈,更利于诊断 // throw ex; // 会重置堆栈,不推荐 }

在 C# 里,如果我们在 catch 块里用 throw; 这种“裸抛”,其实就像是把刚才捕获到的异常原封不动地又扔了出去。 这样做有个很大的好处:异常的“来龙去脉”——也就是它的调用堆栈信息——会被完整保留下来。 举个生活中的例子,就像我们在传递一个快递时,没有拆开包装,也没有换箱子,快递单上的所有信息都还在,别人一看就知道它最初是从哪里发出来的。 如果我们用 throw ex;,就相当于把快递重新打包,原来的寄件信息就丢失了,后面查问题就会很麻烦。 所以,推荐用 throw;,这样方便我们后续定位和排查异常发生的根本原因。


异常筛选器:catch (...) when (条件)

在 C# 里,如果我们想在 catch 块里只处理某些特定类型的异常,就可以用 catch (...) when (条件) 这种“筛选器”。

举个生活中的例子,就像我们在超市买东西时,如果发现商品过期了,我们不会直接扔掉,而是会先检查一下它的保质期,如果还在保质期内,我们就会把它放回货架,继续卖给其他顾客。

|
try { MightFail(); } catch (Exception ex) when (ex.Message.Contains("暂时") ) { Console.WriteLine("临时性错误,准备重试..."); }

我们可以把异常筛选器想象成一道“智能门禁”。当异常发生时,这道门会先检查异常是不是我们关心的那一类,只有符合特定条件的异常才能进来被处理。 比如说,有时候我们只想对“临时性错误”做出反应,而其他类型的异常则让它们继续往上传递,不要在这里被“吞掉”。 这样做的好处是,我们既能精准地处理想要关注的问题,又不会丢失那些重要的异常信息,方便后续排查和定位。 就像在机场安检时,只有带有特殊标记的行李才会被单独检查,其他的行李则正常通过,不会被耽误。这样既高效又安全。


多个 catch 的顺序:先具体后一般

我们可以把多个 catch 想象成一个“多层过滤网”。当异常发生时,这些过滤网会一层一层地检查异常,只有符合特定条件的异常才能通过。

|
try { UseFile(); } catch (FileNotFoundException ex) { // 更具体 } catch (IOException ex) { // 较一般 } catch (Exception ex) { // 最一般 }

从小网眼到大网眼地“兜住”。大网在前会把小网“遮住”。


自定义异常类型

在 C# 这门语言里,我们不仅可以使用系统自带的异常类型,还能根据自己的业务需求,亲手“打造”属于自己的异常类型。这样做的好处是什么呢?就像我们在家里给每个房间贴上不同的门牌号,谁住哪一间一目了然。 自定义异常能让代码在遇到特殊情况时,把“出错的原因”表达得更清楚、更有针对性。

比如说,我们在做一个钱包扣款的功能时,如果余额不足,直接抛出系统的 Exception,别人一看只知道“出错了”,但具体是哪里出错、为什么出错,可能就要费一番功夫去查。 而如果我们自定义一个“余额不足异常”,那一眼就能看明白:哦,原来是钱不够了!

|
using System; public class BalanceNotEnoughException : Exception { public decimal Balance { get; } public decimal Attempt { get; } public BalanceNotEnoughException(decimal balance, decimal attempt) : base($"余额不足:当前 {balance},尝试扣款 {attempt}") { Balance = balance; Attempt = attempt; } } class Wallet { public decimal Balance { get; private set; } public Wallet(decimal initial) => Balance = initial; public void Withdraw(decimal amount) { if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount)); if (Balance < amount) throw new BalanceNotEnoughException(Balance, amount); Balance -= amount; } }

溢出与边界:checked/unchecked

在 C# 这门语言里,整数类型(比如 int、long)在做加法、减法、乘法这些运算时,如果结果超出了它们能表示的最大或最小范围,会发生“溢出”。 这种溢出有点像我们往一个装满水的杯子里继续倒水,多出来的水就会溢出来。但是,C# 默认情况下有时候会“悄悄”让溢出的结果绕回去(比如 int 超过最大值会变成负数),不会主动告诉我们出错了。

那么,我们怎么才能让 C# 在溢出时主动提醒我们呢?这时候就要用到 checked 关键字了。checked 就像给杯子加了一个报警器,只要水一溢出来,立刻响铃报错。 而如果我们觉得溢出没关系,愿意让它悄悄发生,那就可以用 unchecked,相当于把报警器关掉,水溢出来也不管。

|
checked { try { int x = int.MaxValue; x += 1; // 在 checked 上下文将抛 OverflowException } catch (OverflowException) { Console.WriteLine("检测到溢出"); } } unchecked { int y = int.MaxValue + 1; // 不抛异常,绕回负数 }

在需要精确数值安全时开启 checked;性能敏感且可接受环绕时可用 unchecked。


异步里的异常:await 会把异常抛回

在 C# 里,当我们用 await 等待一个异步方法时,如果这个异步方法抛出了异常,那么这个异常会被“抛回”到 await 所在的上下文。

|
using System; using System.Threading.Tasks; async Task<int> GetAsync() { await Task.Delay(10); throw new InvalidOperationException("远程失败"); } async Task Demo() { try { int v = await GetAsync(); // 这里抛出,进入 catch } catch (Exception ex) { Console.WriteLine("捕获到: " + ex.Message); } } // 仅事件处理器允许 async void;其他请使用 Task 返回,便于等待与统一处理

并发等待 Task.WhenAll 时,可能有多个异常,需要汇总或记录全部。


小练习

  1. 在catch块中重新抛出异常,推荐使用哪个关键字来保持原堆栈?
  1. 多个catch块的顺序应该是?
  1. 在C#中,用于异常筛选器的关键字是?

4. throw 与 throw ex 的区别练习

在以下代码的 catch 块中,如何保持原堆栈信息:

  • MightFail() 方法可能抛出异常
  • 在 catch 块中需要记录日志,然后重新抛出异常
  • 要求:保持原始的堆栈跟踪信息,不要丢失异常发生的位置
  • 对比 throw; 和 throw ex; 的区别
|
using System; class Program { static void MightFail() { throw new InvalidOperationException("原始异常"); } static void Main() { try { MightFail(); } catch (Exception ex) { // 记录日志 Console.WriteLine($"记录日志: {ex.Message}"); // 方式1:使用 throw; 保留原堆栈(推荐) throw; // 保留完整的堆栈跟踪信息 // 方式2:使用 throw ex; 重置堆栈(不推荐) // throw ex; // 会丢失原始异常的位置信息 } } }
|
记录日志: 原始异常 Unhandled exception. System.InvalidOperationException: 原始异常 at Program.MightFail() in ... at Program.Main() in ...

说明:

  • throw;(推荐):
    • 保留完整的堆栈跟踪信息
    • 异常会显示原始发生的位置(MightFail())
    • 便于调试和定位问题
  • throw ex;(不推荐):
    • 重置堆栈跟踪信息
    • 异常会显示为从 catch 块抛出,丢失原始位置
    • 不利于调试和问题定位
  • 在需要重新抛出异常时,应该使用 throw; 而不是 throw ex;

5. catch 顺序练习

修正以下代码的 catch 块顺序:

  • UseFile() 方法可能抛出 FileNotFoundException 或 IOException
  • FileNotFoundException 是 IOException 的子类
  • 问题:当前的 catch 顺序不正确,FileNotFoundException 永远不会被捕获
  • 要求:调整 catch 块的顺序,确保更具体的异常先被捕获
|
using System; using System.IO; class Program { static void UseFile() { // 可能抛出 FileNotFoundException 或 IOException string content = File.ReadAllText("nonexistent.txt"); } static void Main() { try { UseFile(); } // 正确顺序:先捕获更具体的异常 catch (FileNotFoundException ex) { Console.WriteLine($"文件未找到: {ex.FileName}"); } catch (IOException ex) { Console.WriteLine($"IO 错误: {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"其他异常: {ex.Message}"); } // 错误示例:如果顺序反过来 // catch (IOException ex) { } // 这会先捕获 FileNotFoundException // catch (FileNotFoundException ex) { } // 永远不会执行 } }
|
文件未找到: nonexistent.txt

说明:

  • catch 块的顺序必须从具体到抽象(从派生类到基类)
  • FileNotFoundException 是 IOException 的子类,必须先捕获
  • 如果先捕获 IOException,FileNotFoundException 也会被它捕获,后面的 catch 块永远不会执行
  • 编译器会警告不合理的 catch 顺序
  • 正确的顺序:FileNotFoundException → IOException → Exception

6. 异常筛选器练习

使用异常筛选器(when 关键字)只捕获临时性异常:

  • Run() 方法可能抛出各种异常
  • 要求:只捕获临时性异常(异常消息包含"暂时"或"临时"关键字)
  • 其他异常应该继续向上传播,不被这个 catch 块捕获
  • 使用 when 关键字添加筛选条件
|
using System; class Program { static void Run() { // 模拟可能抛出不同类型的异常 throw new InvalidOperationException("暂时无法连接服务器"); // throw new InvalidOperationException("永久性错误"); } static void Main() { try { Run(); } // 使用 when 筛选器:只捕获临时性异常 catch (Exception ex) when (ex.Message.Contains("暂时") || ex.Message.Contains("临时")) { Console.WriteLine($"捕获到临时性异常: {ex.Message}"); Console.WriteLine("准备重试..."); } catch (Exception ex) { Console.WriteLine($"捕获到其他异常: {ex.Message}"); throw; // 重新抛出非临时性异常 } } }
|
捕获到临时性异常: 暂时无法连接服务器 准备重试...

说明:

  • when 关键字用于异常筛选器,可以在 catch 块中添加条件
  • 只有满足 when 条件的异常才会被这个 catch 块捕获
  • 不满足条件的异常会继续向上传播,被后面的 catch 块捕获
  • 这种方式可以精确控制哪些异常需要特殊处理
  • 异常筛选器在 C# 6.0 中引入,让异常处理更加灵活

7. using 与 finally 练习

用两种方式确保 FileStream 被正确释放:

  • 方式1:使用 using 语句(推荐)
  • 方式2:使用 try/finally 手动释放
  • 要求:无论是否发生异常,都要确保 FileStream 被释放
  • 对比两种方式的优缺点
|
using System; using System.IO; class Program { static void Main() { // 方式1:使用 using 语句(推荐) Console.WriteLine("方式1:using 语句"); using (var fs1 = new FileStream("test1.txt", FileMode.Create)) { // 使用文件流 fs1.WriteByte(65); } // 自动调用 Dispose,释放资源 // 方式2:使用 try/finally 手动释放 Console.WriteLine("方式2:try/finally"); FileStream? fs2 = null; try { fs2 = new FileStream("test2.txt", FileMode.Create); // 使用文件流 fs2.WriteByte(66); } finally { // 确保资源被释放 fs2?.Dispose(); } // 方式1的简化写法(C# 8.0+) Console.WriteLine("方式1简化:using 声明"); using var fs3 = new FileStream("test3.txt", FileMode.Create); fs3.WriteByte(67); // 方法结束时自动释放 } }
|
方式1:using 语句 方式2:try/finally 方式1简化:using 声明

说明:

  • 方式1:using 语句(推荐):
    • 语法简洁,自动调用 Dispose()
    • 即使发生异常也会自动释放资源
    • 适合实现 IDisposable 接口的类型
    • C# 8.0+ 支持 using var 简化写法
  • 方式2:try/finally:
    • 需要手动调用 Dispose()
    • 代码较长,容易遗漏
    • 适合需要更复杂释放逻辑的场景
  • 推荐使用 using 语句,代码更简洁、安全

8. 参数校验练习

对字符串参数 name 进行"非空且非空白"的校验:

  • 编写一个方法,接受字符串参数 name
  • 要求:如果 name 为 null、空字符串或只包含空白字符,抛出 ArgumentException
  • 异常消息应该明确指出参数名和问题原因
  • 使用 string.IsNullOrWhiteSpace() 方法进行校验
|
using System; class Program { static void Greet(string name) { // 参数校验:非空且非空白 if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("参数不能为空或只包含空白字符", nameof(name)); } Console.WriteLine($"你好,{name}!"); } static void Main() { try { Greet("张三"); // 正常 Greet(""); // 抛出异常 } catch (ArgumentException ex) { Console.WriteLine($"参数错误: {ex.ParamName} - {ex.Message}"); } try { Greet(null); // 抛出异常 } catch (ArgumentException ex) { Console.WriteLine($"参数错误: {ex.ParamName} - {ex.Message}"); } try { Greet(" "); // 抛出异常(只包含空白字符) } catch (ArgumentException ex) { Console.WriteLine($"参数错误: {ex.ParamName} - {ex.Message}"); } } }
|
你好,张三! 参数错误: name - 参数不能为空或只包含空白字符 参数错误: name - 参数不能为空或只包含空白字符 参数错误: name - 参数不能为空或只包含空白字符

说明:

  • string.IsNullOrWhiteSpace(name) 检查字符串是否为 null、空字符串或只包含空白字符
  • ArgumentException 用于参数校验失败的情况
  • nameof(name) 获取参数名,避免硬编码字符串
  • 参数校验应该在方法开始处进行,快速失败(fail-fast)
  • 这种方式可以及早发现问题,避免后续处理出错
  • try/catch/finally
  • 抛出异常:`throw` 与自定义消息
  • 重新抛出:`throw;` vs `throw ex;`
  • 异常筛选器:`catch (...) when (条件)`
  • 多个 `catch` 的顺序:先具体后一般
  • 自定义异常类型
  • 溢出与边界:`checked`/`unchecked`
  • 异步里的异常:`await` 会把异常抛回
  • 小练习

目录

  • try/catch/finally
  • 抛出异常:`throw` 与自定义消息
  • 重新抛出:`throw;` vs `throw ex;`
  • 异常筛选器:`catch (...) when (条件)`
  • 多个 `catch` 的顺序:先具体后一般
  • 自定义异常类型
  • 溢出与边界:`checked`/`unchecked`
  • 异步里的异常:`await` 会把异常抛回
  • 小练习
自在学

© 2025 自在学,保留所有权利。

公网安备湘公网安备43020302000292号 | 湘ICP备2025148919号-1

关于我们隐私政策使用条款

© 2025 自在学,保留所有权利。

公网安备湘公网安备43020302000292号湘ICP备2025148919号-1