通过 Rspack 源码学 Rust(四):用 `Option<T>` 表达“查询可能没有结果”

14 阅读5分钟

读 Rspack 模块图时,dependency 不一定已经连到 module:解析失败、尚未构建完成或某种关系不存在,都意味着查询可能没有值。Rust 不用一个模糊的空指针表示这种情况,而是把“有值/无值”放进返回类型。本篇只讲两个连在一起的知识点:Option<T> 和 let-else。

一、从真实查询看问题

在 module_graph/mod.rs 中,依赖到模块标识符的查询签名是:

pub fn module_identifier_by_dependency_id(
  &self,
  dep_id: &DependencyId,
) -> Option<&ModuleIdentifier> {
  self
    .inner
    .connections
    .get(dep_id)
    .map(|connection| connection.module_identifier())
}

connections.get(dep_id) 找不到 key 时返回 None,找到时返回 Some(&ModuleGraphConnection)。.map(...) 只在有连接时读取目标 module identifier,所以整个查询结果是 Option<&ModuleIdentifier>。

调用处在 module_executor/execute.rs:

let Some(entry_module_identifier) =
  mg.module_identifier_by_dependency_id(entry_dep_id).copied()
else {
  // 没有入口模块:向请求方回传结果并结束当前任务
  result_sender.send(/* ExecuteResult */).expect("should send result success");
  return Ok(vec![]);
};

factorize 失败可能导致入口依赖没有对应连接。这里的 None 不是意外 panic,而是模块图查询允许出现的正常分支:返回执行结果但不继续遍历模块、生成 chunk。

二、知识点一:Option<T> 把可缺失性放进类型

Option<T> 只有两种形态:

enum Option<T> {
    None,
    Some(T),
}

可以把它读作“可能没有 T”。比如 Option<u32> 要么是 Some(42),要么是 None。调用方必须处理这两种可能,编译器不会允许把 Option<u32> 当成一个确定的 u32 使用。

最小可运行例子:

use std::collections::HashMap;

fn lookup_length(values: &HashMap<String, String>, key: &str) -> Option<usize> {
    values.get(key).map(String::len)
}

fn main() {
    let mut values = HashMap::new();
    values.insert("rspack".to_string(), "compiler".to_string());

    println!("{:?}", lookup_length(&values, "rspack"));
    println!("{:?}", lookup_length(&values, "missing"));
}

编译运行后输出:

Some(8)
None

HashMap::get 返回 Option<&String>,map(String::len) 在 Some 时把内部的 &String 转成长度 usize,因此最后是 Option<usize>。map 不会改变“有没有值”的事实,只会转换存在时的内部值。

Option<&T> 与 .copied()

Rspack 的图查询返回 Option<&ModuleIdentifier>,因为 ModuleGraph 借用自身,并返回图里已有标识符的引用。调用处接着 .copied(),把它转换成 Option<ModuleIdentifier>:

Option<&ModuleIdentifier>
       .copied()
Option<ModuleIdentifier>

这里能复制是因为 ModuleIdentifier 实现了 Copy。复制的是轻量标识符值,不是把 ModuleGraph 中的 module 整体克隆出来。若查询返回的是不实现 Copy 的大型对象,就不能照搬 .copied();可以继续借用、显式 .cloned(),或调整后续代码以避免不必要的拥有副本。

Option<T> 与 Result<T, E> 也不要混为一谈:Option 表达“有/没有”;Result 表达“成功/失败,并可能携带错误原因”。若调用者需要区分具体失败原因,通常应该用 Result 或更丰富的领域类型,而不是把所有错误压成 None。

三、知识点二:let-else 做失败即退出的解构

if let 可以在某个分支中解构 Some:

if let Some(value) = optional_value {
    use_value(value);
} else {
    handle_missing();
}

但若后续代码只有拿到值才有意义,层层缩进会让主要路径被包在成功分支里。let-else 把成功值绑定到外层作用域,把失败处理放在前面:

let Some(value) = optional_value else {
    return;
};

use_value(value);

关键约束是 else 分支必须发散,也就是不能正常走到下一行。常见发散语句有 return、break、continue 和 panic!。否则成功绑定后的 value 就无法保证一定存在。

下面的错误示例故意让 else 正常结束:

fn main() {
    let value = Some(3);
    let Some(number) = value else {
        println!("missing");
    };
    println!("{number}");
}

编译器会拒绝它(当前 stable rustc 报 E0308):else 分支必须 diverge,因为若 value 是 None,程序执行完 println!("missing") 后仍会继续,但此时 number 不存在。把分支改成 return;、panic!(...),或其他不会返回当前位置的控制流,类型约束才成立。

在 Rspack 的真实调用处,缺少入口模块时需要发送一个空执行结果,然后 return Ok(vec![])。这个 return 同时完成业务处理和满足 let-else 的发散要求;成功路径则可以直接使用拥有值 entry_module_identifier 初始化 BFS 队列。

四、最小示例:两种写法表达同一控制流

fn main() {
    let parsed = Some("42");
    let Some(text) = parsed else {
        println!("没有输入");
        return;
    };

    let number: i32 = text.parse().expect("示例输入必须是整数");
    println!("{}", number + 1);
}

用 rustc --edition=2021 option_let_else.rs -o option_let_else && ./option_let_else 编译运行,输出:

43

示例输入是 Some("42")。若换成 None,程序会打印“没有输入”并提前返回;number 和后续计算只存在于确认有文本的路径中。.expect() 是本例对固定常量输入的简化,不适合直接照搬到可能收到用户错误输入的生产代码。

五、把类型变化和所有权变化分开看

这一段源码同时发生了两件事:

mg.module_identifier_by_dependency_id(entry_dep_id).copied()

第一,Option<&ModuleIdentifier> 经过 .copied() 成为 Option<ModuleIdentifier>,借用变成复制值;第二,let-else 把 Some(value) 内部的 ModuleIdentifier 解构到局部变量,并要求 None 分支提前离开。Option 本身不会自动复制或移动内部对象,真正的复制策略由 .copied() 决定;let-else 负责控制流和绑定,不是转换 API。

这种区分很有用:先看函数签名决定“有没有值”和借用关系,再看链式方法决定内部类型怎样变化,最后看模式匹配决定缺失值如何处理。

六、常见误区与适用边界

  1. Option<T> 不是错误消息;只表达缺失,不携带缺失原因。
  2. Option<&T> 的引用受原数据生命周期限制。.copied() 只适用于 T: Copy,不是通用的解除借用方法。
  3. let-else 的 else 必须发散。需要在缺失时继续执行时,用 match 或 if let 设计完整的两路逻辑。
  4. 不要见到 None 就 unwrap()。只有程序不变量确实保证值存在时,panic 才是合适策略;图查询、用户输入和解析结果通常应显式处理。
  5. Option 很适合表达自然可缺失的查询;若缺失意味着故障且调用方需要诊断原因,改用 Result 通常更清楚。

简要说明

从 Rspack 的依赖图查询学习 Option<T>、.copied() 与 let-else,理解可缺失值、借用转换及失败分支必须发散的编译约束。