为什么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,少接一个报警电话。回见。