错误处理
错误是软件中不可避免的一部分。Auto 提供了两种类型级别的机制来处理出错的情况,灵感来自 Rust 的方法,但语法更加简洁:
?T表示一个可能不存在的值 —— 等价于 Rust 的Option<T>。它有两个变体:Some(T)和None。!T表示一个可能失败的操作 —— 等价于 Rust 的Result<T, E>(其中E始终是String)。它有两个变体:Ok(T)和Err(String)。
与 Rust 一样,Auto 要求你承认错误的可能性并在代码编译前采取行动。这一要求使你的程序更加健壮,确保你能在部署到生产环境之前发现并适当处理错误。
我们将首先介绍用 ?T 处理可恢复错误,然后讨论用 !T 进行错误传播,最后探讨何时该 panic、何时该恢复。
用 ?T(Option)处理可恢复错误
大多数错误并不严重到需要程序完全停止的程度。有时一个函数可能没有值可返回 —— 例如,在列表中查找一个不存在的项,或查找一个不存在的字符的索引。在这些情况下,?T 是合适的工具。
?T 类型有两个变体:
Some(value)—— 操作成功并产生了valueNone—— 操作失败或没有产生值
从函数返回 ?T
当一个函数可能没有值可返回时,将其返回类型声明为 ?T:
?? 运算符(空值合并)是 Auto 提供的优雅方式,用于在回退值的情况下解包可选值。它替代了 Rust 中冗长的 .unwrap_or(default) 模式。如果值是 Some(v),则返回 v;如果是 None,则返回默认值。
用 is 对 ?T 进行模式匹配
当你需要对 Some 和 None 采取不同的行为时,使用 is 表达式:
is 表达式是 Auto 的模式匹配语法(替代 Rust 的 match)。它解构 ?T 值:None 匹配没有值的情况,Some(v) 将内部值绑定到 v。
?T与Option<T>速查
Auto Rust 含义 ?intOption<i32>一个可能不存在的整数 Some(42)Some(42)值存在 NoneNone无值 x ?? 0x.unwrap_or(0)带默认值解包 x is { None => ... }match x { None => ... }模式匹配 v[i] ?? defaultv.get(i).copied().unwrap_or(default)安全列表访问
用 !T(Result)和错误传播处理不可恢复错误
对于可能以错误消息失败的操作(而不仅仅是"无值"),Auto 使用 !T。这等价于 Rust 的 Result<T, E>,其中 E 是 String。
!T 类型有两个变体:
Ok(value)—— 操作成功Err(message)—— 操作失败,带有字符串错误消息
用 .collect() 提取值
当你确信操作应该成功时,使用 .collect()(转译为 Rust 的 unwrap())从 !T 中提取值。如果实际值是 Err,程序将 panic:
这里,text.to_int() 返回 !int(Result)。.collect() 运算符提取内部值。如果解析失败,程序会带着错误消息 panic。
用 .collect() 和 fn main() ! 进行错误传播
当你希望错误向上传播而不是内联处理时,用 ! 声明函数返回类型,并使用 .collect() 来传播错误:
fn main() ! 语法告诉 Auto main 可以返回错误。这等价于 Rust 的 fn main() -> Result<(), String>。当函数返回 Ok 时,程序以代码 0 退出。当返回 Err 时,错误消息会被打印,程序以非零代码退出。
与 Rust 的关键区别:在 Rust 中,你使用 ? 运算符进行错误传播。在 Auto 中,当错误类型匹配时,.collect()(写为 .!)承担了这个角色。
!T与Result<T, E>速查
Auto Rust 含义 !intResult<i32, String>一个整数或错误消息 Ok(42)Ok(42)成功值 Err("msg")Err("msg".to_string())错误 x.!x?传播错误 / 解包 fn foo() !intfn foo() -> Result<i32, String>可返回错误的函数 fn main() !fn main() -> Result<(), String>可返回错误的 main
在函数中使用 None 和 Some
返回 ?T 的函数使用 None 表示"无值",使用 Some(value) 表示成功结果。这与 Rust 的 Option 相同:
这个函数在除以零时返回 None(一个不可能的操作),在除法成功时返回 Some(result)。调用者必须处理这两种情况。
使用 .expect() 处理不可恢复的错误
当你确信操作应该成功,并希望在失败时用描述性消息 panic 时,使用 .expect():
.expect("message") 方法的工作方式与 Rust 中完全相同:如果是 Ok/Some,则返回内部值;如果是 Err/None,则带着提供的消息 panic。
在生产代码中,优先使用 .expect() 而不是 .collect() —— 消息能让调试更轻松。
要 panic 还是不 panic
Auto 在错误处理决策上遵循与 Rust 相同的理念:
使用 ?T(Option)当:
- 值可能自然不存在(例如,搜索列表、查找键)
- 调用者应该决定没有值时怎么做
- 缺失值是预期的,而非异常情况
使用 !T(Result)当:
- 操作可能以有意义的错误消息失败(例如,解析、I/O)
- 调用者需要知道失败的原因
- 错误可能在更高层级被恢复
使用 .expect() 或 .collect() 当:
- 失败表明代码中存在 bug
- 你拥有比编译器更多的信息(例如,一个硬编码的有效值)
- 在测试和原型中,错误处理会模糊逻辑
使用 fn main() ! 当:
- 你的顶层代码执行可能失败的操作
- 你希望错误传播到程序退出,而不是被静默忽略
- 你更偏好清晰的错误消息而非崩溃诊断
错误处理指南
当你的代码可能进入不良状态时,建议使用 panic(通过 .expect() 或在失败值上使用 .collect()):
- 不良状态是意外的,而非偶尔可能发生的(比如用户输入无效数据)。
- 后续代码依赖于不处于这种不良状态。
- 没有好的方式将这些信息编码到你使用的类型中。
当失败是预期的时,返回 ?T 或 !T 比 panic 更合适。例如:解析器收到格式错误的数据,或用户输入验证检查失败。
在生产质量的代码中,大多数 Auto 程序员选择 .expect("原因") 而不是裸的 .collect(),以提供更多关于为什么操作预期会成功的上下文。这样,如果假设被证明是错误的,你有更多信息可用于调试。
总结
Auto 的错误处理建立在两个直接映射到 Rust 错误类型的类型级构造上:
| 概念 | Auto | Rust |
|---|---|---|
| 可空值 | ?T | Option<T> |
| 错误结果 | !T | Result<T, E> |
| 解包或默认值 | x ?? default | x.unwrap_or(default) |
| 传播错误 | x.! | x? |
| 带消息 panic | .expect("msg") | .expect("msg") |
| 可返回错误的 main | fn main() ! | fn main() -> Result<(), String> |
| 模式匹配 | x is { ... } | match x { ... } |
Auto 相对于 Rust 的主要改进:
- 更简洁的类型语法:
?int替代Option<i32>,!int替代Result<i32, String>。 - 空值合并:
x ?? 0替代冗长的x.unwrap_or(0)。 - 一致的错误类型:
!T始终使用String作为错误类型,消除了泛型错误类型的复杂性。 - 模式匹配:
is替代match,语法更清晰、更直观。
在下一章中,我们将讨论泛型类型以及如何定义自己的带有 ?T 或 !T 参数的类型。