Result 与 Option

9 分钟
本页目录

Option 表示有值或无值;Result<T, E> 表示操作成功或失败,失败时保留 E。它们让调用者从类型看见需要处理的分支。先决定“没有结果”是否正常,再选择类型:查询不存在的可选设置可以返回 None,读取文件失败通常需要 Err 提供原因。

返回 Result 不意味着函数绝不会 panic;它只描述通过返回值报告的失败。未使用 Result 通常触发 must_use 警告,不是不可绕过的强制处理,也不能证明程序正确恢复了错误。Result

一个例子:可选端口设置

把缺失和无效都转换成默认值,会让错误配置悄悄生效。这里约定缺失使用默认端口,但语法无效或超出 u16 范围必须报错;端口 0 是否允许由另一个业务验证步骤决定。

None:没有配置Ok(None):允许使用默认值Some("8080")Ok(Some(8080)):使用指定值Some("oops")Err(ParseIntError):报告输入错误可选配置的三种结果:缺失、有效、无效transpose 保留“缺失”与“失败”的区别;默认值策略由外层决定。
use std::num::ParseIntError;

fn optional_port(input: Option<&str>) -> Result<Option<u16>, ParseIntError> {
    input.map(str::parse::<u16>).transpose()
}

fn configured_port(input: Option<&str>) -> Result<u16, ParseIntError> {
    Ok(optional_port(input)?.unwrap_or(8080))
}

fn main() {
    assert_eq!(optional_port(None).unwrap(), None);
    assert_eq!(optional_port(Some("9000")).unwrap(), Some(9000));
    assert!(optional_port(Some("oops")).is_err());
    assert!(optional_port(Some("70000")).is_err());
    assert_eq!(configured_port(None).unwrap(), 8080);
    assert!(configured_port(Some("")).is_err());
}

map 先把 Option<&str> 变成 Option<Result<u16, E>>;transpose 再调整两层容器的位置。三种状态仍然存在,外层 Result 让调用者可以先传播失败,再决定如何处理缺失。Option::transpose

解析成功之后,还要验证业务范围

u16 的可表示范围与服务允许的配置范围不同。假设该服务明确禁止端口 0,可将语法错误与业务错误分别建模;不必让用户根据错误字符串猜是哪一类。

use std::num::ParseIntError;
#[derive(Debug)]
enum PortError { Syntax(ParseIntError), Zero }
fn port(input: Option<&str>) -> Result<u16, PortError> {
    let parsed = input.map(str::parse::<u16>).transpose()
        .map_err(PortError::Syntax)?.unwrap_or(8080);
    if parsed == 0 { return Err(PortError::Zero); }
    Ok(parsed)
}
fn main() {
    assert_eq!(port(None).unwrap(), 8080);
    assert_eq!(port(Some("9000")).unwrap(), 9000);
    assert!(matches!(port(Some("0")), Err(PortError::Zero)));
    match port(Some("oops")) {
        Err(PortError::Syntax(source)) => assert!(!source.to_string().is_empty()),
        other => panic!("预期语法错误,实际为 {other:?}"),
    }
}

端口 0 在某些绑定接口中有请求系统分配端口的意义,因此这里的拒绝是服务策略,并不是 Rust 数字解析规则。分层表达能让解析器保持复用,同时让应用明确自己的约束。

transpose 保留了哪三种情况?

换位没有合并任何状态,它只改变调用者先处理哪一层。把三种输入依次选一遍:缺失与非法字符串走的路径不同,所以 transpose 后的 ? 可以传播错误,而 unwrap_or 仍只为正常缺失提供默认值。

正在呈现知识画面
transpose 保留了哪三种情况?

None 变为 Ok(None),Some(Ok(v)) 变为 Ok(Some(v)),Some(Err(e)) 变为 Err(e)。

组合方法按哪个分支执行

方法OptionResult
map转换 Some 中的值转换 Ok 中的值
and_thenSome 时执行返回 Option 的操作Ok 时执行返回 Result 的操作
or_elseNone 时尝试替代操作Err 时尝试恢复,可利用错误值
unwrap_or_elseNone 时计算默认值Err 时根据错误计算默认值
map_err不适用转换 Err,保留 Ok

map 的闭包返回普通值,and_then 的闭包返回同类容器,避免形成不需要的嵌套。不是所有方法都“只处理成功分支”;or_else 正是在缺失或失败时运行。

unwrap_or(expensive()) 的参数会在调用前求值,即使已经有值。unwrap_or_else(|| expensive()) 只在需要时执行闭包。默认值会吞掉缺失或错误状态,应当是明确策略,而不是为了少写一个 match。Option

用调用次数看清惰性求值

use std::cell::Cell;
fn main() {
    let calls = Cell::new(0);
    let fallback = || { calls.set(calls.get() + 1); 8080u16 };
    let present = Some(9000u16);
    assert_eq!(present.unwrap_or(fallback()), 9000);
    assert_eq!(calls.get(), 1);
    assert_eq!(present.unwrap_or_else(fallback), 9000);
    assert_eq!(calls.get(), 1);
    assert_eq!(None::<u16>.unwrap_or_else(fallback), 8080);
    assert_eq!(calls.get(), 2);
}

这种区别也适用于 ok_or 与 ok_or_else、or 与 or_else:如果备用分支包含分配、记录日志或读取配置,提前求值可能既浪费工作又产生意外副作用。简单常量默认值则无需为了形式统一改成闭包。

? 在当前返回边界提前结束

对于返回 Result<T, E> 的普通函数,对 Result 使用 ? 时,Ok 被取出,Err 经适用的 From 转换后提前返回。它不会逐个尝试恢复策略,也不会自动把错误记录到日志。

对于返回 Option 的函数,None 会提前返回 None。不能在返回 Result 的函数里直接对 Option 使用 ?:先用 ok_or 或 ok_or_else 指定“缺失对应什么错误”;反方向调用 .ok() 会丢弃错误原因。

? 更一般的语言机制通过残余值传播表达,不能把所有支持 ? 的类型都理解成 Result 的 From 转换。日常理解 Result 和 Option 的具体规则通常足够。

提前返回会正常释放当前作用域中需要析构的局部值,但不会撤销已经发送的请求、写出的文件内容或提交的事务。需要原子更新时,必须另外设计提交协议。

闭包内的 ? 先返回到闭包调用方

use std::num::ParseIntError;
fn prepare(input: &str) -> Result<u32, ParseIntError> {
    let run = || -> Result<u32, ParseIntError> {
        let value = input.parse::<u32>()?;
        Ok(value)
    };
    let result = run();
    // 此处仍会执行,由外层决定如何处理闭包返回的结果。
    result.map(|value| value.saturating_add(1))
}
fn main() {
    assert_eq!(prepare("7").unwrap(), 8);
    assert_eq!(prepare("4294967295").unwrap(), u32::MAX);
    assert!(prepare("bad").is_err());
}

map 的闭包如果返回 Result,外层就会得到一层额外的容器;应检查真正需要的是“保存一个可能失败的结果”,还是用 and_then 串联并传播同类失败。不要单凭缩进判断 ? 将结束哪个函数或闭包。

提前返回不会撤销对已有对象的修改

use std::num::ParseIntError;
fn append_incrementally(target: &mut Vec<u32>, input: &[&str])
    -> Result<(), ParseIntError>
{
    for text in input { target.push(text.parse()?); }
    Ok(())
}
fn append_after_validation(target: &mut Vec<u32>, input: &[&str])
    -> Result<(), ParseIntError>
{
    let parsed: Result<Vec<u32>, _> = input.iter().map(|text| text.parse()).collect();
    target.extend(parsed?);
    Ok(())
}
fn main() {
    let mut first = vec![9];
    assert!(append_incrementally(&mut first, &["1", "bad"]).is_err());
    assert_eq!(first, [9, 1]);
    let mut second = vec![9];
    assert!(append_after_validation(&mut second, &["1", "bad"]).is_err());
    assert_eq!(second, [9]);
    append_after_validation(&mut second, &["1", "2"]).unwrap();
    assert_eq!(second, [9, 1, 2]);
}

第二种实现承诺的是解析错误时不修改 target,并使用额外临时存储换取这个行为;它不是通用数据库事务,也没有把分配失败、进程崩溃等都转换成可恢复错误。函数文档应准确写出失败后状态,而不是只写“返回 Result”。

借用容器,避免不必要地移动

很多组合方法按值取得 self。如果后面还要使用原容器,可先 as_ref 得到 Option<&T> 或 Result<&T, &E>。Option::as_deref 则得到 Option<&str>,适合把拥有的字符串交给只读解析器。

fn main() {
    let setting = Some(String::from("9000"));
    let borrowed: Option<&str> = setting.as_deref();
    assert_eq!(borrowed, Some("9000"));
    assert_eq!(setting.as_ref().unwrap().len(), 4);
    assert_eq!(setting.unwrap(), "9000"); // 此处才消费容器
}

批量处理:失败是停止还是收集

fn main() {
    let values = ["1", "bad", "3"];
    let parsed: Result<Vec<u32>, _> = values.iter().map(|s| s.parse()).collect();
    assert!(parsed.is_err());
}

collect::<Result<Vec<_>, _>>() 在遇到第一个 Err 后短路,不返回已收集的成功前缀。若需要展示每一行的错误,应逐项保存 Result,并记录输入位置。filter_map(|s| s.parse().ok()) 会直接丢弃所有失败项,只有“无效输入明确允许被忽略”时才合适。

输入:1、bad、3每项解析都可能成功或失败Result<Vec<T>, E>遇首错停止,无成功 VecVec<Result<T, E>>保留每项及其原始位置只保留成功项失败被丢弃,需明确允许同一批输入,三种不同的失败策略策略改变输出语义与工作量;不要为了让 collect 编译而无意吞掉错误。

保留行号,才能把失败交还给输入者

use std::num::ParseIntError;
fn main() {
    let input = ["1", "bad", "70000", "3"];
    let results: Vec<(usize, Result<u16, ParseIntError>)> = input.iter()
        .enumerate().map(|(index, text)| (index + 1, text.parse())).collect();
    let errors: Vec<usize> = results.iter()
        .filter_map(|(line, result)| result.as_ref().err().map(|_| *line)).collect();
    assert_eq!(errors, [2, 3]);
    assert_eq!(results[0].1.as_ref().unwrap(), &1);
    assert_eq!(results[3].1.as_ref().unwrap(), &3);
}

这段 filter_map 只是在已保留完整结果后生成错误行号列表,不会丢掉原始错误。若需要一次展示多字段校验,通常也应收集独立错误;但字段 B 的校验依赖字段 A 成功时,应避免拿默认值继续产生误导性连锁错误。

对于很大的输入,保存每项结果需要额外内存,可以逐条输出诊断、限制错误数量,或保存摘要与输入位置。遇首错停止、完整报告和部分成功导入是不同的产品行为,需在接口层做明确选择。

main 的错误报告

main 可以返回 Result<(), E>,其中 E 满足 Debug。标准库的 Termination 实现会对 Err 输出调试表示并返回失败退出码;不要把该接口的跨平台契约写死为数值 1。需要稳定的用户提示和细分退出码时,显式 match 并返回 ExitCode。Termination

unwrap/expect 适合测试断言或能够证明的不变量,不适合把正常外部输入错误一律变成 panic。如何设计 E、保留原因并补充业务上下文,见 Error trait 与生态。