本节摘要:Result 枚举把"可失败"写进函数签名,
?运算符把"失败即上交"压缩成一个字符。本节讲 Result 的手动处理、?的展开语义与错误类型自动转换。读完你能把层层嵌套的错误处理改写成直线代码。
enum Result<T, E> { Ok(T), Err(E), } fn parse_no(s: &str) -> Result<u32, std::num::ParseIntError> { s.parse::<u32>() } let n = match parse_no("173") { Ok(v) => v, Err(e) => { eprintln!("案号解析失败:{}", e); return; } };
失败路径是类型系统可见的:调用方不打开这个盒子就拿不到值。这延续了第 4.2 节 Option 的思路,只是 Err 携带了案情细节。
? 的失败上交链
?:一个字符的和解条款use std::fs::File; use std::io::{self, Read}; fn read_header() -> Result<String, io::Error> { let mut f = File::open("卷宗头.bin")?; // 打不开?当场上交错误 let mut buf = String::new(); f.read_to_string(&mut buf)?; Ok(buf) }
? 的判决展开约等于:匹配结果,Ok 取值继续,Err 提前 return 并上交错误。更妙的是自动转换:函数的错误类型与 ? 处的不一致时,编译器会找 From 实现做转换——这就是 6.3 节自定义错误的黏合剂。
main 也可以返回 Result,命令行工具的顶层失败直接以非零码退庭:
fn main() -> Result<(), Box<dyn std::error::Error>> { let text = std::fs::read_to_string("input.txt")?; println!("{}", text.len()); Ok(()) }
Box<dyn Error> 是"装任何错误"的万能袋,小工具够用;库则应当用具体类型,理由见 6.3。
? 等价于失败即返回,并借 From 做错误类型换乘;⚠️ 常见坑:在返回 Option 的函数里对 Result 用
?(或反之)会报类型不符——两个家族各有?,不通用,需要时用.ok()?或.ok_or(...)?换乘。
? 看似魔法,展开后只是三步:匹配、上抛、转换。把展开式写出来,? 只能用于返回 Result 的函数这条限制就不言自明了。
use std::num::ParseIntError; fn parse_id(raw: &str) -> Result<u32, ParseIntError> { let trimmed = raw.trim().to_string(); let n: u32 = trimmed.parse()?; // ① Err 则整函数 return Err(转换后) Ok(n * 2) } // 与下面手写完全等价: fn parse_id_verbose(raw: &str) -> Result<u32, ParseIntError> { let trimmed = raw.trim().to_string(); let n: u32 = match trimmed.parse() { Ok(v) => v, Err(e) => return Err(e), }; Ok(n * 2) } fn main() { println!("{:?}", parse_id(" 21 ")); println!("{:?}", parse_id("证")); } // 输出:Ok(42) / Err(ParseIntError { kind: InvalidDigit })
第三步"转换"由 From trait 完成:? 发现错误类型与函数声明的不同时,自动调用 From::from。这解释了为什么在返回 Box<dyn Error> 的函数里任何标准错误都能直接 ?——它们都实现了 Into<Box<dyn Error>>。
use std::fs; fn main() -> Result<(), Box<dyn std::error::Error>> { let text = fs::read_to_string("案卷.txt")?; let n: u32 = text.trim().parse()?; println!("编号 {}", n); Ok(()) }
main 返回 Err 时进程以非零码退出并打印错误链。小程序与脚本的错误策略到这里就够了:一路 ? 上抛,顶层统一出口。
处理一批输入时,"一错全停"与"收集全部错误"是两种业务诉求,collect 的双态性正好分庭。
fn parse_all(rows: &[&str]) -> Result<Vec<u32>, std::num::ParseIntError> { rows.iter().map(|r| r.trim().parse::<u32>()).collect() // collect 目标是 Result<Vec<_>, _>:遇错即停 } fn main() { let a = ["1", "2", "3"]; let b = ["1", "x", "3"]; println!("{:?}", parse_all(&a)); // Ok([1, 2, 3]) println!("{:?}", parse_all(&b)); // Err(ParseIntError ...) let kept: Vec<u32> = b.iter() .filter_map(|r| r.trim().parse::<u32>().ok()) .collect(); println!("{:?}", kept); // [1, 3]:容错收集 }
需要"全部错误而不是第一个"时,用 Partition 或者第三方错误聚合模式,把 Vec<Result> 拆成两筐再合并。选择哪种策略是业务判决而非技术判决,写代码前先问清:坏一行是废整批,还是跳过记账。