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

在 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
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
并发等待 Task.WhenAll 时,可能有多个异常,需要汇总或记录全部。
4. throw 与 throw ex 的区别练习
在以下代码的 catch 块中,如何保持原堆栈信息:
MightFail() 方法可能抛出异常throw; 和 throw ex; 的区别|using System; class Program { static void MightFail() { throw new InvalidOperationException("原始异常"); } static void Main() { try { MightFail(); } catch (Exception ex) { // 记录日志
5. catch 顺序练习
修正以下代码的 catch 块顺序:
UseFile() 方法可能抛出 FileNotFoundException 或 IOExceptionFileNotFoundException 是 IOException 的子类FileNotFoundException 永远不会被捕获|using System; using System.IO; class Program { static void UseFile() { // 可能抛出 FileNotFoundException 或 IOException string content = File.ReadAllText("nonexistent.txt"); } static void Main() { try { UseFile();
6. 异常筛选器练习
使用异常筛选器(when 关键字)只捕获临时性异常:
Run() 方法可能抛出各种异常when 关键字添加筛选条件|using System; class Program { static void Run() { // 模拟可能抛出不同类型的异常 throw new InvalidOperationException("暂时无法连接服务器"); // throw new InvalidOperationException("永久性错误"); } static void Main() { try { Run(); } // 使用 when 筛选器:只捕获临时性异常 catch (
7. using 与 finally 练习
用两种方式确保 FileStream 被正确释放:
using 语句(推荐)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(
8. 参数校验练习
对字符串参数 name 进行"非空且非空白"的校验:
namename 为 null、空字符串或只包含空白字符,抛出 ArgumentExceptionstring.IsNullOrWhiteSpace() 方法进行校验|using System; class Program { static void Greet(string name) { // 参数校验:非空且非空白 if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("参数不能为空或只包含空白字符", nameof(name)); } Console.WriteLine($"你好,{name
|记录日志: 原始异常 Unhandled exception. System.InvalidOperationException: 原始异常 at Program.MightFail() in ... at Program.Main() in ...
说明:
throw;(推荐):
MightFail())throw ex;(不推荐):
throw; 而不是 throw ex;|文件未找到: nonexistent.txt
说明:
FileNotFoundException 是 IOException 的子类,必须先捕获IOException,FileNotFoundException 也会被它捕获,后面的 catch 块永远不会执行FileNotFoundException → IOException → Exception|捕获到临时性异常: 暂时无法连接服务器 准备重试...
说明:
when 关键字用于异常筛选器,可以在 catch 块中添加条件when 条件的异常才会被这个 catch 块捕获|方式1:using 语句 方式2:try/finally 方式1简化:using 声明
说明:
using 语句(推荐):
Dispose()IDisposable 接口的类型using var 简化写法try/finally:
Dispose()using 语句,代码更简洁、安全|你好,张三! 参数错误: name - 参数不能为空或只包含空白字符 参数错误: name - 参数不能为空或只包含空白字符 参数错误: name - 参数不能为空或只包含空白字符
说明:
string.IsNullOrWhiteSpace(name) 检查字符串是否为 null、空字符串或只包含空白字符ArgumentException 用于参数校验失败的情况nameof(name) 获取参数名,避免硬编码字符串