不带分号的表达式如何成为返回值?

解读

在 Rust 里,函数体是由一系列语句和可选的尾表达式组成的块(block)
当块里最后一个元素是不带分号的表达式时,该表达式的值会被自动作为整个块的求值结果,进而成为函数的返回值。
这一机制与 C/C++ 的 return 语句不同:Rust 把“尾表达式”视为隐式返回,编译器在借用检查、生命周期推导和类型检查时都把它当作返回值处理,因此必须保证该表达式类型与函数签名一致,否则编译失败。
国内面试官常通过此题考察候选人对“表达式导向语法”与“所有权转移”是否真正理解,能否区分“语句(statement)”与“表达式(expression)”。

知识点

  1. 表达式 vs 语句
    • 表达式求值后有值,如 1 + 1if true { 42 } else { 0 }match x { Some(v) => v, None => 0 }
    • 语句以分号结尾,求值后返回单元类型 (),如 let x = 1;
  2. 尾表达式规则
    函数或代码块中最后一个不带分号的表达式会被自动返回,类型必须与函数签名中的返回类型完全匹配(包括生命周期)。
  3. 隐式移动与复制
    若尾表达式是值类型,则按 Rust 移动语义直接返回;若实现 Copy,则执行按位复制。
  4. 早期返回
    使用 return 关键字可提前返回,但尾表达式不需要 return,这是 Rust 代码风格的重要特征。
  5. 编译器错误示例
    若误加分号,返回类型会变成 (),与签名冲突,编译器会报 “mismatched types: expected i32, found (),国内校招面试中常让候选人现场排错。

答案

在 Rust 函数(或任何块表达式)中,去掉尾部的分号,该表达式即成为返回值。
示例:

fn add(a: i32, b: i32) -> i32 {
    a + b   // 无分号,表达式 a + b 的值被隐式返回
}

编译器将 a + b 视为块的求值结果,类型为 i32,与函数签名一致,无需写 return
若写成 a + b;,则语句返回 (),触发类型错误。

拓展思考

  1. 嵌套块与生命周期
    尾表达式若借用局部变量,会导致返回引用指向已释放栈内存,编译器会拒绝:
    fn bad() -> &i32 {
        let x = 42;
        &x  // 错误:返回的引用生命周期短于函数签名要求
    }
    
    理解尾表达式返回机制必须与生命周期注解结合,才能写出安全接口。
  2. 宏与尾表达式
    macro_rules! 中,若宏展开后作为尾表达式,也必须遵守“无分号即返回”规则,宏设计者需用 $expr 捕获并去掉分号,否则调用方会得到 ()
  3. async 块与尾表达式
    async move { ... } 块的尾表达式决定 Future::Output 类型,不加 await 的尾表达式不会立即执行,面试中常让候选人解释异步块返回值与真正 await 后拿到的值差异。
  4. 国内工程规范
    大厂 Rust 代码规范要求统一使用尾表达式返回,禁止冗余 return,除非提前返回;Clippy lint needless_return 也会强制提醒。面试时若能主动提到 Clippy 规则,可体现工程素养。