为什么Rust老手从不用unwrap?他们都在用什么替代方案
为什么Rust老手从不用unwrap?
哥们,你写Rust的时候是不是也这样——到处 .unwrap()?
我懂,真的懂。写起来多爽啊,编译器不报错,代码跑得通,完事儿。
但你有没有发现,每次提PR,总有人在评论区阴阳怪气:“这个unwrap能不能处理一下?”
我之前也觉得这帮人事儿多。数据明明就在那儿,怎么可能是None?
后来线上炸了一次,我就老实了。
生产环境总在你最意想不到的时候给你"惊喜"
你的代码其实有两条路
先看张图,你写的每段代码其实都在走这两条路:
用户输入
|
v
┌──────────────┐
│ 你的代码 │
└──────┬───────┘
|
┌────┴────┐
| |
成功 失败
| |
v v
正常返回 .unwrap()
| |
v v
程序继续 程序崩溃
写代码的时候,脑子里想的都是左边那条路——数据都在,格式正确,一切顺利。
但生产环境这家伙,专挑右边那条路走,就爱看你翻车。
先说说unwrap是个啥玩意儿
打个比方吧。
你点了个外卖,快递员敲门说"您的餐到了"。unwrap就相当于你眼睛都不睁,直接说"放门口",然后转身就走。
正常情况下没毛病。但万一快递员拿错了呢?万一压根就没你的单呢?unwrap在Rust中就是对Option和Result类型的一种暴力取值方式。Option代表可能有值,也可能没有,unwrap就是不管三七二十一直接拿。
你不知道啊,等你饿得前胸贴后背去拿的时候,门口要么是别人的餐,要么啥都没有。
老手怎么干?他们会先问一嘴:“哪家店的?麻辣烫是吧?微辣?行,放着吧。“代码也是这个理儿。
外卖取餐:先确认再收货,避免拿错
来看个真实的翻车现场
这段代码你肯定眼熟:
fn process_upload(file_path: &str) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(file_path)?;
let lines: Vec<&str> = contents.lines().collect();
let header = lines.first().unwrap(); // 文件肯定有内容吧?
for line in lines.iter().skip(1) {
let fields: Vec<&str> = line.split(',').collect();
let name = fields.get(0).unwrap(); // 肯定有第一列吧?
let email = fields.get(1).unwrap(); // 肯定有第二列吧?
let age = fields.get(2).unwrap().parse::<u32>().unwrap(); // 肯定是数字吧?
save_user(name, email, age)?;
}
Ok(())
}
本地跑得好好的,上线第一天就炸了。有人传了个空文件,崩了。有人的CSV少了一列,又崩了。还有个大哥,年龄那栏填的是"二十五”,你猜怎么着?还是崩。五个unwrap,五种死法。
未处理的unwrap就像代码中的定时炸弹
那老手们怎么写?
同样的活儿,你看人家怎么干的:
fn process_upload(file_path: &str) -> Result<ProcessResult, ProcessError> {
let contents = fs::read_to_string(file_path)
.map_err(|e| ProcessError::FileRead(e.to_string()))?;
let lines: Vec<&str> = contents.lines().collect();
let header = lines.first()
.ok_or(ProcessError::EmptyFile)?; // 空文件?那我告诉你是空文件
let mut processed = 0;
let mut errors = Vec::new();
for (line_num, line) in lines.iter().skip(1).enumerate() {
let fields: Vec<&str> = line.split(',').collect();
if fields.len() < 3 {
// 字段不够?记下来,接着处理下一行
errors.push(format!("第{}行: 字段不够", line_num + 2));
continue;
}
let age = match fields[2].trim().parse::<u32>() {
Ok(a) => a,
Err(_) => {
// 年龄不对?记下来,接着干
errors.push(format!("第{}行: 年龄写的啥玩意儿", line_num + 2));
continue;
}
};
save_user(fields[0], fields[1], age)?;
processed += 1;
}
Ok(ProcessResult { processed, errors })
}
看到区别没?遇到问题不是直接躺平,而是记下来继续干。就像饭店后厨,今天鱼不新鲜,不是直接关门,而是跟客人说"鱼没了,红烧肉要不要?”
优雅的错误处理:记录问题,继续服务
那我以后碰到Option/Result咋办?
简单,问自己三个问题:
第一,这玩意儿真的不可能出错吗?
Rust的Option和Result就是提醒你:这里有值可能没有。如果是硬编码的正则表达式,那确实不太可能错。但如果是用户输入、文件读取、网络请求,那就别做梦了,啥幺蛾子都能出。Option的存在就是为了告诉你:这里可能是个空值。
第二,出错了我想怎么处理?
- 有备选方案?用
unwrap_or(默认值) - 要报错给上层?用
ok_or(错误)配合? - 情况复杂?老老实实写
match
实在不想写丢给AI,AI不像人它们不会感觉这是一种多复杂的写法
第三,我能接受程序直接崩吗?
测试代码里崩就崩了,无所谓。生产代码?你敢让它崩,半夜就等着接电话吧。
几个常用的替代写法
给个默认值:
let port = env_port.unwrap_or(8080); // 没配置?那就用8080
let config = get_config().unwrap_or_default(); // 没有?用默认的
转成错误往上抛:
let user = find_user(id).ok_or(ApiError::NotFound)?; // Option转Result,找不到?告诉调用方
分情况处理:
match find_user(id) { // 完美处理Option的两种情况
Some(u) => do_something(u),
None => {
log::warn!("用户{}不存在", id);
return Ok(default_response());
}
}
那unwrap就完全不能用了?
也不是,有两种情况可以:
测试代码随便用:
#[test]
fn test_parse() {
let result = parse("valid input").unwrap(); // 测试嘛,崩了正好说明有问题
assert_eq!(result, expected);
}
硬编码的东西用expect:
let re = Regex::new(r"^\d+$").expect("这正则我自己写的,不可能错");
注意是expect不是unwrap。expect能带个说明,万一真崩了,至少知道是哪儿出的问题。
说个真事儿
我之前接手一个项目,全局搜了一下,200多个unwrap。
半夜三点的夺命连环call,是每个开发者的噩梦
花了两天挨个改完,上线之后线上崩溃直接归零。不是说没错误了,而是错误变成了日志,变成了能查的东西,不再是半夜三点的夺命连环call。
最后说两句
Go语言被人吐槽 if err != nil 写到手软,但人家至少逼着你处理每一个错误。
Rust给了你自由,你可以unwrap一把梭,也可以老老实实处理。大部分人选了前者,然后在生产环境交学费。
代码评审:发现并修复unwrap的时刻
现在去搜一下你项目里有多少unwrap,找几个在关键路径上的,想想怎么改改。
别等线上炸了再后悔。
觉得有用的话点个赞,转发给你那些还在unwrap一把梭的同事,顺手关注一下,后面还有更多踩坑实录。
少写一个unwrap,少接一个报警电话。回见。