08|Result 与问号:把失败交给合适的层
目标:用 Result 返回可预期的失败,并用 ? 向调用者传播错误。
Option 只回答“有没有”。当输入不是一个合法数字、文件打不开时,我们还需要知道失败原因。Result<T, E> 的 Ok(T) 保存成功结果,Err(E) 保存错误。失败也是函数可能返回的值,因此调用者必须选择处理或继续传播。
? 的作用是:遇到成功值就取出它继续执行;遇到错误就从当前函数提前返回错误,必要时通过兼容转换调整错误类型。它不是打印错误,也不是自动重试。包含 ? 的函数需要有相容的返回类型,最常见的是 Result。
从一个数字解析器开始
创建 examples/errors.rs:
use std::num::ParseIntError;
fn doubled_limit(input: &str) -> Result<u16, ParseIntError> {
let value: u8 = input.parse()?;
Ok(u16::from(value) * 2)
}
fn main() {
match doubled_limit("12") {
Ok(value) => println!("limit={value}"),
Err(error) => println!("invalid: {error}"),
}
match doubled_limit("many") {
Ok(value) => println!("limit={value}"),
Err(_) => println!("invalid input"),
}
}
use 把标准库类型引入当前名字空间。u8 能表示 0–255,u16 能表示 0–65535;先转成 u16 再乘 2,保证所有合法 u8 输入都不会在这次乘法中溢出。parse() 从左侧 u8 推断要解析的目标类型。
运行前先预测:第二次解析失败会不会使整个程序 panic?
cargo run --quiet --example errors
预期输出:
limit=24
invalid input
第一次从 Ok(12) 中取出 12,再返回 Ok(24)。第二次在 ? 处提前返回 Err,由 main 选择输出消息;Err(_) 表示这里暂不使用具体错误值。真实 CLI 会保留文件系统错误原因,让用户知道如何修复。
追踪一个越界值
- 将第一次调用中的
"12"改为"256"。 - 运行后,第一行应进入
invalid:分支,错误信息指出数值超出目标类型范围;具体措辞由标准库版本决定。 - 改为
"255",应输出limit=510。 - 恢复主示例。这区分了“解析范围不合法”与“计算阶段溢出”。
独立练习:写 add_inputs(left: &str, right: &str) -> Result<u16, ParseIntError>。分别把两个输入解析为 u8,转为 u16 后相加。任何一个输入不合法都返回错误。
完整参考实现
use std::num::ParseIntError;
fn add_inputs(left: &str, right: &str) -> Result<u16, ParseIntError> {
let left: u8 = left.parse()?;
let right: u8 = right.parse()?;
Ok(u16::from(left) + u16::from(right))
}
fn main() {
assert_eq!(add_inputs("2", "3").ok(), Some(5));
assert_eq!(add_inputs("255", "255").ok(), Some(510));
assert!(add_inputs("bad", "3").is_err());
assert!(add_inputs("2", "bad").is_err());
println!("result checks passed");
}
替换实验文件运行,预期 result checks passed。ok() 将成功值转为 Option,丢弃错误信息,仅用于这里的简短断言;业务边界不要无意丢掉诊断。is_err() 判断是否失败,assert! 要求条件为真。
如果提示 ? 不能用于当前函数,检查函数签名是否仍返回 () 或普通整数。如果提示无法转换错误类型,先比较表达式中的错误类型和函数声明的错误类型。不要为了统一它们立即改成 unwrap();unwrap() 在 Err 时 panic,只适合确有依据的不变量或简短测试。
本课新增 examples/errors.rs。项目读取文件时会使用相同的“底层返回 Result,上层决定如何展示失败”结构。