📚 **严老师 · Day 10(Phase 2 第 3 课)**
10推送消息数
18327字符数
2026-08-09教学日期
<!-- message_id: om_x100b684871f520a0b4abd8aafc14d0c -->
📚 **严老师 · Day 10(Phase 2 第 3 课)**
主人早上好。今天讲 **.NET 异常处理机制 —— 栈展开 / SEH / CIL 异常表**。
这个主题和 Day 9 的 JIT 是天生搭档 —— 异常表的 unwind info 就是 JIT 生成的,SEH 是 OS 的派发机制。理解了异常处理,你就理解了整个 CLR 控制流的"紧急通道"。
一、今日知识点
1. 是什么(精确定义)
**.NET 异常处理 = CIL Exception Handling Table + Windows SEH 的双层抽象**。
很多人以为 try/catch/finally 是 C# 关键字级别的语法。事实上:
- **CIL 层**:Roslyn 编译器把 try/catch/finally 编码到方法的 **Exception Handling Clause Table(异常子句表)** 中。这张表描述 try 块的 IL 偏移范围、handler 类型(catch / filter / finally / fault)、handler 起始偏移、异常类型元数据 token。
- **OS 层**:CLR 通过 Windows **SEH(Structured Exception Handling,结构化异常处理)** 注册并派发这些表。CLR 本身是个 Win32 进程,它用 SEH 统一处理**托管异常**(throw new Exception())和**非托管异常**(访问违例 0xC0000005 / 除零 / 硬件 trap)。
**关键洞察**:C# 的异常处理不是语言特性,而是**编译器 + CLR + OS 三方协议**。
---
<!-- message_id: om_x100b6848716fa8a0b491867e521093f -->
2. 为什么(设计哲学)
哲学一:**零开销(Zero Overhead)—— Bjarne Stroustrup 的原话**
"An exception is not an expected event. The zero-overhead principle is the most important design constraint."
**具体含义**:
• ✅ **正常路径(无异常抛出)**:CPU 执行 try 块代码**完全没有额外开销** —— JIT 不写寄存器、不做边界检查、不插入任何 sentinel。leave 指令在正常路径下等价于一次直接跳转。
• ❌ **异常路径(throw + 展开 + 捕获)**:开销由"零星罕见事件"承担,不污染热路径。
**反例(不是零开销)**:
• Java HotSpot 的隐式 NPE 检查(每个对象解引用插入隐式 null 测试)→ **违反零开销** → 显著拖累正常路径
• C# 完全无此开销 —— JIT 不做任何隐式检查
哲学二:**SEH 优于传统 C++ 异常(Itanium ABI)**
| 维度 | Windows SEH | C++ Exceptions (Itanium ABI) |
| -------- | ------------------ | ---------------------------- |
| 注册机制 | 静态表(编译期生成到 PE) | 动态栈帧(throw 时构造) |
| 栈展开时机 | 找到 handler 后才展开 | throw 后立即展开 |
| 硬件异常支持 | ✅ 原生(访问违例、除零) | ❌ 不支持 |
| RAII 析构 | ❌ 不直接支持 | ✅ throw 时沿途析构 C++ 对象 |
| 性能(正常路径) | ✅ 零开销 | ✅ 零开销 |
| 性能(异常路径) | 较快(找到 handler 再展开) | 较慢(沿途析构调用栈) |
**.NET 选用 SEH 的三个理由**:
1. 统一托管/非托管异常(硬件异常 → 托管异常自动翻译)
2. unsafe 块的内存访问违例也能被 catch (AccessViolationException) 捕获
3. 与 Windows 调试器深度集成(First / Second Chance Exception)
哲学三:**异常是"非正常情况"的信号**
文档明示:异常用于"不可恢复或意外条件",**不是**正常控制流。
这就是为什么 int.TryParse 比 int.Parse + try/catch 性能高 **50-100 倍**。
---
<!-- message_id: om_x100b68480ec98130b10f9c2fae76756 -->
3. 怎么用(代码 + CIL 异常表剖析)
基础用法骨架
public decimal CalculateOrderPrice(Order order)
{
Throw.IfNull(order); // .NET 7+ 零开销守卫,Release 保留
try
{
var rate = _exchange.GetRate(order.Currency); // 可能抛
return order.Amount * rate;
}
catch (ExchangeRateException ex) // 已知业务异常
{
_logger.LogWarning(ex, "汇率获取失败,使用默认汇率");
return order.Amount * DEFAULT_RATE; // 业务降级
}
catch (OperationCanceledException) // 取消不重试
{
throw; // rethrow 保留原始栈
}
catch (Exception ex) // 兜底
{
_logger.LogError(ex, "订单计算未知异常");
throw new OrderProcessingException("订单计算失败", ex); // 包装,保留 inner
}
finally
{
_telemetry.TrackCalculationComplete(order.Id); // 必执行
}
}
CIL 异常表(IL Dumper 风格)
.method public hidebysig instance decimal CalculateOrderPrice(...) cil managed
{
.try {
IL_000a: ... 业务代码
IL_0042: leave IL_0090 ; ← leave 而非 br,触发 finally
}
catch ExchangeRateException {
IL_0043: stloc.0
IL_005f: leave IL_0090
}
catch OperationCanceledException {
IL_0060: pop
IL_0061: rethrow ; ← rethrow 保留原始栈
}
catch Exception {
IL_0063: stloc.0
IL_006f: leave IL_0090
}
IL_0090: ... finally 块
IL_00a8: endfinally
}
.exception table {
[try IL_000a..IL_0042] -> [handler IL_0043 (catch ExchangeRateException)]
[try IL_000a..IL_0042] -> [handler IL_0060 (catch OperationCanceledException)]
[try IL_000a..IL_0042] -> [handler IL_0063 (catch Exception)]
[try IL_0042..IL_00a8] -> [handler IL_0090 (finally)]
}
**关键 IL 指令**:
- leave:正常退出 try 块,跳到 finally 之后(**不能用 br 代替**,否则 finally 不执行)
- rethrow:opcode 0xFE 0x1A,重新抛出当前异常对象,保留原始 StackTrace
- endfinally:标记 finally 块结束(leave finally 区)
栈展开(Stack Unwinding)5 步过程
抛出 ExchangeRateException 后:
1. **CLR 抛出异常对象**,通过 RDI 寄存器传递(x64 平台约定)
2. **沿着调用栈向上搜索**:每个方法的 Exception Table 检查 try 块 IL 范围是否覆盖抛出点
3. 找到匹配 → 进入 handler,**先执行沿途所有方法的 finally**
4. 没找到 → 继续向上,直到 Thread.ThreadHelper.ThreadStart 入口
5. → AppDomain.UnhandledException 事件 → 进程终止(默认 .NET Framework 弹窗,.NET Core/.NET 5+ 直接退出)
---
<!-- message_id: om_x100b68480e9ac4a4b27522b8fd59f57 -->
4. 常见误区(6 大反模式)
误区 1:throw ex; vs throw; —— 栈重置陷阱
catch (Exception ex)
{
_logger.LogError(ex, "出错");
throw ex; // ❌ IL: ldloc.0; throw → StackTrace 从这一行开始
}
catch (Exception ex)
{
_logger.LogError(ex, "出错");
throw; // ✅ IL: rethrow → 保留原始抛出位置
}
误区 2:catch 后吃掉异常(No-op Catch)
try { RiskyOperation(); }
catch (Exception) { } // ❌ 完全吞掉,调试地狱
误区 3:用异常做正常控制流(性能杀手)
// ❌ 失败时 ~1-10 μs + 栈展开 + GC 分配 Exception 对象
try { return int.Parse(s); }
catch (FormatException) { return -1; }
// ✅ 零开销路径
if (int.TryParse(s, out var result)) return result;
return -1;
**基准**:失败场景下,int.Parse + try/catch 比 int.TryParse 慢 **50-100 倍**。
误区 4:finally **不一定**执行
**5 种不执行 finally 的情况**:
1. Environment.FailFast() —— 立即终止进程
2. Process.Kill() / kill -9 —— OS 直接 SIGKILL
3. **StackOverflowException** —— 栈空间耗尽,无法执行 finally
4. **进程 OOM 提前崩溃**(_failFastOnOutOfMemory)
5. **AppDomain 卸载** —— 强制终止线程
**反直觉**:StackOverflowException 在 .NET Framework 时代可 catch,**.NET Core 2.0+ 完全不可 catch**(CLR 故意让其直接崩溃,避免栈损坏)。
误区 5:catch 顺序错误(多态陷阱)
catch (Exception) { } // ❌ 先匹配,后面的 catch 永远进不来
catch (SpecificException) { } // 死代码,编译器警告 CS1058
误区 6:AggregateException 只属于 TPL
Task.WhenAll(tasks).Wait() 把每个 task 的异常包装成 AggregateException。业务层应:
catch (AggregateException ae)
{
foreach (var inner in ae.Flatten().InnerExceptions)
_logger.LogError(inner, "子任务失败");
}
───
5. 关联知识网络
| 已教知识点 | 与异常的关联 |
| ----------------- | ------------------------------------------------------------------- |
| Day 1 值类型 | 值类型作为异常对象的"装箱/捕获"成本(可忽略,异常本身是引用类型) |
| Day 2 泛型 | Task<T> 异常传播 vs Task(无返回值 Task)—— await Task<int> 时异常被重新抛出 |
| Day 3 委托/事件 | 事件订阅者抛出异常会中断后续订阅者,且不会传播到触发者(这是大坑) |
| Day 4 async/await | async 方法的异常通过状态机保存在 AsyncStateMachine,Task 完成时通过 TrySetException 传递 |
| Day 7 Span | Span 是 ref struct,异常路径仍可用(异常不涉及"跨方法逃逸"问题) |
| Day 8 GC | 异常对象是托管堆分配,频繁抛异常 → Gen0 收集压力 |
| Day 9 JIT | JIT 生成 catch handler 代码时包含栈展开辅助(unwind info),与 SEH 协同 |
---
<!-- message_id: om_x100b68480e5de4a4b3f8f3437ac34b5 -->
二、递进式面试题
Q1(基础概念)
请简述 .NET 中 try/catch/finally 的底层执行原理。
- 为什么说 .NET 异常处理是"零开销"的?"零开销"具体指什么场景?
- 什么情况下异常处理**不是**零开销的?
Q2(原理与辨析)
1. CIL 的 **Exception Handling Clause Table** 是什么结构?它和 IL 代码的对应关系是什么?
2. Windows **SEH** 与 C++ 传统异常模型(Itanium ABI)在栈展开机制上有什么区别?为什么 .NET 选用 SEH?
3. throw; 和 throw ex; 在 IL 层有什么区别?对调试栈(stack trace)的影响如何?
4. 什么是 **First Chance Exception**?什么是 **Second Chance Exception**?调试器在哪里挂钩?
Q3(实战与深度)
你在开发一个高性能 WPF 金融交易终端(类似 Bloomberg Terminal),要求:
1. 订单处理流水线(**每秒 5000+ 订单**)中**不能**因为网络抖动抛出异常就中断流水线
2. 关键路径(**P99 延迟 < 5ms**)必须避免异常抛出
3. 异常必须 100% 捕获并上报到遥测系统,但**绝不能**因为异常处理逻辑拖垮正常路径
4. 业务上需要在「已知失败(业务异常)」和「未知异常(程序错误)」之间区分:网络超时这种**已知业务异常**走 Retry 策略,其他走告警通道
请给出完整的异常处理架构设计:
- **5 层架构**(网络层 / 业务层 / 调度层 / 聚合层 / 遥测层)的代码骨架
- **"零开销断言"**方案的具体实现(Debug.Assert vs Contract.Requires vs 自定义 Guard 类 vs .NET 7+ Throw.IfNull 对比)
- **4 种方案对比**:try/catch(异常流) vs Result<T>(Rust 风格返回值) vs OneOf<T1,T2> 库 vs Try* 模式(TryParse 风格)
- **5 个反模式**(每条用代码举例说明)
---
<!-- message_id: om_x100b68480e1a60a0b1c34825dec0e73 -->
───
三、全面复盘讲解
Q1 答案
**得分点**:
• ✅ 明确提到**两层抽象**:CIL Exception Handling Table + Windows SEH
• ✅ 解释"零开销"的具体含义:**正常路径** CPU 完全无额外开销
• ✅ 承认边界:**异常路径**有显著开销(栈展开 + 表查找 + Exception 对象 GC 分配)
**深度解析 —— 3 步执行流程**:
1. **编译期**:Roslyn 把 try/catch/finally 编码到方法的 Exception Handling Clause Table(每个方法独立表,存储在 PE 文件 .method 区段)
2. **运行期 - 正常路径**:
• CPU 执行 try 块 IL 指令时**完全不知道**异常表存在
• leave 指令在正常路径下相当于一次直接跳转
• JIT 编译时**不插入任何额外检查**(与 Java HotSpot 的隐式 NPE 检查形成对比)
3. **运行期 - 异常路径**(5 步):
throw → CLR 创建 Exception 对象 (Gen0 分配)
→ RDI 寄存器传递异常指针 (x64 约定)
→ CLR 遍历当前方法的 Exception Table 找匹配 handler
→ 没找到 → 向上遍历调用栈
→ 找到 → 进入 handler,先执行沿途所有 finally
**"零开销"精确含义**:
| 路径 | 开销 | 原因 |
| --------------- | -------- | ------------------------------------- |
| ✅ 正常路径 | 零 | JIT 不写任何寄存器/检查 |
| ❌ 异常路径 | ~1-10 μs | 栈展开 + 表查找 + Exception 分配 + 触发 finally |
| ⚠️ finally 大代码量 | 显著 | JIT 内联展开到所有 leave,代码膨胀拖累 I-Cache |
**反例(不是零开销)**:
• **finally 中大量代码** → JIT 必须内联展开到所有 leave → 代码膨胀
• **频繁抛异常** → 每次 ~1-10 μs + StackTrace.GetFrames() + Gen0 GC 压力
• **调用栈极深** → 栈展开成本 O(栈深度)
**易错点**:
• ❌ "异常处理零开销 = 可以随便用 try/catch" —— **错误**,零开销是"正常路径",异常路径代价极高
• ❌ "finally 一定执行" —— **错误**,StackOverflowException / FailFast / kill -9 都不执行
---
<!-- message_id: om_x100b68480fe4d8a0b149de2e6db3b6c -->
Q2 答案
1. CIL Exception Handling Clause Table 结构
每条记录包含 **4 个核心字段**:
| 字段 | 含义 |
| ----------------------------- | ---------------------------------------------------- |
| Flags | 0x01=Catch / 0x02=Fault / 0x04=Filter / 0x08=Finally |
| TryOffset / TryLength | try 块 IL 偏移范围(字节) |
| HandlerOffset / HandlerLength | handler IL 偏移范围 |
| ClassToken / FilterOffset | 异常类型元数据 token 或 filter 表达式偏移 |
完整数据存储在 PE 文件的 .method 表中,通过反射暴露:
var body = method.GetMethodBody();
foreach (var clause in body.ExceptionHandlingClauses)
{
Console.WriteLine($"{clause.Flags}: {clause.TryOffset}-{clause.TryOffset+clause.TryLength}");
}
2. SEH vs C++ 异常(Itanium ABI)
| 维度 | Windows SEH | C++ Exceptions |
| -------- | --------------- | -------------------- |
| 注册机制 | 静态表(编译期生成) | 动态栈帧(throw 时构造) |
| 栈展开时机 | 找到 handler 后才展开 | throw 后立即展开 |
| 硬件异常支持 | ✅ 原生(访问违例、除零) | ❌ 不支持 |
| RAII 析构 | ❌ 不直接支持 | ✅ throw 时沿途析构 C++ 对象 |
| 性能(正常路径) | ✅ 零开销 | ✅ 零开销 |
| 性能(异常路径) | 较快 | 较慢(沿途析构调用栈) |
**.NET 选用 SEH 的三个理由**:
1. **统一托管/非托管异常** —— 硬件异常通过 VEH(Vectored Exception Handler)翻译成托管异常
2. **unsafe 块的内存访问违例**可被 catch (AccessViolationException) 捕获
3. **Windows 调试器深度集成**(First / Second Chance Exception)
4. throw; vs throw ex; 的 IL 区别
| 写法 | IL | StackTrace 行为 |
| --------- | -------------------------- | ---------------- |
| throw; | rethrow (opcode 0xFE 0x1A) | 保留原始抛出位置 |
| throw ex; | ldloc.0; throw | 重置到 throw ex 那一行 |
// 实战推荐:用 ExceptionDispatchInfo 跨上下文 rethrow
var info = ExceptionDispatchInfo.Capture(originalEx);
info.Throw(); // 跨线程/跨 async 上下文仍保留原始栈
4. First Chance vs Second Chance Exception
| 类型 | 触发时机 | 处理者 | 调试器挂钩 |
| ------------- | -------------- | ------------ | ----------------------------------- |
| First Chance | 异常首次抛出时 | CLR + 调试器订阅者 | IClrDebugging::FirstChanceException |
| Second Chance | 异常未被捕获、即将终止进程时 | 默认崩溃弹窗 / OS | KiDispatchException 内核路径 |
**调试器挂钩位置**:
• VS 的"异常设置"窗口订阅 First Chance(可在 catch 之前看到)
• 进程崩溃时 OS 触发 Second Chance(dump 抓取时机)
• 商业 APM(如 Application Insights / Datadog)订阅 First Chance 上报
---
<!-- message_id: om_x100b68480fba50a0b049aff43d2534a -->
Q3 答案 —— 5 层异常处理架构
1. 五层代码骨架
// ============ 第 1 层:网络层(已知瞬时错误 → 业务异常,未知 → 上抛) ============
public async ValueTask<OrderAck> SendOrderAsync(Order order, CancellationToken ct)
{
try
{
return await _tcpClient.SendAsync(order, ct).ConfigureAwait(false);
}
catch (SocketException ex) when (IsTransient(ex)) // C# 6+ exception filter
{
throw new TransientOrderException("网络瞬时错误,可重试", ex);
}
catch (TimeoutException)
{
throw new TransientOrderException("订单超时,可重试");
}
// 其他异常不捕获,直接上抛到业务层
}
// ============ 第 2 层:业务层(区分业务异常 vs 程序异常) ============
public async ValueTask<OrderResult> ProcessOrderAsync(Order order)
{
try
{
return await _orderPipeline.ExecuteAsync(order);
}
catch (TransientOrderException ex) when (CanRetry(ex)) // 已知业务异常 → 重试
{
_retryPolicy.ExecuteAsync(() => ProcessOrderAsync(order));
return OrderResult.Pending;
}
catch (OrderValidationException ex) // 已知业务异常 → 拒绝订单
{
_telemetry.TrackBusinessRejection(order, ex);
return OrderResult.Rejected(ex.Reason);
}
catch (Exception ex) // 未知异常 → 告警 + 兜底
{
await _alert.TriggerAsync($"未知异常: {ex.GetType().Name}");
_telemetry.TrackUnexpectedError(ex, order);
return OrderResult.Failed("系统错误");
}
}
// ============ 第 3 层:调度层(Dispatcher 异常隔离) ============
private void OnOrderReceived(OrderResult result)
{
if (!_dispatcher.CheckAccess())
{
_dispatcher.BeginInvoke(() => OnOrderReceived(result));
return;
}
try { _viewModel.UpdateOrder(result); }
catch (Exception ex)
{
// UI 更新失败不影响业务正确性 — 订单已成功处理
_logger.LogError(ex, "UI 更新失败,但订单已成功");
}
}
// ============ 第 4 层:聚合层(异常聚合 + 采样上报) ============
public sealed class ExceptionTelemetry
{
private long _dropCount;
private long _reportedCount;
public void Track(Exception ex, [CallerMemberName] string caller = "")
{
// 采样上报 — 高频异常不能拖垮遥测管道
if (Interlocked.Increment(ref _dropCount) % 100 != 0) return;
Interlocked.Increment(ref _reportedCount);
// 异步上报,不在业务路径上等
_ = _hubClient.TrackExceptionAsync(ex, caller);
}
}
// ============ 第 5 层:遥测层(完全异步 + 队列缓冲) ============
public Task TrackAsyncExceptionAsync(Exception ex, string context)
{
// Channel 背压 + 后台线程消费
return _channel.Writer.WriteAsync(new ExceptionReport(ex, context)).AsTask();
}
---
<!-- message_id: om_x100b68480f0780a0b36facb2b1d4001 -->
2. 零开销断言方案对比
| 方案 | 正常路径开销 | 异常路径行为 | Release 行为 | 适用 |
| -------------------------- | --------- | ----------------------- | ---------- | ------------------------------------ |
| Debug.Assert | 零(条件编译移除) | 弹对话框 | 完全移除 | 调试期断言 |
| Contract.Requires<T>(bool) | 零(条件编译移除) | 抛 ContractException | 完全移除 | Code Contracts(已废弃,仅 .NET Framework) |
| 自定义 Guard.AgainstNull(x) | 零(内联) | 抛 ArgumentNullException | 保留(强制) | 生产期不变量 |
| Throw.IfNull(x) (.NET 7+) | 极小(内联) | 抛 ArgumentNullException | 保留(强制) | 推荐 |
// .NET 7+ 推荐写法
public decimal Calculate(Order order)
{
Throw.IfNull(order); // Release 保留 — API 契约
Debug.Assert(order.Amount > 0, "订单金额必须为正"); // Release 移除 — 内部假设
return order.Amount * _rate;
}
**核心区分原则**:
• **API 边界**(public 方法参数)→ Throw.IfNull(Release 必须保留,保护调用方)
• **内部假设**(private 方法不变量)→ Debug.Assert(Release 移除,节省开销)
3. 4 种"失败处理"方案对比
| 维度 | try/catch(异常流) | Result<T>(返回值) | OneOf<T1,T2> 库 | Try* 模式 |
| ---------- | -------------- | ---------------- | ---------------- | ------------ |
| 性能(成功路径) | ✅ 零开销 | ⚠️ 多一次 struct 返回 | ⚠️ 装箱(若非 struct) | ✅ 零开销 |
| 性能(失败路径) | ❌ 1-10 μs | ✅ 1 ns | ✅ 1-2 ns | ✅ 1 ns |
| 调用方强制处理 | ❌ 可忽略 | ✅ 编译器强制 | ✅ 编译器强制 | ❌ bool 返回可忽略 |
| 错误信息丰富度 | ✅ 完整栈 | ⚠️ 需额外字段 | ✅ 完整 | ❌ 仅 bool |
| 调试友好 | ✅ 原始栈 | ⚠️ 需手动记录 | ⚠️ 需手动记录 | ⚠️ 需手动记录 |
| .NET 生态友好度 | ✅ 原生 | ⚠️ 自建 | ⚠️ 引入第三方 | ✅ 原生 |
**实战策略(混合使用)**:
• **业务层** → try/catch(异常即业务事件,必须丰富信息)
• **核心热路径**(订单解析、价格计算)→ TryParse 风格(避免异常拖慢热路径)
• **跨层边界** → Result<T>(强制调用方处理)
• **多态错误** → OneOf<OrderSuccess, ValidationError, NetworkError>(业务有多种合理失败)
// 例:Order 解析热路径用 TryParse 风格(避免异常)
public static bool TryParseOrder(ReadOnlySpan<byte> wire, out Order order)
{
order = default;
if (wire.Length < FIXED_HEADER_SIZE) return false;
if (wire[0] != (byte)'N') return false; // New Order Single
// ... 解析失败返回 false,不抛异常
return true;
}
---
<!-- message_id: om_x100b68480ce5aca4b1766b34f0ecccc -->
4. 5 个反模式(每条用代码举例)
// 反模式 1:用 catch 代替 if(热路径性能杀手)
try { return int.Parse(s); }
catch (FormatException) { return -1; } // ❌ 比 int.TryParse 慢 50-100 倍
// 反模式 2:catch 后无脑包装(丢失业务上下文)
catch (Exception ex)
{
throw new MyException("出错", ex); // ❌ 上层只看到 "出错",看不到原始异常类型
}
// ✅ 应该传递异常类型,而不是模糊包装
// 反模式 3:空 catch 吞掉异常(调试地狱)
try { RiskyOp(); }
catch (Exception) { } // ❌ 默默失败,生产事故定位困难
// 反模式 4:在 finally 中抛异常(吞掉原始异常 + 破坏栈)
try { Op(); }
finally { throw new Exception(); } // ❌ 如果 Op() 已抛异常,原始异常被吞
// 反模式 5:异步中 catch 后不传播(进程状态不一致)
try { await Task.Run(...); }
catch (Exception) { /* 静默 */ } // ❌ Task 状态被吞,后续 await 不知道失败
四、扩展思考(引导动手验证)
1. **ExceptionDispatchInfo**:跨线程/跨 async 上下文重新抛出,**保留原始栈**。试写一个 Demo:try { throw new InvalidOperationException("原始位置"); }
catch (Exception ex) { _savedEx = ExceptionDispatchInfo.Capture(ex); }
// 跨线程后:
_savedEx.Throw(); // StackTrace 仍是原始位置
2. **AOT 剪裁下的异常**:NativeAOT 下异常仍然工作,但**异常类型必须静态可知**(反射动态生成 Activator.CreateInstance(Type.GetType("...")) 在 AOT 下失败)。验证:分别用 NativeAOT 和普通 JIT 编译一段抛自定义异常的代码,观察异常对象 metadata 完整性。
3. **异常表与 Roslyn 分析器**:写一个 Roslyn 分析器扫描代码,统计每个方法的 Exception Handling Clause 数量 + finally 块行数,对**潜在代码膨胀**告警。
4. **[LoggerMessage] 源生成异常日志**(.NET 6+):用源生成避免反射 + 字符串拼接 —— 与 Day 6 反射主题呼应。
五、关联与下节预告
今天讲的异常处理是 **Day 9 JIT 的延伸**:JIT 生成的 unwind info 是 SEH 派发的基础。下一次进入 **Phase 2 第 4 课:线程池与 TaskScheduler** —— TaskScheduler 与今天的异常处理天然耦合(Task 通过 TrySetException 把异常传给等待方,机制完全不同于 try/catch)。
主人看完可以随便问:
- 想深入哪一层?(CIL 异常表结构 / SEH 内部细节 / ExceptionDispatchInfo 实现 / 4 方案对比中的具体一个)
- Q1 / Q2 / Q3 哪道题想再聊?
- 有 WPF / 金融交易 / 高性能服务的实际场景想套这个架构?