- 标准库
- 网络
- 密码学
- 并发编程
【免费下载链接】cangjie_stdx
仓颉编程语言提供了 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 包总览)。
正是由于这种"可恢复"语义,引出了两个新的正确性边界:
- 一个
Resumption只能被恢复一次。如果同一个恢复点被resume两次,程序状态将变得不一致,行为不可预期,此时需要抛出DoubleResumeException。 - 每个被
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" } }两个异常类的共性
| 维度 | DoubleResumeException | UnhandledCommandException |
|---|---|---|
| 父类型 | Exception | Exception |
| 构造方式 | 无参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的完整触发链路:
getActiveFrame()通过线程局部存储(ThreadLocal<FrameBox>,见 common.cj)取到当前活动的最内层 Handler 帧;- 从最内层帧开始,逐帧遍历该帧注册的所有
HandlerCase(处理用例),调用tryHandle尝试匹配并处理当前效应; - 若当前帧没有匹配的 Handler,则沿
frame.parent链向外层帧继续查找; - 如果所有帧都未命中,最终执行
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 模块,该模块提供了网络、安全等领域的通用能力。
相关推荐
cangjie_stdx stdx.effect 异常机制详解:DoubleResumeException 与 UnhandledCommandException 的触发场景与源码剖析
cangjie_stdx stdx.effect 异常机制详解:DoubleResumeException 与 UnhandledCommandExceptio
标准库网络密码学并发编程仓颉 std.time 包异常体系详解:InvalidDataException 与 TimeParseException 的触发场景与处理实践
仓颉 std.time 包异常体系详解:InvalidDataException 与 TimeParseException 的触发场景与处理实践 在仓颉语言标准
编程语言语言运行时标准库垃圾回收并发编程仓颉标准库 std.io 异常体系深度解析:ContentFormatException 与 IOException 的触发场景与实战处理
仓颉标准库 std.io 异常体系深度解析:ContentFormatException 与 IOException 的触发场景与实战处理 std.io 是仓颉
文档知识库教程OpenHarmony
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考