异常处理深层原理
大约 11 分钟约 3325 字
异常处理深层原理
简介
C# 的异常处理机制基于 SEH(Structured Exception Handling),由 CLR 提供统一的支持。理解异常的抛出和捕获过程、异常过滤器的原理、以及异常对性能的影响,有助于编写健壮且高效的代码。
特点
异常机制原理
CLR 异常处理流程
// 异常抛出时的 CLR 处理流程:
// 1. 创建异常对象(分配内存)
// 2. 捕获当前栈帧信息(Stack Trace)
// 3. 搜索异常处理程序(从当前方法向上搜索)
// 4. 执行 finally 块(栈展开)
// 5. 匹配 catch 块
// 6. 如果没有匹配 → 未处理异常 → 进程终止
// 异常搜索过程
void MethodA() { MethodB(); }
void MethodB() { MethodC(); }
void MethodC() { throw new InvalidOperationException("Error"); }
// 搜索顺序:MethodC → MethodB → MethodA → ... → 顶层
// 每个 catch 块按类型匹配,匹配第一个合适的
// Exception 类层次结构
// System.Exception
// ├── System.SystemException
// │ ├── System.InvalidOperationException
// │ ├── System.ArgumentException
// │ │ ├── System.ArgumentNullException
// │ │ └── System.ArgumentOutOfRangeException
// │ ├── System.NullReferenceException
// │ ├── System.IndexOutOfRangeException
// │ ├── System.IO.IOException
// │ └── System.OutOfMemoryException
// ├── System.ApplicationException(已不推荐使用)
// └── System.AggregateException
// 关键属性
try
{
throw new InvalidOperationException("Test error") { Source = "MyModule" };
}
catch (Exception ex)
{
Console.WriteLine($"Message: {ex.Message}"); // "Test error"
Console.WriteLine($"Type: {ex.GetType().Name}"); // InvalidOperationException
Console.WriteLine($"Source: {ex.Source}"); // MyModule
Console.WriteLine($"StackTrace: {ex.StackTrace}"); // 调用栈
Console.WriteLine($"HResult: {ex.HResult}"); // 0x80131509
}异常过滤器(when 子句)
// C# 6 异常过滤器 — 在 catch 匹配前执行条件判断
// 不会重置栈跟踪(重新 throw 会重置)
try
{
DangerousOperation();
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
// 只捕获 404 错误
HandleNotFound();
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
// 只捕获 429 错误
await Task.Delay(1000);
Retry();
}
catch (HttpRequestException ex) when (LogException(ex))
{
// LogException 返回 false → 不捕获,继续搜索
// 用于记录日志但不处理异常
}
static bool LogException(Exception ex)
{
Serilog.Log.Error(ex, "未处理异常");
return false; // 不捕获,让上层处理
}
// 过滤器 vs rethrow
// ❌ rethrow 重置栈跟踪
catch (Exception ex)
{
if (!CanHandle(ex)) throw; // 重置 StackTrace
Handle(ex);
}
// ✅ 过滤器保留原始栈跟踪
catch (Exception ex) when (CanHandle(ex))
{
Handle(ex);
}性能影响
异常的性能开销
// 异常的开销:
// 1. 异常对象分配(堆上分配)
// 2. 栈跟踪捕获(遍历栈帧,非常昂贵)
// 3. 栈展开(执行 finally 块)
// 4. 异常搜索(向上遍历调用链)
// 性能对比
var sw = new Stopwatch();
// 方式 1:异常控制流(慢)
sw.Start();
for (int i = 0; i < 1000; i++)
{
try { int.Parse("abc"); }
catch { } // 1000 次异常
}
sw.Stop();
Console.WriteLine($"异常方式: {sw.ElapsedMilliseconds}ms"); // ~500ms
// 方式 2:返回值控制流(快)
sw.Restart();
for (int i = 0; i < 1000; i++)
{
int.TryParse("abc", out _); // 无异常
}
sw.Stop();
Console.WriteLine($"TryParse: {sw.ElapsedMilliseconds}ms"); // ~1ms
// 最佳实践:异常用于"异常"情况,不用作正常控制流
// ✅ 正确使用:文件不存在、网络断开、权限不足
// ❌ 错误使用:输入验证、正常业务逻辑分支高性能异常处理模式
// 1. 预检查模式(避免异常)
public class SafeParser
{
// ❌ 异常模式
public int Parse(string input) => int.Parse(input);
// ✅ Try 模式
public bool TryParse(string input, out int result) => int.TryParse(input, out result);
// ✅ Result 模式
public Result<int, string> SafeParse(string input)
{
if (string.IsNullOrEmpty(input))
return Result<int, string>.Failure("输入为空");
if (!int.TryParse(input, out int value))
return Result<int, string>.Failure("格式错误");
return Result<int, string>.Success(value);
}
}
// Result 类型
public readonly struct Result<T, TError>
{
public bool IsSuccess { get; }
public T Value { get; }
public TError Error { get; }
private Result(T value, TError error, bool isSuccess)
{
Value = value; Error = error; IsSuccess = isSuccess;
}
public static Result<T, TError> Success(T value) => new(value, default!, true);
public static Result<T, TError> Failure(TError error) => new(default!, error, false);
}
// 2. 缓存异常信息(减少栈跟踪开销)
// 在关键路径使用 ExceptionDispatchInfo 保留异常
try
{
DangerousOperation();
}
catch (Exception ex)
{
var edi = ExceptionDispatchInfo.Capture(ex);
// 可以稍后重新抛出,保留原始栈跟踪
edi.Throw();
}
// 3. 在循环中避免异常
// ❌
for (int i = 0; i < list.Count; i++) { }
// ✅ 如果可能越界
foreach (var item in list) { }全局异常处理
未处理异常捕获
// 1. AppDomain 级别
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
var ex = e.ExceptionObject as Exception;
Console.WriteLine($"未处理异常: {ex?.Message}");
Console.WriteLine($"是否终止: {e.IsTerminating}");
// 记录日志、发送报告
Log.Fatal(ex, "未处理异常导致进程终止");
};
// 2. Task 未观察异常
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
Console.WriteLine($"未观察的 Task 异常: {e.Exception}");
e.SetObserved(); // 标记为已观察,防止进程终止
};
// 3. ASP.NET Core 全局异常
app.UseExceptionHandler("/Error");
// 自定义中间件
app.Use(async (context, next) =>
{
try
{
await next(context);
}
catch (Exception ex)
{
Log.Error(ex, "请求处理异常");
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new
{
Error = "内部服务器错误",
TraceId = Activity.Current?.Id
});
}
});
// 4. WPF 全局异常
DispatcherUnhandledException += (sender, e) =>
{
Log.Error(e.Exception, "WPF 未处理异常");
e.Handled = true; // 阻止应用崩溃
};
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
Log.Error(e.ExceptionObject as Exception, "AppDomain 未处理异常");
};
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
Log.Error(e.Exception, "Task 未观察异常");
e.SetObserved();
};异常设计指南
自定义异常类型
// 自定义异常最佳实践
[Serializable]
public class BusinessException : Exception
{
public string Code { get; }
public int HttpStatusCode { get; }
public BusinessException(string code, string message, int httpStatusCode = 400)
: base(message)
{
Code = code;
HttpStatusCode = httpStatusCode;
}
public BusinessException(string code, string message, Exception inner)
: base(message, inner)
{
Code = code;
}
// 序列化支持
protected BusinessException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
Code = info.GetString(nameof(Code))!;
HttpStatusCode = info.GetInt32(nameof(HttpStatusCode));
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(Code), Code);
info.AddValue(nameof(HttpStatusCode), HttpStatusCode);
}
}
// 使用
public class UserNotFoundException : BusinessException
{
public UserNotFoundException(int userId)
: base("USER_NOT_FOUND", $"用户 {userId} 不存在", 404) { }
}
// 全局异常处理中间件
app.Use(async (context, next) =>
{
try
{
await next(context);
}
catch (BusinessException ex)
{
context.Response.StatusCode = ex.HttpStatusCode;
await context.Response.WriteAsJsonAsync(new
{
Code = ex.Code,
Message = ex.Message
});
}
});优点
缺点
总结
CLR 异常处理基于 SEH,抛出时分配异常对象、捕获栈跟踪、搜索处理程序。异常过滤器(when 子句)不会重置栈跟踪,适合日志记录和条件过滤。异常性能开销巨大(栈跟踪捕获最昂贵),只用于真正的异常情况。全局异常处理:AppDomain.UnhandledException、TaskScheduler.UnobservedTaskException、ASP.NET 中间件、WPF DispatcherUnhandledException。最佳实践:优先使用 Try 模式避免异常、异常过滤器保留栈跟踪、自定义异常继承 Exception 并支持序列化。
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道“为什么这样写”和“在什么边界下不能这样写”。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
适用场景
- 当你准备把《异常处理深层原理》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了“高级”而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把《异常处理深层原理》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《异常处理深层原理》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《异常处理深层原理》最大的收益和代价分别是什么?
异常与资源管理
// 1. using 语句确保资源释放
public async Task<string> ReadFileAsync(string path)
{
// await using 确保异步资源释放
await using var stream = new FileStream(path, FileMode.Open);
using var reader = new StreamReader(stream);
return await reader.ReadToEndAsync();
}
// 2. 异常安全的锁释放
private readonly object _lock = new();
public void SafeMethod()
{
Monitor.Enter(_lock);
try
{
// 临界区代码
}
finally
{
Monitor.Exit(_lock); // 即使异常也会执行
}
}
// 3. SemaphoreSlim 的异步安全释放
private readonly SemaphoreSlim _semaphore = new(1);
public async Task SafeAsyncMethod()
{
await _semaphore.WaitAsync();
try
{
// 临界区代码
}
finally
{
_semaphore.Release(); // 即使异常也会释放
}
}
// 4. 事务中的异常处理
public async Task TransferMoneyAsync(
string fromAccount, string toAccount, decimal amount)
{
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
await _dbContext.Accounts
.Where(a => a.Id == fromAccount)
.ExecuteUpdateAsync(s => s.SetProperty(a => a.Balance, a => a.Balance - amount));
await _dbContext.Accounts
.Where(a => a.Id == toAccount)
.ExecuteUpdateAsync(s => s.SetProperty(a => a.Balance, a => a.Balance + amount));
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw; // 重新抛出异常
}
}异常分类与处理策略
/// <summary>
/// 异常分类与对应的处理策略
/// </summary>
// 1. 可恢复异常 — 可以重试
// 网络: HttpRequestException, TimeoutException
// 数据库: DbUpdateConcurrencyException (乐观并发冲突)
// 服务: HttpRequestException (503 Service Unavailable)
public async Task<T> WithRetryAsync<T>(
Func<Task<T>> action,
int maxRetries = 3,
TimeSpan? baseDelay = null)
{
var delay = baseDelay ?? TimeSpan.FromSeconds(1);
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
try
{
return await action();
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
if (attempt == maxRetries) throw;
var retryAfter = ex.Headers?.RetryAfter?.Delta ?? delay * Math.Pow(2, attempt);
await Task.Delay(retryAfter);
}
catch (TimeoutException)
{
if (attempt == maxRetries) throw;
await Task.Delay(delay * Math.Pow(2, attempt));
}
}
throw new InvalidOperationException("不应到达此处");
}
// 2. 业务异常 — 返回错误信息给调用方
public class ValidationException : Exception
{
public Dictionary<string, string[]> Errors { get; }
public ValidationException(Dictionary<string, string[]> errors)
: base("数据验证失败")
{
Errors = errors;
}
}
// 3. 系统异常 — 记录日志,返回通用错误
// 如: OutOfMemoryException, StackOverflowException
// 这些通常不应该被捕获,让它们传播到全局处理器
// 4. 编程错误 — 不应该被捕获
// 如: NullReferenceException, IndexOutOfRangeException, ArgumentException
// 这些表示代码有 bug,应该修复代码而不是捕获异常异常处理的性能注意事项
// 1. 异常对象的内存分配
// 每次 throw new Exception(...) 都会在堆上分配对象
// StackTrace 的捕获尤其昂贵(遍历整个调用栈)
// 热路径上避免异常
// 2. 检查 vs 异常模式对比
using System.Diagnostics;
// 方式 1: 异常模式(慢)
var sw = Stopwatch.StartNew();
for (int i = 0; i < 10000; i++)
{
try { int.Parse("not-a-number"); }
catch { }
}
sw.Stop();
Console.WriteLine($"异常模式: {sw.ElapsedMilliseconds}ms"); // ~2000ms
// 方式 2: 检查模式(快)
sw.Restart();
for (int i = 0; i < 10000; i++)
{
int.TryParse("not-a-number", out _);
}
sw.Stop();
Console.WriteLine($"检查模式: {sw.ElapsedMilliseconds}ms"); // ~1ms
// 性能差距:1000-2000 倍!
// 3. 减少 StackTrace 捕获
// 在 .NET 8+ 中,可以使用 ExceptionDispatchInfo
// 或者使用 Environment.StackTrace(较轻量)
// 但最根本的解决方案是避免在热路径上抛出异常
// 4. 异常过滤器避免异常对象的创建
// when 子句在匹配成功之前不会创建异常对象
try
{
DangerousOperation();
}
catch (Exception ex) when (IsRecoverable(ex)) // 如果返回 false,异常对象不会被创建
{
Recover();
}
static bool IsRecoverable(Exception ex)
{
return ex is TimeoutException or HttpRequestException
{ StatusCode: System.Net.HttpStatusCode.TooManyRequests or System.Net.HttpStatusCode.ServiceUnavailable };
}结构化日志与异常关联
// 使用 Serilog 记录结构化异常日志
// 1. 基本异常日志
try
{
await ProcessOrderAsync(order);
}
catch (Exception ex)
{
logger.Error(ex,
"订单处理失败 {OrderId} {UserId}",
order.Id, order.UserId);
// 输出包含完整的异常信息和结构化属性
}
// 2. EnrichFromLogContext — 自动关联请求上下文
// 在中间件中设置日志上下文
app.Use(async (context, next) =>
{
using (LogContext.PushProperty("RequestId", context.TraceIdentifier))
using (LogContext.PushProperty("UserId", context.User.Identity?.Name))
using (LogContext.PushProperty("Path", context.Request.Path))
{
await next();
}
});
// 所有日志自动携带 RequestId、UserId、Path
// 方便在日志平台中按请求过滤
// 3. 异常分类标签
try
{
await ProcessPaymentAsync(order);
}
catch (PaymentException ex)
{
logger.Error(ex, "支付失败 {ErrorCode} {OrderId}",
ex.Code, order.Id);
// 结构化标签: ErrorCode=INSUFFICIENT_BALANCE
}
catch (Exception ex)
{
logger.Error(ex, "未知错误 {OrderId}", order.Id);
}
// 4. 异常关联 TraceId
// 在 ASP.NET Core 中,Activity.Current.Id 就是 TraceId
// Serilog 会自动记录
logger.Error(ex, "请求异常 {TraceId}", Activity.Current?.Id);
// 在日志平台中按 TraceId 搜索可以找到完整的请求链路