异常

异常是语言对异常完成的控制流建模:当计算无法产生正常结果时,异常机制选择一条独立的异常续延,将处置权转移到具备相应语义与能力的处理边界。

这篇文档讨论编程语言层面的异常。

第一性原理:异常为何成为语言级概念

现实计算具有部分性

函数通常被简化为从输入到输出的映射:

输入 -> 输出

这一模型隐含了一个前提:计算总能按照约定产生结果。现实程序还会受到输入、状态、资源和外部环境的约束,因而存在三种基本结局:

完成形态 含义 控制流结果
正常完成 产生契约约定的结果 进入正常后继计算
异常完成 无法沿当前契约继续,但能够发出信号 转移到异常处理路径
不终止 计算持续进行而没有结果 不进入任何后继计算

异常机制处理的是第二种。它没有消除失败,只是使“当前计算无法正常完成”成为可表达、可传播、可约束的程序语义。

失败检测点与处置决策点分离

异常产生的根本矛盾是:

底层计算知道发生了什么,上层边界才知道这意味着什么以及应该如何处置。

检测点掌握局部事实,例如某项前提不成立或某个操作无法完成;它通常缺少调用目标、替代方案和全局状态等上层语义。处理边界拥有这些语义,却远离实际发生位置。

两者之间往往存在多层调用关系。中间层既无法恢复,也不应理解底层失败,只承担传递责任。

显式转发造成弥漫性修改

若异常完成只能通过普通返回值表达,每一层都必须:

  1. 扩展自身的返回契约;
  2. 检查被调用计算是否失败;
  3. 在无力处理时继续转发;
  4. 保证后续正常逻辑不再执行。

失败传播因而侵入整条调用链。任何底层失败类型的变化,都可能迫使无处置能力的中间层同步修改。

异常把这项重复责任转交给语言及其执行系统:中间计算专注于正常组合,异常完成沿独立通道自动寻找处理边界。

异常的价值与代价同源

机制效果 获得的价值 同时产生的代价
正常路径与异常路径分离 正常计算保持线性和可组合 失败控制边不再完全显式
自动跨层传播 中间层免于机械转发 局部代码难以看全潜在出口
延迟处置决策 由拥有语义的边界决定策略 处理距离过远时上下文可能损失
非局部控制转移 迅速退出无效计算 被跳过作用域必须完成资源和状态收束

异常的核心权衡可以概括为:

让运行时自动传播失败,避免每一层都手工检查和转发;代价是失败路径不再能从局部代码中直接看清。

与相邻概念的关系

语言异常位于“发现问题”和“决定怎样处理”之间:发现问题的代码发出异常,异常沿调用链向上传递,直到遇到真正知道怎样处理它的代码。

相邻概念 与语言异常的关系
错误条件 错误条件是实际发生的问题,例如输入无效或资源不可用;异常是程序报告这类问题的一种方式。同一个问题既可能作为普通结果返回,也可能触发异常,取决于当前操作原本承诺什么
处理策略 异常负责把问题送到合适的位置,处理策略决定收到异常后怎样做,例如重试、提供替代结果、转换异常或结束当前任务
硬件与操作系统异常 处理器和操作系统也会在遇到除零、非法指令等问题时改变执行方向;语言运行时可以把其中一部分转换成语言异常,但两者发生的层次和处理者不同
进程间失败 语言异常通常只能在同一进程的调用链中传播。跨越进程或网络时,需要先把失败写入响应、消息或任务状态,再由接收方决定是否转换成异常

是否适合使用异常,不取决于问题是否少见或严重,而取决于当前操作还能否按承诺完成。如果某种结果本来就需要调用者正常处理,更适合使用普通返回值;如果当前代码无法处理而上层可能处理,异常通常更合适。

异常传播模型:从异常完成到处理边界

语义层:双续延模型

双续延模型把正常返回与异常抛出统一为计算结束后的两种去向,是解释异常为何中断正常路径并自动传播到处理者的最小模型。由此还可以继续推导栈展开、资源收束和处理边界等机制。

续延表示“当前计算结束后还要执行什么”。一个可发生异常完成的计算同时面对两条续延:

续延 输入 职责
正常续延 正常结果 继续完成调用者原定目标
异常续延 异常信号 转移到能够解释该信号的处理边界

因此,计算的完整语义可以表示为:

计算 = 正常完成(结果, 正常续延) | 异常完成(信号, 异常续延)

普通返回选择正常续延;异常抛出选择异常续延。异常机制的独特之处在于:异常续延通常由当前执行上下文隐式提供,抛出点无需显式知道最终处理者。

结构层:异常系统的四个要素

要素 核心问题 职责
异常信号 发生了什么 携带足以识别失败语义的信息
异常续延 控制权去哪里 将异常完成送往处理路径
处理边界 谁可以决策 限定可处理的信号及其作用域
上下文收束 被跳过的计算怎么办 退出失效作用域并维护必要不变量

这四个要素构成异常机制的骨架。异常对象、标签、模式匹配、栈表和清理语法都属于具体实现选择。

过程层:异常传播的生成过程

异常传播可以从双续延模型及四个要素推导为六个阶段:

  1. **识别异常条件**:当前计算确认无法沿正常契约完成;
  2. **生成异常信号**:把局部事实编码为可识别的失败语义;
  3. **选择异常续延**:停止当前正常路径,转向活动处理边界;
  4. **匹配处理者**:判断当前边界是否理解并愿意承担该信号;
  5. **展开执行上下文**:越过无处置能力的中间计算,执行必要收束;
  6. **形成处置结果**:恢复为正常完成、转换后传播、原样传播或终止。

处理者搜索、传播和上下文展开并非互不相关的功能。它们共同实现一件事:把异常完成从发现位置安全地交给决策位置。

决策层:处理边界的五种结果

结果 含义 成立条件
恢复 建立新的合法状态并回到正常路径 处理者拥有恢复语义与所需资源
替代 用降级结果满足较弱契约 当前契约允许明确的替代语义
转换 改写为当前抽象层理解的异常信号 跨越了抽象或模块边界
传播 当前边界不作决策 更外层可能拥有处置能力
终止 放弃当前计算单元 已不存在可信、安全的继续路径

部分异常模型还允许处理后返回原抛出点继续执行。它们改变的是续延语义,并未改变“信号、续延、边界、收束”这组核心要素。

异常正确处理的成立条件

异常传播转移了控制权,正确处理还依赖四项条件:

模型要素 成立条件 判断规则
异常信号 语义可解释 信号应支持接收者判断;与外层契约不匹配时才转换,并保留根因
异常续延 传播路径可达 失败应到达明确的责任边界;执行关系不连续时,通过任务结果、消息或协议交接
处理边界 处置能力匹配 具备语义、权限与恢复能力时处理,否则继续传播或明确终止
上下文收束 必要不变量保持 异常退出仍须履行资源与状态义务;是否恢复原状取决于契约,异常本身不提供回滚

契约可见性贯穿上述条件:自动传播隐藏了部分失败路径,接口应公开影响调用者决策的失败语义、状态保证与接收责任,使调用者能够预先安排处理。

失败处理机制的设计空间

失败处理不是单一固定方案。不同语言的设计可以在三个稳定维度中理解:

维度 回答的问题 工程意义
承载通道 失败作为返回值,还是中断正常控制流? 权衡调用者显式处理的负担与跨层自动传播的便利
契约可见性 调用者能否从接口得知可能发生哪些失败? 权衡局部可推理性与声明、类型的维护成本
延续语义 处理后放弃原计算,还是回到发生点继续? 决定所需的状态保证与恢复语义

三个维度可以分别分析,但并非完全独立:结果类型通常同时提高失败契约的可见性;可恢复语义会改变上下文保留与收束的要求。值通道严格说是异常机制的替代方案,纳入比较是为了覆盖完整的失败处理谱系。

承载通道:值通道与控制效果通道

通道 异常完成如何表达 优势 代价
普通值通道 将正常结果与失败结果共同表示为返回值 契约显式、易于局部推理和组合 每层必须参与检查、转换或传播
控制效果通道 异常信号绕过普通返回路径 正常路径简洁,中间层可保持透明 控制依赖隐式,组合与推理更困难

两者表达的是同一个语义事实:计算可能正常完成,也可能异常完成。差异在于这两个分支由值系统承载,还是由控制系统承载。

契约可见性:隐式与类型化

方式 失败契约 复杂性承担者 主要代价
动态隐式 调用点无需声明全部异常效果 运行时与开发者经验 潜在出口难以穷举
声明式 接口声明可能发生的异常完成 接口设计者与调用者 声明可能沿调用链扩散
类型化效果 类型系统跟踪并组合异常效果 类型系统、编译器与工具链 类型表达和推导复杂度上升

契约越显式,局部推理能力通常越强,调用与演进成本也越高。这是语言设计中的持续权衡,不存在脱离场景的最优端点。

延续语义:终止式与可恢复式

语义 处理后的控制位置 适用前提 风险
终止式 放弃当前计算,从处理边界继续 抛出点后的局部状态不再可信或无需保留 回退范围大,清理义务重
可恢复式 处理后返回原异常位置继续 异常条件可被修复,恢复协议定义明确 控制关系复杂,处理者与抛出点耦合

多数工程场景倾向终止当前续延,因为它更容易维护局部状态不变量。可恢复语义适用于具有明确交互协议的条件处理系统。

复杂性不会消失,只会转移

设计选择 复杂性主要落点
显式失败值 调用者与组合逻辑
隐式异常效果 运行时与调试过程
声明或类型化效果 接口、类型系统与编译器
直接终止 外层恢复单元与运行环境

异常机制的演进没有持续减少复杂性,而是在正常路径、失败契约、运行时和类型系统之间重新分配复杂性。

非局部控制的正确性义务与代价

异常机制只负责转移控制权。一次异常处理是否正确,取决于非局部退出过程中能否维护三个不变量。

状态与资源完整性

异常发生后,被跳过作用域中的资源和可变状态必须保持可解释:

异常机制本身不提供事务性回滚。它只提供退出与收束的控制机会,状态恢复仍依赖作用域管理、不可变性、补偿或事务等其他机制。

可以按失败后的承诺强度区分三类保证:

保证 异常完成后的承诺
基本保证 无资源泄漏,对象仍处于合法但未必不变的状态
强保证 对外效果等价于操作从未发生
不失败保证 指定操作在其契约范围内不会异常完成

保证越强,对状态隔离、暂存、回滚能力和资源协议的要求越高。

因果信息完整性

异常信号需要支持从结果追溯原因。有效的因果信息通常包括:

上下文不足会使异常无法诊断;上下文无边界累积则会泄露内部细节并制造噪声。原则是在抽象边界补充决策所需信息,同时保留根因引用。

抽象契约完整性

异常是接口契约的一部分,即使语言没有要求显式声明。稳定的异常契约应回答:

  1. 哪些条件意味着当前抽象无法正常完成;
  2. 调用者可以依赖哪些失败语义;
  3. 调用者是否能够恢复,以及恢复后哪些不变量成立;
  4. 哪些实现细节必须被当前抽象屏蔽。

处理位置由能力而非距离决定。合理边界是同时具备以下条件的最低层:

反模式是对不变量的破坏

反模式 被破坏的不变量 结构性后果
捕获后忽略 因果、契约完整性 失败被伪装为成功或无故消失
无能力处理却捕获 抽象契约完整性 决策发生在缺少语义的层次
过宽匹配 状态、契约完整性 不同失败被错误归并到同一策略
逐层重复包装 因果信息完整性 信噪比下降,真实原因被淹没
无新增处置价值地记录后原样传播 因果、契约完整性 同一事件被重复处理和重复记录
用异常表达正常分支 抽象契约完整性 正常领域状态被隐藏在非局部控制流中
清理失败覆盖主异常 状态、因果完整性 原始失败及收束结果均不可判断

成本模型

异常的成本需要分为两个层次:

成本 来源 判断原则
认知成本 隐式出口、非局部控制边、失败契约不完整 由代码规模、传播距离和契约可见性决定
运行时成本 信号构造、上下文捕获、处理者匹配、上下文展开 由具体语言及执行系统决定

正常路径和异常路径的成本结构通常不同,但不能脱离实现断言哪一种机制必然昂贵。更稳定的工程结论是:高频、预期且可组合的结果应优先留在普通值通道,避免用非局部控制机制表达日常分支。

决策边界与知识网络

普通结果、异常与终止的选择

三者按"失败语义应由哪一抽象层承载"划分的三种归属方:

判据只有一处:当前层是否理解该失败的含义,是否拥有处置它的能力与权限。

失败语义的归属 选择 理由
属于调用者应正常处理的契约分支 普通值或显式结果类型 分支可预期,须参与常规组合,不应进入非局部控制流
当前抽象无法履约,上层可能拥有处置策略 异常完成 跨越无处置能力的中间层,把决策权交还上层
系统不变量已破坏,继续执行缺乏可信语义 终止当前恢复单元 错误状态不可收束,须阻止其沿计算链扩散

处理、转换与传播的决策

面对异常信号,当前边界应依次判断:

  1. **能否解释**:是否理解该信号对当前目标的含义;
  2. **能否处置**:是否拥有恢复、替代或终止的权限;
  3. **能否收束**:处理后是否可以建立明确且合法的状态;
  4. **是否越界**:继续传播是否会泄露当前层的实现语义。
判断结果 行为
能解释、能处置、能收束 在当前边界处理
不能处置,外层仍可能处理 保留因果后继续传播
跨越抽象边界 转换为外层理解的语义并保留根因
无法建立可信状态 终止当前恢复单元

执行拓扑决定传播边界

异常传播依赖活动的执行关系:

边界 原异常续延是否连续 边界后的表示
同步调用链 通常连续 沿活动处理边界传播
独立任务或异步阶段 通常断裂 任务记录异常完成,观察者重新触发处理
独立线程 不共享调用续延 在线程终点处理或封装为任务结果
进程与网络 不共享语言运行时 转换为协议定义的失败结果

异常到达这些边界后,发生的是失败语义的重新表示,而非原异常对象自然穿越边界。

演进中的不变张力

错误处理机制长期围绕两项目标摆动:

自动异常传播强化前者,显式结果和类型化效果强化后者。结构化资源管理与结构化并发则试图恢复非局部控制被打破的生命周期边界。演进表现为复杂性承担者的迁移,没有一种表达在所有场景同时取得最大显式性、最低调用负担和最简单运行时。

总结

异常机制的稳定认知可以收束为五点:

  1. 现实计算可能无法产生正常结果,异常使异常完成成为语言语义;
  2. 双续延模型揭示了正常返回与异常传播的统一结构;
  3. 自动传播解决了中间层机械转发,同时引入隐式控制依赖;
  4. 非局部退出必然伴随状态、资源、因果和契约的完整性义务;
  5. 错误值、异常与终止的差异,体现了复杂性在调用者、类型系统和运行时之间的不同分配。

异常设计的核心是让每一种异常完成到达有能力决策的边界,并在传播过程中保持程序状态与因果语义可解释。

关联内容(自动生成)