1. 可驳与不可驳模式

在理解 if let 之前,先理解两种模式:

类型 定义 示例
不可驳 (irrefutable) 一定能匹配成功 let x = 42;、函数参数 fn f((a, b): (i32, i32))
可驳 (refutable) 可能匹配失败 Some(v)Ok(x)(1, _)42..=100

编译器强制不同上下文接受不同模式:

1
2
3
4
5
6
7
// let 语句要求不可驳模式
let (a, b) = (1, 2); // ✅ 枚举总能解构成 (a, b)
let Some(x) = Some(42); // ❌ 编译错误:可驳模式用于 let
// error[E0005]: refutable pattern in local binding

// if let 接受可驳模式
if let Some(x) = Some(42) { } // ✅

这就是 if let 存在的根本原因:你需要一个接受可能失败的模式的语法位置。

2. if let 逐层拆解

2.1 语法 = 一个模式 + 一个守卫 + 一个分支

1
2
3
4
5
if let 模式 = 表达式 {
// 匹配成功时执行
} else {
// 匹配失败时执行(可选)
}

match 的等价关系:

1
2
3
4
5
6
7
8
9
10
11
// 以下两者完全等价
if let Some(x) = opt {
println!("{}", x);
}

// --- 等价于 ---

match opt {
Some(x) => println!("{}", x),
_ => (), // 什么都不做
}

2.2 执行顺序:先求值表达式,再匹配模式

1
2
3
4
5
6
if let Some(x) = expensive_call() {
// ^^^^^^^^^^^^^^^ 1. 先计算表达式,得到值
// ^^^^^^ 2. 将值对模式 Some(x) 进行匹配
// 3. 匹配成功 → 绑定 x,进入分支
// 4. 匹配失败 → 跳过(或进入 else)
}

expensive_call() 只调用一次,结果暂存再用于匹配——和 match 完全一致。

2.3 解构同时完成绑定

模式可以任意复杂,解构在匹配成功时原子完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Message {
Move { x: i32, y: i32 },
Text(String),
Quit,
}

let msg = Message::Move { x: 10, y: 20 };

// 深层解构:匹配 Move 变体,提取 x, y
if let Message::Move { x, y } = msg {
println!("Move to ({}, {})", x, y); // Move to (10, 20)
}

// 嵌套模式 + 条件
if let Message::Text(ref s) = msg {
// s: &String —— ref 表示不可变引用,不取走所有权
}

解构与匹配同时发生——不存在”先匹配成功,再单独解构”的两步过程。

2.4 所有权:默认移动,用 ref 借引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let opt = Some(String::from("hello"));

// 默认移动 —— opt 的值被移入 s
if let Some(s) = opt {
println!("{}", s); // s: String(拥有所有权)
}
// println!("{:?}", opt); // ❌ 编译错误:opt 已被部分移动

// 借引用
if let Some(ref s) = opt {
// s: &String
}
// println!("{:?}", opt); // ✅ 还能用

// 可变引用
if let Some(ref mut s) = opt {
s.push_str(" world");
}

同一规则适用于 matchlet、函数参数——模式匹配的所有权语义是统一的。

2.5 模式守卫:匹配后额外过滤

1
2
3
4
5
6
7
8
9
let opt = Some(42);

if let Some(n) = opt && n > 50 {
// 匹配成功 + 守卫条件成立 → 进入此分支
println!("big: {}", n);
} else {
// 匹配失败 或 守卫条件不成立 → 进入此分支
println!("small or none");
}

注意执行顺序:

1
2
3
4
1. 计算 opt,得到 Some(42)
2. 匹配模式 Some(n),成功,绑定 n = 42
3. 检查守卫 && n > 50,失败
4. 进入 else 分支

守卫是 && 条件,不是 ||——if let Some(n) = opt && a || b 会因优先级问题报错,需要加括号。

2.6 多分支链:if let — else if let — else

1
2
3
4
5
6
7
8
9
if let Some(x) = a {
// a 是 Some
} else if let Some(x) = b {
// a 是 None,b 是 Some
} else if let Some(x) = c {
// a、b 都是 None,c 是 Some
} else {
// 全是 None
}

等价于嵌套 match,但链式写法在”依次尝试多个备选”的场景中更直观。

3. let…else:失败必须分流

Rust 1.65 引入。语法:

1
2
3
4
let 模式 = 表达式 else {
分岔语句; // 必须发散的块
};
// 此后正常使用绑定变量

3.1 核心语义:匹配失败就必须离开当前作用域

1
2
3
4
5
let Some(user) = find_user(id) else {
return Err("not found");
};
// 到这一行时,user 一定被成功解构出来了
println!("{}", user.name);

else 块必须发散(diverge)——即不返回到 let 之后继续执行。合法的发散路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// return
let Some(x) = opt else { return; };

// break
loop {
let Some(x) = try_get() else { break; };
}

// continue
loop {
let Some(x) = try_get() else { continue; };
}

// panic / unreachable
let Some(x) = opt else { panic!("impossible"); };
let Some(x) = opt else { unreachable!(); };

// 永不返回的函数
let Some(x) = opt else { std::process::exit(1); };

3.2 为什么不能写成普通条件分支

let…elseif let…else 的关键区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
// if let:即使匹配失败,代码沿当前路径继续
if let Some(x) = val {
// 有 x
} else {
// 没 x,但走完以后代码仍然继续
}
// 这一行总能执行到,但 x 不一定存在

// let…else:匹配失败 = 当前路径终结
let Some(x) = val else {
return;
};
// 这一行只在匹配成功时执行,x 一定存在

let…else 卖的是保证:成功路径上没有 option/result 的包袱——x 是个裸值,不需要 unwrap()

3.3 与 if let 的对照

if let let…else
匹配失败后 继续执行(进入 else 或跳过) 必须离开当前作用域
绑定变量作用域 仅在 if 块内 let…else 之后开始(整个外层作用域)
典型用途 条件分支逻辑 提前 return / 参数校验 / 守卫子句
引入版本 1.0 1.65

3.4 实际场景:函数入口的守卫子句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn process(config: Option<Config>) -> Result<(), Error> {
// 三个守卫子句 —— 失败直接返回
let Some(Config { db_url, port, .. }) = config else {
return Err(Error::missing_config());
};

let Some(db_url) = db_url else {
return Err(Error::missing_field("db_url"));
};

let Ok(port) = u16::try_from(port) else {
return Err(Error::invalid_port(port));
};

// ── 以下代码假定所有校验已通过 ──
let conn = Database::connect(&db_url, port)?;
conn.execute("SELECT 1")?;
Ok(())
}

对比没有 let…else 的写法:

1
2
3
4
5
6
7
8
9
10
// 不用 let…else —— 向右无限缩进
let config = match config {
Some(c) => c,
None => return Err(Error::missing_config()),
};
let db_url = match config.db_url {
Some(u) => u,
None => return Err(Error::missing_field("db_url")),
};
// ...

let…else 把”失败就走”的语义拉平到一行,每种异常情况一个 else 分支,后续代码全部是正常路径。

4. 与 match 的关系:何时用哪个

场景 推荐
匹配多个变体,各自有不同逻辑 match
只关心一个变体,其他忽略 if let
只关心一个变体,其他忽略且条件过滤 if let … && guard
依次尝试多个备选 if let … else if let … else
提前 return / 参数校验 / 守卫子句 let…else
循环中持续匹配直到失败 while let

match 适合需要穷尽性保证的多分支场景。if let 适合单分支提取。let…else 适合”这个条件不满足就别往下走了”的守卫子句。

5. 解构模式速查

if let / let…else / match / while let 中可用的解构模式一览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 元组解构
if let (x, y) = pair { }

// 枚举变体(具名字段)
if let ApiResult::Success { code, body } = result { }

// 枚举变体(元组字段)
if let ApiResult::Error(code, msg) = result { }

// 结构体部分字段
if let Config { db_url, port: 5432, .. } = cfg { }
// ^^^^^^^^ ^^^^^^^^^ 绑定值的同时字面量匹配

// 引用解构
if let Some(&v) = opt_ref { } // v: i32(拷贝)
if let Some(ref v) = opt { } // v: &T

// 范围模式
if let 200..=299 = code { } // 2xx

// 或模式
if let Ok(v) | Err(v) = result { } // 多模式共享同一个分支

// 绑定 + 守卫
if let Some(n) = opt && n > 0 { }

6. 小结

  • if let = 单分支 match:接受可驳模式,只关心匹配成功的路径
  • 执行顺序:表达式求值 → 模式匹配 → 绑定变量 → 守卫判断 → 入分支
  • 解构在匹配成功时原子完成,所有权规则与 match 完全统一(默认移动、ref 借引用)
  • let…else = “失败就走”:else 块必须发散,保证后续代码中绑定变量可用
  • match 管多分支穷尽,if let 管单分支提取,let…else 管守卫子句——三个工具分工明确