news 2026/10/15 5:44:57

仓颉 stdx.effect 异常体系详解:DoubleResumeException 与 UnhandledCommandException 的触发场景与源码原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
仓颉 stdx.effect 异常体系详解:DoubleResumeException 与 UnhandledCommandException 的触发场景与源码原理
  • 标准库
  • 网络
  • 密码学
  • 并发编程

【免费下载链接】cangjie_stdx

仓颉编程语言提供了 stdx 模块,该模块提供了网络、安全等领域的通用能力。

项目地址:https://gitcode.com/Cangjie/cangjie_stdx
点击查看免费下载

Effect Handlers(效应处理器)是仓颉语言stdx.effect包提供的一项实验性非局部控制能力,它允许开发者通过perform触发效应、通过handle处理效应,并在处理完毕后用resume恢复被中断的计算。为了保证程序状态的一致性,这套机制配套定义了两个专用异常:DoubleResumeException与UnhandledCommandException。本文以 effect_package_exceptions.md 为主体,结合stdx.effect的源码实现,完整剖析这两个异常的 API 定义、默认消息、触发场景、底层调用链以及实际编码中如何捕获与规避它们,帮助读者在使用 Effect Handlers 时写出更健壮的代码。

背景:为什么 Effect Handlers 需要专用异常

Effect Handlers 与传统的异常处理(throw/catch)在形态上非常相似:异常可以被抛出并被捕获,效应则通过perform触发、通过handle处理。但两者有一个本质区别——当异常被捕获时,触发异常的计算过程已经中止,无法恢复;而当效应被处理时,Handler 可以选择通过resume将控制权返回至perform执行点,让中断的计算继续运行(详见 effect 包总览)。

正是由于这种"可恢复"语义,引出了两个新的正确性边界:

  1. 一个Resumption只能被恢复一次。如果同一个恢复点被resume两次,程序状态将变得不一致,行为不可预期,此时需要抛出DoubleResumeException。
  2. 每个被perform的效应都必须有对应的处理逻辑。如果某个效应没有被任何 Handler 捕获,计算将无法继续,此时需要抛出UnhandledCommandException作为运行时安全兜底。

stdx.effect包在 effect.cj 中定义了这两个异常类,它们均直接继承自仓颉标准库的Exception,是整个效应机制运行时安全性的重要组成部分。

异常类全景:API 定义、默认消息与父类型

class DoubleResumeException

public class DoubleResumeException <: Exception { public init() }

核心语义:当尝试对一个已经resume(恢复)过的Resumption再次进行恢复时,抛出此异常。Effect Handlers 的语义要求每个Resumption最多只能被恢复一次,以防止程序状态不一致或出现不可预期的行为。

父类型:Exception

构造函数init()

public init()

无参构造函数,使用默认信息"Cannot resume: resumption has already been resumed."初始化该异常对象。也就是说,抛出此异常后,通过exn.message读取到的内容就是上述默认文案,无需也不支持传入自定义消息。

对应的源码实现位于 effect.cj:

public class DoubleResumeException <: Exception { public init() { super("Cannot resume: resumption has already been resumed.") } protected override func getClassName(): String { "DoubleResumeException" } }

class UnhandledCommandException

public class UnhandledCommandException <: Exception { public init() }

核心语义:当某个 Command(即某个效应)被perform触发,但没有任何 Handler 对其进行处理时,抛出此异常。它是一个运行时安全机制,提示开发者必须为该效应提供明确的处理逻辑。

父类型:Exception

构造函数init()

public init()

无参构造函数,使用默认信息"Unhandled command: no enclosing handler handles this command."初始化异常对象。

对应的源码实现位于 effect.cj:

public class UnhandledCommandException <: Exception { public init() { super("Unhandled command: no enclosing handler handles this command.") } protected override func getClassName(): String { "UnhandledCommand" } }

两个异常类的共性

维度DoubleResumeExceptionUnhandledCommandException
父类型ExceptionException
构造方式无参init()无参init()
默认消息Cannot resume: resumption has already been resumed.Unhandled command: no enclosing handler handles this command.
getClassName()返回值"DoubleResumeException""UnhandledCommand"

值得注意,两个类都通过protected override func getClassName()返回类名。这是 cangjie_stdx 中异常类的通用模式(如 tar_exception.cj、zlib_exception.cj、log_package_exceptions.md 等),用于在格式化异常信息时输出准确的类名,便于日志与排查。

源码原理:两个异常究竟在何处被抛出

兜底出口:Command.defaultImpl()

要理解UnhandledCommandException的抛出路径,必须先看Command的默认实现。在 effect.cj 中:

public abstract class Command<Res> { public open func defaultImpl(): Res { throw UnhandledCommandException() } }

Command<Res>是表示"效应"的抽象基类,泛型参数Res是该效应被恢复(resume)后perform表达式返回的结果类型。defaultImpl()是效应的默认处理器:

  • 默认行为:直接throw UnhandledCommandException(),这正是"未处理效应"异常的总出口;
  • 可覆盖行为:在具体的Command子类中override该方法,可以为效应提供默认实现,从而避免抛出该异常。

从源码结构看,throw UnhandledCommandException()位于defaultImpl()中,而defaultImpl()又是perform分发链的最终兜底,因此"没有任何 Handler 处理该效应"与"抛出 UnhandledCommandException"在实现上是一体的。

分发流程:HandlerFrame.perf 的逐帧匹配

perform的实际执行逻辑位于 common.cj 的HandlerFrame.perf静态方法:

public static func perf<Cmd, Res>(cmd: Cmd): Res where Cmd <: Command<Res> { let mainBox = getActiveFrameBox() var maybeFrame = getActiveFrame() while (let Some(frame) <- maybeFrame) { for (i in 0..frame.handlers.size) { if (let Some(result) <- frame.handlers[i].tryHandle(cmd, box: mainBox)) { return result } } maybeFrame = frame.parent } cmd.defaultImpl() }

这段代码揭示了UnhandledCommandException的完整触发链路:

  1. getActiveFrame()通过线程局部存储(ThreadLocal<FrameBox>,见 common.cj)取到当前活动的最内层 Handler 帧;
  2. 从最内层帧开始,逐帧遍历该帧注册的所有HandlerCase(处理用例),调用tryHandle尝试匹配并处理当前效应;
  3. 若当前帧没有匹配的 Handler,则沿frame.parent链向外层帧继续查找;
  4. 如果所有帧都未命中,最终执行cmd.defaultImpl()——由于默认实现是throw UnhandledCommandException(),异常在此处被抛出。

这解释了文档中的表述"当某个效应被perform但没有任何 handler 处理时抛出",也印证了"运行时会匹配最近的 handle 子句"的语义:匹配顺序是从最内层帧逐级向外,直到最外层。

DoubleResumeException 的抛出位置

与UnhandledCommandException不同,DoubleResumeException并没有出现在当前仓库可见的.cj源码的显式throw语句中。原因在于:Effect Handlers 是编译器特性,resume等关键字的执行语义由编译器展开(desugar)为对stdx.effect内部私有符号的调用。这一点在 unused/unused.cj 的注释中有明确说明——"使用stdx.effect需要编译器将 effects 语法 desugar 为涉及stdx.effect中私有条目的表达式"。可以推断:对"同一Resumption只能恢复一次"的检查就发生在编译器展开后的恢复逻辑中,一旦检测到重复恢复,便构造并抛出DoubleResumeException(类定义位于 effect.cj)。

因此在使用层面可以这样理解:这两个异常一个由库代码显式抛出(未处理效应),一个由编译器展开后的恢复逻辑抛出(重复恢复),开发者只需按统一的Exception捕获方式处理即可。

实战:什么时候会看到这些异常

场景一:效应未被任何 Handler 处理 → UnhandledCommandException

这是最常见的触发场景。仓库自带的示例 simple_immediate_effect.md 完整演示了如何捕获它:

import stdx.effect.{Command, UnhandledCommandException} // 定义两个效应类型:生成随机数和发送消息 class Random <: Command<Int64> {} class Send <: Command<Unit> { Send(let msg: String) {} } main() { try { // 触发 Random 效应 let x = perform Random() println("Got random number ${x}, sending...") // 触发 Send 效应 perform Send("Sending ${x}") } handle(_: Random) { // 处理 Random 效应,立即返回 42 resume with 42 } catch(_: UnhandledCommandException) { // 如果某个效应没有被处理(这里是 Send),会抛出该异常 println("Attempting to perform an unhandled command") } }

输出:

Got random number 42, sending... Attempting to perform an unhandled command

运行过程分析:Random效应被handle捕获并resume with 42,perform Random()得到42;随后perform Send("Sending 42")触发Send效应,但当前try只注册了Random的 Handler,Send没有匹配项,分发链走到底后执行defaultImpl(),抛出UnhandledCommandException,随后被紧随其后的catch块捕获。

这里体现了一个重要实践:UnhandledCommandException是Exception的子类,可以用catch块像捕获普通异常一样捕获它,为"未处理效应"提供兜底策略;但它更推荐的使用方式是通过defaultImpl覆盖或注册 Handler 来主动避免。

场景二:对同一 Resumption 重复 resume → DoubleResumeException

当一个Resumption已经通过resume with value恢复过一次后,若再次对其执行resume(例如在回调或异步场景中误用同一个恢复点),编译器展开后的恢复逻辑会检测到重复恢复并抛出DoubleResumeException,默认消息为"Cannot resume: resumption has already been resumed."。

编码建议:

  • 把"一个 Resumption 只允许 resume 一次"视为与"资源只能释放一次"同级的正确性约束;
  • 在可能多次回调的场景中,先判断恢复点是否已被消费,或为恢复操作包一层幂等保护;
  • 与处理普通异常一致,可以通过catch (exn: DoubleResumeException)捕获并记录日志,帮助定位重复恢复的调用来源。

场景三:覆盖 defaultImpl,从源头避免 UnhandledCommandException

文档 effect_package_classes.md 提供了一个完整的计数示例,其中GetDepth.defaultImpl返回0,表示"在没有显式 Handler 的情况下深度为 0"。由于defaultImpl被覆盖为总是返回一个值,整个程序永远不会抛出UnhandledCommandException,其行为等价于将main的主体包裹在try { ... } handle(_: GetDepth) { resume with 0 }中。

import stdx.effect.* public class GetDepth <: Command<Int64> { public func defaultImpl(): Int64 { 0 } } func deeper<T>(f: () -> T): Unit { try { f() } handle(e: GetDepth) { resume with ((perform e) + 1) } } main() { println("Depth: ${perform GetDepth()}") deeper { println("Depth: ${perform GetDepth()}") deeper { println("Depth: ${perform GetDepth()}") } println("Depth: ${perform GetDepth()}") } println("Depth: ${perform GetDepth()}") }

输出:

Depth: 0 Depth: 1 Depth: 2 Depth: 1 Depth: 0

类似地,effect_package_overview.md 中的示例三给出了更直观的默认实现写法:

import stdx.effect.Command class Print <: Command<Unit> { Print (let msg: String) {} override func defaultImpl(): Unit { println(msg) } } main() { perform Print("No handler here") // 调用默认实现 }

输出:

No handler here

继承自 Exception 的公共能力

由于两个异常类都继承自Exception,它们天然具备仓颉异常体系的通用能力:

  • message属性:读取构造函数中传入的默认消息(即上文列出的两段默认文案),可用于日志输出与用户提示;
  • getClassName():protected方法,返回异常类的确切名称("DoubleResumeException"/"UnhandledCommand"),供异常格式化输出使用;
  • 与catch块协作:可在try表达式中通过catch (exn: DoubleResumeException)或catch (exn: UnhandledCommandException)精确捕获;也可用catch (exn: Exception)作为更宽泛的兜底;
  • 在 Handler 中抛出:resume throwing exn允许 Handler 在恢复前向perform处抛出异常,exn必须是Exception或Error的实例(或其子类),这两个异常类同样满足该约束。

编译前提与工具链支持

Effect Handlers 是一项实验性功能,使用这两个异常类(乃至整个stdx.effect包)前需要满足以下前提(详见 effect 包总览 的说明):

  • 编译器:需要支持 Effect Handlers 机制的仓颉编译器;
  • 编译选项:编译含 Effect Handlers 的代码时必须启用--experimental与--enable-eh;
  • 导入方式:import stdx.effect.{Command, UnhandledCommandException}或import stdx.effect.*;
  • 链接依赖:stdx.effect是独立的静态库目标(见 CMakeLists.txt),在 libs_overview.md 的依赖表中仅依赖自身,无额外包依赖;使用 cjc 手动链接时加入-lstdx.effect即可(完整命令见 libs_overview.md)。

工具链支持现状(来自官方文档):

工具支持情况
Cjdb已可正常使用
LSP暂不识别与 Effect Handlers 相关的关键字
Formatter尚未支持
Coverage尚未支持
Cjlint尚未支持

此外,stdx.effect包中还有一个特殊的 unused 子包,其唯一用途是确保 cjpm 调用 cjc 构建父包时不使用--no-sub-pkg标志,从而使编译器 desugar 效应语法时所需的私有符号能够暴露在构建产物中。这是包结构层面的工程细节,不影响开发者对这两个异常类的正常使用。

小结

stdx.effect包通过两个专用异常为 Effect Handlers 的正确性提供保障:

  • UnhandledCommandException:效应被perform但无任何 Handler 捕获时抛出,兜底出口是 effect.cj 中Command.defaultImpl()的默认实现,触发链路是 common.cj 中HandlerFrame.perf逐帧匹配失败后的最终回退;
  • DoubleResumeException:同一Resumption被重复恢复时抛出,其检查逻辑由编译器展开后的恢复代码承担,类定义位于 effect.cj。

在实际开发中,优先通过覆盖defaultImpl()提供默认行为、或为每个效应注册明确的 Handler 来避免这两个异常;同时在边界场景(未覆盖的效应、可能重复的恢复点)用catch块做好兜底与日志记录。结合--experimental、--enable-eh编译选项,即可安全地使用这套实验性的非局部控制能力。

  • 标准库
  • 网络
  • 密码学
  • 并发编程

【免费下载链接】cangjie_stdx

仓颉编程语言提供了 stdx 模块,该模块提供了网络、安全等领域的通用能力。

项目地址:https://gitcode.com/Cangjie/cangjie_stdx
点击查看免费下载

相关推荐

上一篇:深度解析:Qwen3.6-27B-AEON-Ultimate-Uncensored-BF16大语言模型部署与优化实战指南
下一篇:【亲测免费】 VacuumRobot 开源项目教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 5:44:37

AirLLM实战:单卡24GB显存如何跑通70B大模型推理

1. 当大模型装不进显卡时&#xff0c;我在想什么第一次看到 airllm 这个项目名的时候&#xff0c;我正对着一台只有单张 24GB 显存的机器发愁。手上有一个 70B 参数级别的开源模型想跑推理&#xff0c;按常规思路&#xff0c;要么上多卡并行&#xff0c;要么量化到 4bit 以下&a…

作者头像 李华
网站建设 2026/10/15 5:44:37

AI逆向工程实战:用大语言模型自动反编译二进制代码

1. 这个项目到底在做什么第一次看到“AI搞逆向工程”这个说法&#xff0c;我脑子里冒出来的第一个念头是&#xff1a;这玩意儿靠谱吗&#xff1f;逆向工程这活儿&#xff0c;干过的都知道&#xff0c;它不像写业务代码那样有明确的输入输出&#xff0c;更多时候靠的是经验、直觉…

作者头像 李华
网站建设 2026/10/15 5:44:19

AD9513时钟抖动控制与三路延时对齐实战指南

简介&#xff1a;本资源为ADI公司AD9513高性能时钟分配芯片的官方中文数据手册PDF版&#xff0c;面向嵌入式硬件工程师、高速ADC/DAC系统设计人员及射频与精密仪器开发从业者&#xff0c;解决低抖动时钟信号分配、多路相位对齐与可编程分频等关键设计难题。手册全面覆盖器件架构…

作者头像 李华
网站建设 2026/10/15 5:43:52

在线教育公司校招技术笔试攻略:题型分布、高频考点与备考路线

每年校招季&#xff0c;都会有不少学弟学妹来问我同样的问题&#xff1a;在线教育公司的技术类笔试到底怎么准备&#xff1f;算法题占多少&#xff1f;计算机基础考不考&#xff1f;要不要专门去刷那些脑筋急转弯式的逻辑题&#xff1f;说实话&#xff0c;我一开始也以为这类公…

作者头像 李华
网站建设 2026/10/15 5:43:35

Vivado HLS开发必读:UG871中文版核心实践指南

简介&#xff1a;本资源是Xilinx官方《Vivado HLS教程》UG871中文版&#xff08;v2020.1&#xff09;完整PDF文档&#xff0c;面向FPGA开发初学者及Zynq平台工程师&#xff0c;聚焦高层次综合&#xff08;HLS&#xff09;技术入门与实践&#xff0c;解决传统Verilog/VHDL开发周…

作者头像 李华
网站建设 2026/10/15 5:43:14

读书无用是误解:呼吸式阅读,让碎片时间成为灵魂的养料

读书无用——读书不是修行&#xff0c;是呼吸&#xff1a;在尘埃里种花&#xff0c;让灵魂长出触角这个标题乍一看像是一句丧气话。“读书无用”&#xff0c;四个字扎眼得很。但仔细品一下&#xff0c;它其实是一句反话&#xff0c;是替所有被“读书改变命运”“读书要有产出”…

作者头像 李华