






















异常处理是现代编程语言中不可或缺的机制,.NET平台通过
try-catch-finally结构和强大的异常类型系统,为开发者提供了高效、类型安全的错误控制能力。在《CLR via C#》第20章中,Jeffrey Richter 以系统性的方式剖析了异常与程序状态之间的本质联系。本文将围绕该章节的精华内容展开讲解。
异常代表方法未能完成其既定任务。它是一种信号,告诉调用者:当前方法执行失败,必须采取额外措施进行处理。
举例说明:
public void Transfer(Account from, Account to, decimal amount) {
if (from == null || to == null) throw new ArgumentNullException();
if (amount <= 0) throw new ArgumentOutOfRangeException();
// 执行转账逻辑
}
此设计比返回错误码更优雅,因它迫使调用方必须显式处理失败路径,确保系统稳定性。
.NET 使用标准结构处理异常:
try {
// 可能抛出异常的代码
}
catch (InvalidOperationException ex) {
// 针对 InvalidOperationException 类型的异常进行恢复处理
}
finally {
// 无论异常是否发生,都会执行(如释放资源)
}
catch块必须从最具体的异常类型写起,逐步过渡到一般类型(如 Exception)。
finally通常用于释放锁、关闭文件、回收资源等清理逻辑。
当异常被抛出后,CLR 会从 try 所属的多个 catch 中查找第一个能匹配异常类型的块。类型匹配规则与继承一致。
示例:
catch (IOException ex) { ... } // 捕获 I/O 异常
catch (Exception ex) { ... } // 捕获所有异常(兜底)
匹配时的最佳实践是:按继承深度顺序排序,避免捕获过于宽泛的异常类型影响诊断。
可通过 catch (Exception ex) 中的 ex 对象获取错误信息。
在调试器中使用 $exception 变量,查看当前异常的详细堆栈、调用位置、异常源。
若抛出的异常未被任何 catch 块处理:
CLR 会沿调用堆栈逐层查找catch块。
若最终未处理,将引发程序崩溃(终止进程),并记录日志或转入系统默认异常处理程序。
当 try 块中代码抛出异常后,CLR 会尝试在调用堆栈中找到匹配类型的 catch 块来处理异常。你有三种方式处理异常:
重新抛出同一个异常:保留原始异常信息传递到调用者。
抛出新的异常:包装原始异常以提供更多上下文信息。
不处理异常:执行完 catch 后不再抛出异常,继续进入 finally(若有)或后续代码。
异常对象可通过 catch (Exception e) 捕获,并访问其堆栈信息等,但不推荐修改这个异常对象
finally 块用于执行清理操作,其代码无论是否发生异常都保证被执行。例如处理文件流时:
FileStream fs = null;
try {
fs = new FileStream(path, FileMode.Open);
} catch (IOException) {
// 错误处理
} finally {
if (fs != null) fs.Close(); // 确保文件关闭
}
这是所有标准异常的基类,包含多个实用属性:
| 属性名 | 说明 |
|---|---|
System.Exception 是所有异常的基类,它提供了多个关键属性:
InnerException:指向引发当前异常的原始异常,形成链式追踪结构,方便定位根本原因。
HResult:用于跨越托管与非托管边界,保留 COM 异常的 HRESULT 值。
StackTrace:调用链的字符串表示,有助于开发者定位异常来源。
📌 注意:StackTrace 不是简单的字符串,它是通过调用 CLR 内部逻辑动态生成的,仅在异常真正被抛出之后才有值
代码中常见的错误是使用 throw e; 重新抛出异常,这样会重置异常的起始位置,导致 StackTrace 误导调试。
catch (Exception e)
{
// 错误方式:重设了堆栈起点
throw e;
}
正确方式是使用裸 throw;,保留原始堆栈信息:
catch (Exception e)
{
// 正确方式
throw;
}
🧠 深入理解:虽然 CLR 保留了正确的位置,但操作系统的错误报告如 Windows Error Reporting 会显示最后一次 throw 的位置,影响实地调试
默认情况下,JIT 编译器可能会将一些方法“内联”,使得这些方法在堆栈跟踪中缺失。
为避免这一点:
[MethodImpl(MethodImplOptions.NoInlining)]
public void DoNotInline()
{
...
}
此外,使用 /debug 编译开关或设置 DebuggableAttribute 也可以控制 JIT 的内联行为,从而获取更完整的调试信息
FCL 中提供了丰富的异常类层级体系,涵盖了系统错误、IO、反射、序列化、线程等领域。
常用异常包括:
NullReferenceExceptionArgumentException / ArgumentNullExceptionInvalidOperationExceptionIOException / FileNotFoundExceptionFormatException / OverflowException这张树形图表明实际开发中不一定严格遵循“CLR异常派生自 SystemException,应用异常派生自 ApplicationException”的规则。
🧩 设计教训:过度依赖 ApplicationException 是反模式,建议自定义异常直接继承 Exception 并以“Exception”结尾命名,如 UserLoginFailedException
System.Exception 的派生类。System.Exception 或 System.SystemException,因为这样会让调用者难以精确捕捉和处理。为了让异常更具语义性,同时支持序列化,书中定义了一个泛型异常基类:
[Serializable]
public sealed class Exception<TExceptionArgs> : Exception, ISerializable
where TExceptionArgs : ExceptionArgs
{
private readonly TExceptionArgs m_args;
public TExceptionArgs Args => m_args;
public Exception(TExceptionArgs args, string message = null, Exception inner = null)
: base(message, inner) => m_args = args;
public override string Message => (m_args == null) ? base.Message : base.Message + " (" + m_args.Message + ")";
}
还定义了一个简单的 ExceptionArgs 抽象类,用来封装参数:
[Serializable]
public abstract class ExceptionArgs {
public virtual string Message => string.Empty;
}
使用示例:
throw new Exception<DiskFullExceptionArgs>(
new DiskFullExceptionArgs(@"C:\"), "The disk is full");
一些建议
finally 块: 确保资源得到正确清理,防止资源泄漏。using System;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;
public class ExceptionDemo : MonoBehaviour
{
void Start()
{
SafeParseAndUse();
SafeFileOperation();
StartCoroutine(RunCoroutineSafely());
try
{
var result = DangerousOperation();
Debug.Log($"结果:{result}");
}
catch (InvalidOperationException ex)
{
Debug.LogError($"关键错误: {ex.Message}");
Environment.FailFast("系统已损坏", ex); // 致命异常处理
}
}
// 1. 使用 TryParse 避免 Parse 异常
void SafeParseAndUse()
{
string input = "123abc";
if (int.TryParse(input, out int number))
{
Debug.Log($"转换成功:{number}");
}
else
{
Debug.LogWarning("输入无效,无法转换为整数");
}
}
// 2. 使用 try-finally 管理资源
void SafeFileOperation()
{
FileStream fs = null;
try
{
fs = new FileStream("data.txt", FileMode.OpenOrCreate);
// 读写文件
}
catch (IOException ioEx)
{
Debug.LogError($"文件异常: {ioEx.Message}");
}
finally
{
fs?.Close(); // 确保关闭
}
}
// 3. 封装任务错误,避免抛出异常影响控制流
public static (bool success, string data, Exception error) LoadConfig()
{
try
{
string data = File.ReadAllText("config.json");
return (true, data, null);
}
catch (Exception ex)
{
return (false, null, ex); // 返回错误元组
}
}
// 4. Unity中协程异常捕获
System.Collections.IEnumerator RunCoroutineSafely()
{
try
{
yield return new WaitForSeconds(1);
throw new Exception("协程内错误");
}
catch (Exception ex)
{
Debug.LogError($"协程异常:{ex}");
}
}
// 5. 模拟致命错误
string DangerousOperation()
{
throw new InvalidOperationException("配置缺失,无法恢复");
}
}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。