跳转到主内容match 和模式匹配深入 | 物联网民工

match 和模式匹配深入

C 程序员第一次看到 match,很容易把它当成更严格的 switch。

这个类比能帮你入门,但不够。C 的 switch 主要按整数值分支;如果状态背后还有数据,你还要手工从结构体或 union 里取字段。

Rust 的 match 做的是模式匹配:

判断值长什么样,同时把里面的数据取出来。

前面讲 enum 时已经看到,Rust 可以把 C 的 tagged union 写成携带数据的 enum。这一篇继续往下看:match 怎样处理更复杂的状态、引用、条件和局部解包。

match 同时分支和解包

C tagged union 常见写法是先看 tag,再读 payload:

switch (token->kind) {
case TOKEN_INT:
    printf("%d\n", token->data.int_value);
    break;
case TOKEN_IDENT:
    printf("%s\n", token->data.ident);
    break;
}

Rust 里分支和解包在同一个模式里完成:

enum Token {
    Int(i32),
    Ident(String),
    End,
}

fn describe(token: &Token) {
    match token {
        Token::Int(value) => println!("int {value}"),
        Token::Ident(name) => println!("ident {name}"),
        Token::End => println!("end"),
    }
}

Token::Int(value) 不只是判断“这是 Int”。它还把里面的 i32 绑定到 value。

这个模型比 switch 更接近:

如果值符合这个形状,就进入这个分支,并把形状里的局部变量取出来。

结构体和 tuple 也能解包

模式不只用于 enum。结构体也可以匹配字段:

struct Header {
    version: u8,
    flags: u8,
    len: u16,
}

fn print_header(header: Header) {
    match header {
        Header { version, flags, len } => {
            println!("v={version} flags={flags} len={len}");
        }
    }
}

如果只关心部分字段,可以用 .. 忽略剩余字段:

fn is_ack(header: &Header) -> bool {
    match header {
        Header { flags, .. } => (*flags & 0x01) != 0,
    }
}

tuple 也一样:

let point = (10, 20);

match point {
    (0, y) => println!("on y axis: {y}"),
    (x, 0) => println!("on x axis: {x}"),
    (x, y) => println!("point {x}, {y}"),
}

C 里这些通常是 if 加字段访问。Rust 的模式把“判断形状”和“取字段”合在了一起。

匹配引用时要分清借用和移动

对拥有数据的 enum,直接按值 match 可能会移动里面的数据:

enum Message {
    Text(String),
    Ping,
}

fn consume(msg: Message) {
    match msg {
        Message::Text(text) => println!("{text}"),
        Message::Ping => println!("ping"),
    }
}

这里 text 得到的是 String 的所有权。consume 本来就接收 Message,所以没问题。

如果只是查看消息,不想拿走所有权,就匹配引用:

fn inspect(msg: &Message) {
    match msg {
        Message::Text(text) => println!("{text}"),
        Message::Ping => println!("ping"),
    }
}

这里 text 的类型是 &String,因为 msg 本身是 &Message。常见场景下,编译器会自动处理引用模式,不需要到处写 ref。

对 C 程序员来说,可以先这样理解:

match value  -> 可能移动 value 里的数据
match &value -> 只是借用并查看

写库 API 时,如果函数只是读状态,参数通常接收引用。

if let 处理单个关心的分支

如果只关心一个变体,用完整 match 会显得重:

match token {
    Token::Ident(name) => println!("{name}"),
    _ => {}
}

Rust 提供 if let:

if let Token::Ident(name) = token {
    println!("{name}");
}

它的含义是:如果 token 符合 Token::Ident(name) 这个模式,就执行代码并绑定 name。

处理 Option 很常见:

if let Some(index) = find_index(items, target) {
    println!("found at {index}");
}

这比 C 里用 -1 特殊值再判断更明确:

int index = find_index(items, len, target);
if (index >= 0) {
    printf("%d\n", index);
}

如果两个分支都重要,用 match;如果只关心一个成功形状,用 if let。

while let 处理重复取值

C 里常见循环是“不断取下一个元素,直到没有”:

while (next_token(&token)) {
    handle(token);
}

Rust 中如果接口返回 Option<T>,可以用 while let:

while let Some(token) = lexer.next_token() {
    handle(token);
}

这表示:只要返回值匹配 Some(token),就继续循环;遇到 None 自动结束。

对 Vec 的 pop 也一样:

let mut stack = vec![1, 2, 3];

while let Some(value) = stack.pop() {
    println!("{value}");
}

这比手写 loop 加 break 更贴近数据形状。

匹配守卫表达额外条件

有时模式本身还不够,还要加条件。Rust 用匹配守卫:

match token {
    Token::Int(value) if *value > 0 => println!("positive {value}"),
    Token::Int(value) => println!("non-positive {value}"),
    Token::Ident(name) if name.len() > 16 => println!("long ident"),
    Token::Ident(name) => println!("ident {name}"),
    Token::End => println!("end"),
}

if *value > 0 是这个分支的额外条件。只有模式匹配并且条件为真,才进入这个分支。

匹配守卫适合表达“同一种形状下再按条件细分”。如果条件越来越复杂,通常应该先计算一个中间值,或者把判断放进函数里。

_ 会吞掉未来分支

_ 表示“其他所有情况”:

match token {
    Token::Int(value) => println!("{value}"),
    _ => println!("other"),
}

它很方便,但在协议、状态机、命令处理这类代码里要谨慎。_ 会让新增变体不再触发编译器提醒。

如果状态有明确工程意义,优先写全:

match token {
    Token::Int(value) => println!("{value}"),
    Token::Ident(name) => println!("{name}"),
    Token::End => println!("end"),
}

新增 Token::Error 时,所有需要处理新状态的地方都会编译失败。这是 match 比 switch 更有价值的部分。

如果确实只想忽略某些值,可以把忽略写得更窄:

match token {
    Token::Int(value) => println!("{value}"),
    Token::Ident(_) => println!("identifier"),
    Token::End => {}
}

这样比一个大 _ 更不容易掩盖未来变化。

matches! 适合简单判断

如果只想得到一个布尔值,可以用 matches!:

let is_end = matches!(token, Token::End);

也可以带守卫:

let is_large = matches!(token, Token::Int(value) if value > 1024);

这适合替代简单的 tag 判断。不要把复杂业务逻辑硬塞进 matches!,需要解包和处理多分支时,match 更清楚。

这一篇先记住什么

match 不是 C switch 的语法升级,而是模式匹配:

按值的形状分支
在分支里解包数据
让编译器检查是否覆盖所有情况
用 if let / while let 表达单分支和循环取值
用守卫表达额外条件
谨慎使用 _

从 C 迁移到 Rust 时,不要只把 switch 翻译成 match。更重要的是把“tag + payload + 手工字段读取”改成“enum + 模式匹配”,让状态和数据的关系进入类型系统。

下一篇讲 iterator。C 里习惯用索引和指针移动遍历数据,Rust 会把很多遍历逻辑变成可组合的值流。