Skip to content
📖 Auto 编程语言(主书)本书目录

错误处理 ​

错误是软件中不可避免的一部分。文件可能不存在,网络请求可能超时,用户可能在你期望数字时输入了 "abc"。每个程序都必须决定在出错时该怎么做。

Auto 提供了两种类型级别的机制来处理这些情况,灵感来自 Rust,但语法更加简洁:

  • ?T 表示一个可能不存在的值 -- 等价于 Rust 的 Option<T> 或 TypeScript 的 T | null。它有两个变体:Some(T) 和 None。
  • !T 表示一个可能失败的操作 -- 等价于 Rust 的 Result<T, E>。它有两个变体: Ok(T) 和 Err(String)。

与 Rust 一样,Auto 要求你承认错误的可能性并在代码编译前采取行动。这一要求使你的程序更加健壮,确保你处理错误而不是默默忽略它们。

在本章中,我们将讨论:

  • 处理意外情况 -- 为什么错误处理很重要
  • ?T -- 可选值 -- 空安全和可选链
  • !T -- 错误类型 -- 错误传播模型和 ! 运算符
  • 自定义错误 -- 定义你自己的错误类型
  • try / catch -- 显式错误处理
  • 最佳实践 -- 何时使用 ?T vs !T,如何组合使用

处理意外情况 ​

大多数编程语言通过以下三种机制之一来处理错误:

  1. 异常(Java、Python、C#):代码抛出异常,运行时沿着调用栈查找匹配的 catch 块。这很强大但不可见 -- 你无法从函数签名判断它是否会抛出异常。

  2. 错误码(C、Go):函数返回特殊值(如 -1 或 null)表示失败。这很明确但容易被忘记检查。

  3. 类型级错误处理(Rust、Haskell):错误被编码在类型系统中。可能失败的函数返回 Result<T, E> 而不是 T。编译器强制你处理成功和失败两种情况。

Auto 采用第三种方法,但语法比 Rust 更简单。不用写 Result<i32, String>,写 !int 即可。不用写 Option<User>,写 ?User 即可。

好处是巨大的:错误在函数签名中变得可见,编译器在编译时捕获未处理的错误,你永远不会不小心把空值或错误结果当作有效值来使用。

?T -- 可选值 ​

?T 类型表示一个可能存在也可能不存在的值。它有两个变体:

  • Some(value) -- 值存在
  • None(或 none)-- 无值

这与 Rust 的 Option<T>、TypeScript 的 T | null、Python 的 Optional[T] 是同一个概念。

从函数返回 ?T ​

当函数可能没有值可返回时,将返回类型声明为 ?T:

返回 `?User` 的函数

find_user 函数返回 ?User。当 id == 1 时,返回一个 User 值。当 id 是其他值时,返回 none -- 值不存在。

在 Auto 中,当函数返回 ?T 且最后一个表达式是裸值(未用 Some 包装)时,编译器会自动包装它。所以 User { ... } 变成 Some(User { ... })。你只需在无值的情况下显式写 none。

这在各语言中的映射:

  • Rust 使用 Option<User>,配合 Some(...) 和 None。转译器在 Auto 直接访问可选值的字段时生成 .unwrap()。
  • Python 使用 Optional[User] 配合 None。转译器在访问字段前生成 if ... is not None 守卫。
  • C 在结构体中使用 _some 标志。如果 _some 为 0,则值不存在。
  • TypeScript 使用 User | null。转译器在字段访问前生成 null 检查。

可选链 ​

当你有一连串的可选值时 -- 例如一个有可选地址的用户 -- 你可以使用 ?. 运算符安全地访问嵌套字段,而无需手动检查每一层:

使用 `?.` 进行可选链访问

输出:

Shanghai
None

?. 运算符是 Auto 的可选链语法。alice.address?.city 的意思是:如果 address 是 Some,访问 city;如果 address 是 none,短路并返回 none。这与 TypeScript 的 ?.、Kotlin 的 ?. 和 Swift 的 ?. 是相同的概念。

如果没有可选链,你需要嵌套检查:

auto
// 没有可选链 -- 冗长
if alice.address is {
    Some(addr) => print(addr.city),
    None => print("None")
}

有了 ?.,意图在一个表达式中就很清楚了。转译器在每个目标语言中生成相应的 null 检查:

  • Rust 使用 if let Some(ref addr) = alice.address。
  • Python 使用 if alice.address is not None。
  • C 检查 _address_some 标志。
  • TypeScript 检查 !== null。

!T -- 错误类型 ​

?T 表示"这个值可能不存在",而 !T 表示"这个操作可能以错误消息失败"。它有两个变体:

  • Ok(value) -- 操作成功
  • Err(message) -- 操作失败,带有字符串错误消息

这等价于 Rust 的 Result<T, E>,其中 E 始终是 String。与 ?T 的区别在于:当没有值时,还有一个原因 -- 一个解释出错的错误消息。

从函数返回 !T ​

返回 `!int` 的函数

当函数返回 !T 时,裸值会自动包装在 Ok 中。字符串字面量会自动包装在 Err 中。所以在返回 !int 的函数中,表达式 42 变成 Ok(42),表达式 "bad input" 变成 Err("bad input")。

这在各语言中的映射:

  • Rust 使用 Result<i64, String>,配合 Ok(...) 和 Err(...)。转译器在 Auto 直接使用值时生成 .unwrap()。
  • Python 没有原生的 Result 类型。错误通常作为异常抛出。
  • C 使用带有 _ok 标志、value 字段和 error 字段的结果结构体。
  • TypeScript 可以直接使用值,因为 TypeScript 不在语言层面强制 Result 类型。

! 运算符 -- 错误传播 ​

! 运算符(后缀)用于传播错误。当你调用一个返回 !T 的函数并附加 ! 时,你是在说:"如果成功,给我值;如果失败,立即从我的函数返回该错误。"

这等价于 Rust 的 ? 运算符,但使用 ! 代替。

使用 `!` 运算符传播错误

! 运算符做两件事:

  1. 如果 read_file(path) 返回 Ok(content),解包并给你 content。
  2. 如果 read_file(path) 返回 Err(msg),立即从 read_config 返回 Err(msg)。

这被称为_错误传播_ -- 错误沿着调用栈向上冒泡,直到有人处理它们。这与 Rust 的 ? 运算符是相同的模式,消除了在每个中间调用上写 match 的样板代码。

在 main() 中,read_config("config.txt")! 上的 ! 解包结果。如果是 Err,程序会带着错误消息 panic。

?T vs !T 速查 ​

AutoRust使用场景
?UserOption<User>值可能不存在(查找、搜索)
!UserResult<User, String>操作可能以错误失败
noneNone无值
"error msg"(在 !T 中)Err("msg".to_string())带消息的错误
expr!expr?传播错误
x?.fieldx.as_ref()?.field可选链

自定义错误 ​

当你需要提供更多关于出错原因的上下文时,可以定义自定义错误类型。Auto 的 !T 始终携带 String 错误消息,但你可以从自定义类型构造该消息:

使用 `!T` 的自定义错误类型

当函数返回 !T 且最后一个表达式是结构体值(如 ParseError { ... })时,Auto 将其视为错误。该结构体被格式化为字符串错误消息。在这个例子中,ParseError { message: "empty input", line: 0 } 变成 Err("empty input at line 0")。

这让你两全其美:用于调试的结构化错误数据,结合 Auto 简单的基于字符串的 !T 错误模型。

这在各语言中的映射:

  • Rust 创建 ParseError 结构体并使用 format! 将其格式化为 Err 字符串。
  • Python 抛出带有格式化错误消息的 ValueError。
  • C 使用 snprintf 在结果结构体中填充错误字符串。
  • TypeScript 抛出带有格式化消息的 Error。

try / catch ​

虽然 ! 运算符是传播错误的惯用方式,但有时你需要显式处理错误 -- 记录日志、转换错误,或根据错误类型采取不同操作。Auto 提供了 try 和 is 来实现这个目的。

使用 `try` 和 `is` 进行显式错误处理

输出:

Got: 42

try 关键字捕获 !T 表达式的结果而不传播它。它不是让错误向上冒泡,而是将完整的 Ok/Err 值赋给变量。然后你可以使用 is 来检查结果并决定怎么做。

这与 ! 运算符不同:

  • risky()! -- 解包值或 panic/传播错误
  • try risky() -- 捕获 Ok 和 Err 以供手动检查

当你想让错误传播时使用 !。当你需要自己处理错误时使用 try。

这在各语言中的映射:

  • Rust 直接对 Result 值使用 match。try 关键字是不必要的,因为 Rust 的 ? 运算符已经做了传播,你可以在不使用它的情况下 match 结果。
  • Python 使用 try/except,这是 Python 中捕获错误的自然模式。
  • C 检查结果结构体中的 _ok 标志。
  • TypeScript 使用 try/catch,反映基于异常的方法。

最佳实践 ​

何时使用 ?T vs !T ​

在 ?T 和 !T 之间的选择取决于你是否需要解释失败_为什么_发生:

  • 当值可能自然不存在时使用 ?T -- 在映射中查找键、在列表中搜索项目、访问可选字段。值不存在不是错误,只是信息。

  • 当操作可能因调用者需要知道的原因失败时使用 !T -- 解析字符串、读取文件、发起网络请求。调用者需要错误消息来决定怎么做。

组合使用 ?T 和 !T ​

有时你需要在两者之间架起桥梁。例如,你可能有返回 !T 的函数(因为它可能以错误失败),但你想将其转换为 ?T(因为调用者只关心是否有值,不关心为什么失败):

组合使用 `?T` 和 `!T` 实现灵活的错误处理

输出:

Some(3.333...)
None

divide 函数返回 !float,因为除以零是一个调用者应该知道的错误。safe_divide 函数将其转换为 ?float -- 它捕获错误并将其变为 none。safe_divide 的调用者不需要知道除法_为什么_失败;他们只需知道是否成功。

这种将 !T 转换为 ?T(丢弃错误消息)的模式,在你基于底层 API 构建高层 API 时很常见。底层 API 需要丰富的错误信息;高层 API 只需要知道成功或失败。

错误处理指南 ​

使用 ?T 当:

  • 值可能自然不存在(搜索、查找键)
  • 调用者应该决定没有值时怎么做
  • 缺失值是预期的,而非异常情况

使用 !T 当:

  • 操作可能以有意义的错误消息失败(解析、I/O)
  • 调用者需要知道失败_为什么_发生
  • 错误可能在更高层级被恢复

使用 ! 运算符当:

  • 你想让错误沿调用栈向上传播
  • 你不需要在此层检查错误
  • 你的函数也返回 !T

使用 try + is 当:

  • 你需要检查错误并采取特定操作
  • 你想将 !T 转换为 ?T
  • 你需要记录或转换错误

实用错误处理模式 ​

两种常见模式:使用 ? 运算符传播错误,以及处理可能缺失的环境变量。

使用 ? 运算符进行错误传播
带安全默认值的环境变量

总结 ​

Auto 的错误处理建立在两个类型级构造上,使错误可见并强制你处理它们:

概念AutoRustTypeScript
可选值?TOption<T>T | null
错误结果!TResult<T, String>throws
无值noneNonenull
成功裸值Ok(value)返回值
错误字符串字面量Err(msg)throw Error
传播错误expr!expr?(重新抛出)
可选链x?.fieldx.as_ref()?.fieldx?.field
显式处理try + ismatchtry/catch

Auto 相对于 Rust 的主要改进:

  1. 更短的类型语法:?User 替代 Option<User>,!int 替代 Result<i32, String>。
  2. 自动包装:裸值变成 Ok(...),字符串字面量变成 Err(...)。无需显式写构造器。
  3. 一致的错误类型:!T 始终使用 String 作为错误类型,消除了泛型错误类型的复杂性。
  4. 可选链:x?.field 用于安全访问嵌套可选值。

在下一章中,我们将讨论 Auto 如何处理模块和项目组织 -- 如何使用你学到的特性来组织大型程序。

基于 MIT 许可发布。