函数与表达式:语句与返回值
掌握了数据的所有权规则,接下来是把数据组织成逻辑。这篇解决的问题是: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 一章的前置能力,建议写够十个小函数再进入下一章,手形成比看懂更重要。
