Menu

Zero Raises 和 Check:不用异常的显式错误处理

Zero 函数用 raises 声明自己可能如何失败,调用方用 check 明确承认这一点。本文介绍这套机制如何运作、为什么不会有悄悄抛出的错误,以及它如何与 World 能力对象配合。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

基本思路

在 Zero 中,失败不是一条独立的、平行的控制流。它是常规控制流的一部分:在函数签名中声明,在每个调用处被承认。这由两部分实现:

  • 函数签名中的 raises:“这个函数可能失败。”
  • 调用处的 check:“如果这里失败了,就让我的函数以同样的错误失败。”

两者结合,足以表达大多数语言要靠 try/catch 或 Result 类型才能处理的情况。

声明可能失败的函数

在返回类型后面加上 raises:

fun validate(ok: Bool) -> i32 raises { InvalidInput } {
    if ok == false {
        raise InvalidInput
    }
    return 42
}

这个签名有两种读法:

  • “validate 返回一个 i32,或者抛出 InvalidInput。”
  • “可能的结果集合是 { i32, InvalidInput }。”

两种读法都正确。编译器会跟踪这两种可能,并要求调用方对每一种都做出明确处理。

你也可以写单独的 raises 子句:

pub fun main(world: World) -> Void raises {
    check world.out.write("hello\n")
}

不带错误列表的 raises 表示“这个函数可能以任何错误失败”。在 main 上这是约定的写法:只要出了任何问题,程序就会以非零状态码退出,运行时负责把错误呈现出来。

对于调用栈更深处的函数,优先使用显式形式 raises { ErrorA, ErrorB },这样每一层的失败方式都有据可查。

抛出错误

在可能失败的函数中,raise 会带着指定的错误退出函数:

fun validate(ok: Bool) -> i32 raises { InvalidInput } {
    if ok == false {
        raise InvalidInput
    }
    return 42
}

raise InvalidInput 会在这一行产生一个 InvalidInput 错误。函数不会继续往下执行:控制权返回给调用方,调用方看到的是错误,而不是 i32。raises 子句列出了这个函数唯一允许抛出的错误类型;抛出不在列表中的错误会导致编译错误。

用 check 传递错误

调用可能失败的函数时,调用方必须承认失败的可能。最常见的方式就是 check:

fun run() -> Void raises { InvalidInput } {
    check validate(true)
}

check validate(true) 做了两件事:

  1. 调用 validate(true)。
  2. 如果 validate 抛出了错误,就把它向上传递:run 会向它自己的调用方抛出同样的错误。

要允许这样传递,run 必须在自己的 raises 子句中声明它可能抛出 InvalidInput(或兼容的错误)。编译器会检查这一点。如果 run 的签名写的是 raises { OtherError },这次传递就无法通过编译,因为 InvalidInput 不在集合中。

下面是语言官方示例中的一个完整例子,点击“运行”看看错误传递如何顺利完成:

错误类型随着函数签名一路沿调用栈向上传递。main 单独的 raises 接受 run 可能抛出的任何错误,所以传递能安全落地。

为什么不用 try/catch?

raises/check 背后的设计原则是:在调用处,失败永远不会是不可见的。在使用 try/catch 的语言中,异常可以悄悄穿过一个甚至不知道自己牵涉其中的函数:这个函数看起来很纯粹,但它函数体深处的某个调用抛出了异常,异常就从它身上展开穿过。

这对写抛出代码的人来说很方便,对其他所有人来说代价却很高:

  • 读者(和智能体)无法从签名判断一个函数是否涉及失败路径。
  • 重构变得提心吊胆:把一个调用移到另一个函数里,就可能改变哪些异常能够到达。
  • 恢复代码离真正知道该怎么处理的地方很远。

Zero 选择提前付出代价:每个可能失败的函数都要加标注,每个可能失败的调用都要写 check,换来的是“函数涉及哪些失败方式,从签名就能看出来”这一性质。人类和智能体都可以依赖这一点。

为什么不直接用 Result<T, E>?

你也可以用 choice 来表达同样的意思:一个带有 ok 和 err 变体的 Result<T, E> 类型。Zero 也提供这种写法;当失败是你想要检查、存储或四处传递的数据时,它是很有用的工具。

raises/check 增加的是针对常见情况的语法层约定:“如果这里失败了,就让我的函数以同样的方式失败。”没有它,每次调用都要包在一个 match 里,而这个 match 几乎总是把错误重新打包进调用方自己的 Result。check 就是这种写法的捷径,并由编译器保证传递在类型上是正确的。

所以:

  • Result<T, E>(一个 choice):当你想把失败作为值来检查或携带时。
  • raises + check:当你只想把失败沿调用栈向上传递时。

两者都可以用,它们满足的是不同的使用需求。

多种错误类型

一个函数可以抛出不止一种错误:

fun parse(input: String) -> i32 raises { Empty, Malformed } {
    if std.mem.len(input) == 0 {
        raise Empty
    }
    // ... parsing logic ...
    raise Malformed
}

调用方可以:

  • 如果自己的签名同时列出了 Empty 和 Malformed(或它们的超集),就用 check parse(input) 向上传递。
  • 用 match,或者语言为此提供的 try 式结构,显式处理其中一种或两种错误。

精细处理的确切语法(匹配特定错误类型,同时传递其他错误)是 1.0 之前的 Zero 中可能变动的部分之一。而契约本身,即函数可能抛出的每种错误类型都写在签名里,是稳定的部分。

一个思维模型

Zero 的失败处理机制,是每门命令式语言都有的那套机制的严格诚实版本:

概念Try/CatchZero
把函数标记为可能失败无(隐式)raises { ... }
抛出错误throw eraise E
传递给调用方悄无声息地向上冒泡check call(...)
就地处理try { ... } catch(e) { ... }对返回的 Result 使用 match,或者带类型的处理形式

行为上的差别很小,标注上的差别却很大,而且是有意为之。副作用是显式的,失败也是一种副作用。

风格建议

  • 大方地使用 check。当你在这一层没有特别要做的事时,它就是正确的默认选择。
  • 内部辅助函数避免使用单独的 raises。错误集合越窄,签名就越有用。
  • 把可能失败的操作和它们需要的能力对象放在一起。使用 World 向 stdout 写入的函数几乎总是需要 raises,因为写入可能失败。

下一步:JSON 诊断

raises/check 与 Zero 的编译器诊断协同工作:当你对一个没有声明正确错误的函数使用 check 时,编译器会以结构化的形式准确告诉你哪里出了问题。下一篇文档介绍 JSON 诊断,也就是智能体用来修复代码的机器可读信息。

常见问题

Zero 中的 raises 是什么意思?

函数签名上的 raises 声明这个函数可能失败。单独的 raises 允许任何错误类型。raises { InvalidInput } 这样的具体形式则规定函数只能以列出的错误类型失败。调用方必须承认失败的可能,要么用 check,要么用其他显式的处理形式。

check 运算符有什么作用?

check expr 会计算 expr,如果产生了错误,就把错误向上传给当前函数的调用方。可以把它理解为“执行这个,如果失败,就让我的调用方以同样的错误失败”。要让传递被调用方的 raises 子句允许,调用方自己也必须声明可能抛出兼容的错误。

Zero 中怎么抛出错误?

在 raises 子句包含该错误的函数中使用 raise ErrorName。示例:if ok == false { raise InvalidInput }。函数会在这里退出,错误成为函数的结果,调用方可以对它使用 check。

Zero 为什么不用 try/catch?

try/catch 让异常可以悄悄穿过那些对它毫不知情的函数。Zero 的设计要求每个函数的签名都必须承认它所涉及的失败方式。没有隐藏的控制流:函数可能失败,签名就会写明,每个调用方都必须用 check 明确承认。

Zero 函数可以抛出多种错误类型吗?

可以。把它们列在 raises { ... } 子句中,用逗号分隔(或者遵循当前 Zero 版本的错误集合语法)。可能出现的错误集合是函数契约的一部分,就像参数类型和返回类型一样。调用方可以对抛出的是哪种错误做模式匹配,也可以直接用 check 向上传递。

Coddy 编程语言插图

和 Coddy 一起学编程

开始