函数与表达式:语句与返回值

掌握了数据的所有权规则,接下来是把数据组织成逻辑。这篇解决的问题是:Rust 函数的参数、返回值、表达式语法在概念上和别的语言有何不同——尤其是「语句与表达式」的区分,以及 Rust 没有传统 return 关键字也能返回值的机制是否合法可用。读完你能写出地道的 Rust 函数体,不再下意识地把它写成 C 的形状。

一、语句与表达式:最基础的分野

Rust 语法里最重要的概念之一,是把代码分成两类。语句(statement)执行动作但不产生值;表达式(expression)求值并产生一个值。这个区分贯穿整个语言:if 分支、match 分支、代码块 {} 全都是表达式,都能产生产值。先从简单的对照看起:

fn main() {
    let x = 5;                 // 语句
    let y = {                  // 代码块是表达式
        let z = x * 2;
        z + 1                  // 末尾无分号,作为块的值
    };                         // y = 11
    println!("{}", y);
}

块内最后一句不加分号,整个块就以它的值为值;加分号就变成语句,块的值变成单元类型 ()。编译器会用类型检查拒绝意料之外的空值返回,错误信息通常是 mismatched types expected ... found ()。

从其他语言来的人需要刻意训练这个直觉:分号把表达式降级为语句。反过来说,该返回值的地方写多了分号,是 Rust 新手最常见的低级错误,频率高到值得在编辑器里养成习惯——写完一个返回表达式,先检查分号。

一个有效的小练习:把常用的三行函数改成表达式形式写一遍。最初你会觉得别扭,因为多数语言的 if 不产生产值,分支只能靠 return 或赋值。一旦适应,你会发现 Rust 的函数体天然短小,流程逻辑与值计算被同一套语法统一表达,这正是它代码紧凑的技术原因。

二、函数签名与参数

函数声明以 fn 开头,参数必须标注类型,这与动态语言不同,但和 C++、Java 一致。默认参数与可变参数都不支持,需要灵活性时用 trait 对象切片、builder 模式或宏来代替:

fn build_greeting(name: &str, excited: bool) -> String {
    let mut s = format!("你好,{}", name);
    if excited {
        s.push('!');
    }
    s
}

返回类型用 -> 标注;不写就是返回 ()。参数命名惯例是小写下划线。当参数过多(超过四五个)时,考虑引入结构体参数或 builder 模式——这不是品味问题,可读性差的调用点通常是 bug 的温床。下表把三种典型函数形态放在一起对比。

形态 特点 何时使用
传值参数 参数被 move 进来 内部需长期持有
传引用参数 仅借用,不持所有权 只读或临时修改
多返回值 返回元组或结果类型 多个结果一并交付
无返回值 返回 () 或省略 执行动作型逻辑

Rust 没有 Python 那样的元组解包语法糖之外的默认值机制,但解构让多返回值用起来很自然:let (scheme, host) = parse(url); 一步到位。对外的 API 若参数组合多变,builder 模式比堆参数更受欢迎:Greeting::new().name("张三").excited(true).build() 这类调用链既能默认值又能链式表达,标准库与社区库中俯拾皆是。

三、返回值与 return 的真相

return 关键字存在,但只用于提前退出;正常出口的返回值是函数体末尾表达式的值。这一点和 Ruby、Elixir 同族,与 C 家族相反:

fn classify(n: i32) -> &'static str {
    if n > 0 {
        "正数"
    } else if n < 0 {
        "负数"
    } else {
        "零"
    }
}

if 链本身是表达式,整个函数没有出现一个 return 却能正确返回。再看提前退出的写法:

fn first_word(s: &str) -> Option<&str> {
    if s.is_empty() {
        return None;     // 提前退出需要 return
    }
    // 主逻辑用表达式返回
    s.split_whitespace().next()
}

混用两套风格完全正常:正常路径用表达式,异常路径用 return。坚持这个分工,函数会读起来像声明而不是流程图。

四、发散函数:永不返回

有一类特殊函数标记为 -> !,称为发散函数,表示它永不返回——panic、无限循环、进程退出都属于此类。它们的价值在于类型系统的完备性:match 的某些分支调用发散函数时,编译器不会要求该分支也产出与其它分支同类型的值,因为控制流根本不会从那里继续。

fn bail(msg: &str) -> ! {
    panic!("{}", msg);
}

fn pick(v: &[i32], i: usize) -> i32 {
    match v.get(i) {
        Some(n) => *n,
        None => bail("索引越界"),   // 类型为 !,与 i32 兼容
    }
}

标准库的 process::exit 就是发散函数,错误处理章节会再次遇到这个概念。目前只要看到 -> ! 知道它意味着「这一步走不到后面」即可。

发散类型在写 match 时几乎天天出现:panic 的强制退出、std::process::exit 的进程终止、以及 tokio 等运行时里表示「不归路」的粘合函数。没有它,每个 match 分支就必须老老实实产出同类型值,错误处理代码会被迫绕路。

五、函数指针与闭包预告

函数也是一等公民,可以存在变量里传来传去,fn 类型就是函数指针。更高频使用的是闭包,后文迭代器与多线程章节会展开。这里先建立印象:函数与闭包都可以作为参数,它们由 trait(Fn、FnMut、FnOnce)统一描述。

类型 调用方式 典型场景
fn 指针 直接调用 简单回调、C 接口
闭包 捕获环境 迭代器、回调、线程任务
实现 Fn 的结构体 方法调用 把闭包存进容器长期使用

从 JavaScript 或 Python 转来的同学会把闭包当默认工具,在 Rust 里多了一层「捕获方式」的考虑——闭包默认按引用捕获,需要拥有数据时写 move 闭包。这些细节到迭代器与多线程章节再细讲,现在记住方向即可。

六、错误信息与函数设计

函数相关的两类高频错误值得记住。一类是 expected one of 找不到分号或括号的语法错误,报错位置可能比真实位置靠后,从报错点往前找分号问题往往一找就准。第二类是返回类型不匹配,尤其是块末多分号导致的单元类型返回,错误信息里的 found () 就是它最明显的指纹。

设计层面,地道 Rust 函数的标志是短:一个函数做一件事,复杂逻辑拆成小函数再用表达式串起来。配合类型系统,短函数几乎不需要注释就能自解释。若你在维护一个长函数项目,Claude Code 使用指南 里有按函数边界自动拆分的重构示例,可以先把表达式语法练熟再尝试。写扩展程序时回调函数的组织方式也遵循同样原则,浏览器扩展开发指南 的消息处理一章可以互为参照,两边的函数边界划分经验可以直接互相套用。

另一个常被忽略的细节是参数个数:超过四到五个位置的函数调用点会变得难读,此时应把参数聚成一个 struct 传入,既利于命名也利于后续扩展。

延伸阅读

  • 下一篇「控制流」把 if、loop、while、for 放在一起讲,与本篇的表达式规则合并食用。
  • 熟悉函数式语言的读者可以在 正则表达式精进 里看到 Rust 函数式风格的全套写法,包括把解析逻辑表达为一串小函数的组合。
  • 函数签名设计是 trait 一章的前置能力,建议写够十个小函数再进入下一章,手形成比看懂更重要。