
函数签名与参数
Rust 在类型检查调用点时必须解析出被调函数的完整签名,但源码不要求按文本顺序“先定义、后调用”:同一模块中的函数项可以写在调用者之后。真正的约束是名称在当前位置可见、参数与返回类型满足签名,以及借用关系合法。这个区分能避免把 C 头文件式前向声明经验错误地搬到 Rust。
这一节从三个角度拆解函数签名:如何声明一个函数、参数如何标注类型、返回值如何显式定义。这三者共同构成了 Rust 函数最外层的「门面」——你写的每个函数,都在用它告诉编译器:我承诺接收这些,我保证交还那些。
1. fn 声明:函数定义的基本骨架
在 Rust 中,声明一个函数使用 fn 关键字,后跟函数名、参数列表(可为空)和返回类型。语法骨架如下:
fn function_name(param1: Type1, param2: Type2) -> ReturnType {
// 函数体
}
一个最简单的、无参数、无返回值的函数甚至不需要 -> ReturnType 部分:
fn greet() {
println!("Hello, world!");
}
函数项可以直接定义在模块中,不需要 class 或实例上下文;这一点类似 C 和 Python 的模块级函数,与 Java 主要把函数放在类中的模型不同。Rust 也支持在 impl/trait 中定义关联函数和方法,二者共同形成 API:
- 函数可以被定义为「裸函数」:直接在模块层级(mod 内)或 crate 根部声明,不需要先实例化任何对象。
- 函数的位置自由:Rust 不在乎 fn 在哪里定义,只要在同一作用域内可访问即可。你完全可以在主函数之后声明辅助函数,编译器会按照名称解析,不存在「先定义后使用」的前向声明烦恼。
一个可运行的最小示例:
// 声明在后,调用在前——这在 Rust 中完全合法
fn main() {
let result = add(3, 5);
println!("3 + 5 = {}", result);
}
// Rust 允许函数在调用点之后定义
fn add(a: i32, b: i32) -> i32 {
a + b
}
2. 参数类型:类型标注是强制性契约
**Rust 要求每个参数的类型都必须显式标注。**这一条规则没有例外——不会从函数体内部推断参数类型。
为什么?这里有一个与 Rust 的核心设计目标直接相关的理由:类型标注是编译期错误检查的前提。如果编译器不知道 a 是 i32 还是 f64,调用时传入 "hello" 就不会报错;而类型一旦确定,编译器就能在编译阶段拦截 90% 以上的「传错参数」类 bug。
// 错误:必须标注参数类型
fn broken_add(a, b) {
a + b
}
// 正确:显式标注每个参数的类型
fn correct_add(a: i32, b: i32) -> i32 {
a + b
}
多个参数之间用逗号分隔,每个参数独立标注类型。Rust 不会像 C 那样普遍执行数值提升,例如不能把 i32 自动当成 f64;但“调用时必须逐字匹配类型”也过于绝对。函数调用点仍可能发生有规则的 coercion,例如 &String 到 &str 的解引用强制、函数项到 fn 指针、非捕获闭包到函数指针,以及具体类型到 dyn Trait 指针。数值语义变化则应使用 From/TryFrom 或显式 as,并理解溢出与截断边界。
// 混合参数类型完全允许——只要调用时一一对应
fn describe_person(name: &str, age: u8, height_cm: f32) {
println!("{} is {} years old and {:.1} cm tall.", name, age, height_cm);
}
注意 &str——这是字符串切片引用。Rust 的 String 和 &str 之间的差异会在后续「所有权」章节展开,本节只需知道:函数参数默认是「值传递」(对于 i32、bool 等基本类型,参数是副本),而引用类型(&T)允许函数借用数据而不接管所有权。
3. 返回类型:-> Type 语法与隐式返回
函数通过 -> Type 声明返回类型。如果函数没有返回值,省略 -> 部分即可——实际上,它隐式返回一个单元类型(unit type),写作 (),你可以将其理解为「空元组」,表示「计算完成但没有产出」。
// 等价的两者——右侧写法是左侧的省略形式
fn log_message(msg: &str) -> () {
println!("[LOG] {}", msg);
}
fn log_message_short(msg: &str) {
println!("[LOG] {}", msg);
}
配合返回类型,Rust 提供了两种返回机制,理解它们之间的区别是构建「表达式语感」的关键:
机制一:尾表达式(Tail Expression)。Rust 的函数体是一个块表达式 { … },块中最后一个表达式的值会自动作为整个块的求值结果。如果这个块是函数体,那么最后一个表达式的值就是函数的返回值——不需要 return 关键字,不需要分号。
// 尾表达式:最后一个表达式 `a + b` 没有分号,它的值成为返回值
fn add(a: i32, b: i32) -> i32 {
a + b // 没有分号!
}
如果你在 a + b 后面加了分号,函数体就变成了「语句序列,无返回值」,编译器会立刻报错:mismatched types: expected i32, found ()。**「分号 = 语句,无分号 = 表达式」**是 Rust 中最基础但也最容易绊倒新手的语法陷阱。
**机制二:显式 return。**可以在函数体的任何位置使用 return value; 提前退出并返回指定值。它通常用于条件提前返回,以节约代码空间:
fn absolute(x: i32) -> i32 {
if x >= 0 {
return x; // 提前返回
}
// 兜底:继续执行到尾表达式
–x
}
值得注意的是:return 带分号(它是一条语句),而尾表达式不带分号(它是一个表达式)。这恰好呼应了 Rust 的底层设计——一切赋值、分支、块都在求值,而不只是在「执行」。
为什么这一节是「表达式语感」的基石
到这里,你可能已经隐约察觉:Rust 函数里的 if、{ }、甚至函数调用本身,都在「返回一个值」而非「执行一段操作」。尾表达式机制正是这种「一切都是表达式」哲学的终极体现。
从维护者的视角看,这种设计让 Rust 代码非常紧凑:不需要几十个 return 关键字,也不需要记住「这段逻辑是在哪个分支返回的」——函数末尾的表达式始终是「默认出口」。这不是炫技,而是刻意降低认知负担:你只需要在脑海中将「最后一个表达式的值」替换为「函数的返回值」即可。
你现在已经掌握了函数最外层的骨架:如何声明、参数如何标注类型、返回值如何声明与产生。但 if 表达式如何使用尾表达式机制,让条件分支直接参与赋值? 这是表达式语感真正接管控制流的第一步——下一节,我们拆解 if 作为表达式在 Rust 中的求值规则。
表达式与语句
签名是函数的"外壳",而函数体才是灵魂所在。上一节我们建立了函数的完整签名,但还留下一个关键问题:函数体内部到底在发生什么? 在 Rust 中,答案是一场"值"的流动——而理解这场流动的关键,在于区分两个核心概念:表达式(expression)与语句(statement)。这组区分不仅决定了你如何书写函数体,更直接决定了 Rust 独特的"隐式返回"风格。
1. 表达式与语句:语感的基石
几乎每门语言都有表达式与语句之分,但在 Rust 中,这条分界线被抬到了前所未有的重要位置。简单来说:
- 表达式返回一个值。5、x + 1、foo() 都是表达式,它们求值后产生一个结果。
- 语句不返回值。它执行一个动作,但不产出任何值。let x = 5; 是语句,println!("hi"); 也是语句。
这个区分看似简单,但它有一个直接的推论:在 Rust 中,你无法把一条语句当作值来使用。例如,下面的代码无法编译:
// 错误:语句不返回值
let result = let y = 5;
「这和我有什么关系?」你可能想问。关系重大——因为函数体的最后一个表达式,就是函数的返回值。这一设计贯穿 Rust 的所有代码块结构,从最简单的函数体到最复杂的循环体,无一例外。
Rust 编译器对"表达式结尾是否有分号"极度敏感:加上分号,表达式就变成语句,值被丢弃;不加分号,值被保留并向上传递。这是 Rust 新手最常遇到的"编译器报错瞬间"。
2. 块表达式:一切皆表达式
既然表达式如此重要,那么代码块 {} 能够作为表达式,就是 Rust 最具设计感的特性之一。一个块 { … } 本身就是一个表达式——它包含若干条语句(可以有零条),以及一个可选的尾部表达式。块的值就是尾部表达式的值。
fn main() {
let x = {
let a = 10;
let b = 20;
a + b // 尾部表达式,没有分号!
};
println!("x = {}", x); // 输出:x = 30
}
这段代码的亮点在于:let a = 10; 和 let b = 20; 都是语句,它们执行绑定但不产生值;而 a + b 是最后一个表达式,没有分号,它的值(30)作为整个块的值被赋给 x。
这里有一个极易踩的坑:如果在 a + b 后面加上分号,块的值就变成了 ()(单元类型),x 同样会被推断为 (),而不是数字。这种错误编译器能捕捉到,但报错信息可能一时看不明白——这正是理解"表达式与语句"的价值所在:你不需要靠猜,而是直接读懂编译器在说什么。
3. 尾部值:隐式返回机制
理解了块表达式,函数体的"隐式返回"就水到渠成了。Rust 函数的返回值要么用 return 关键字显式返回,要么通过函数体末尾的表达式隐式返回。后者被称为尾表达式(tail expression)。
// 无分号的尾表达式 → 隐式返回
fn square(x: i32) -> i32 {
x * x // 没有 return,没有分号
}
// 带分号的语句 → 隐式返回 ()
fn print_square(x: i32) {
let result = x * x;
println!("{} squared is {}", x, result);
// 末尾没有表达式,函数隐式返回 () 单元类型
}
第一个函数 square 中,x * x 没有分号,它作为尾表达式的值成为返回值。第二个函数 print_square 的最后一个语句是 println! 宏调用,它本身是一个表达式,但后面有分号——值被丢弃,函数隐式返回 ()。
这两种风格的等价关系,可以借 if 表达式来进一步理解。Rust 中 if 不是传统意义上的语句,它本身也是一个表达式,可以返回值:
fn classify(n: i32) -> &'static str {
if n % 2 == 0 {
"偶数"
} else {
"奇数"
}
}
这里每个分支都是没有分号的字符串字面量表达式,if 表达式的值就是被选中分支的值。注意每个分支都必须返回同一类型——编译器会强制检查。
理解"表达式 vs 语句"和"尾表达式 vs 显式 return"的区别,不仅让你读懂 Rust 代码,更让你写出地道的 Rust:简洁的函数体倾向于省略 return,让数据的流动变得像流水一样自然。有了这个基础,下一节我们将进入控制流的完整世界——循环、匹配和 ? 运算符,你会发现这些强大的工具,本质上都是"表达式"思想的延伸。
函数的返回值
上一节的核心结论是:函数体的最后一个表达式,就是函数的返回值。这一设计让 Rust 的函数体读起来像一场数据流动的管道——值从入口流入,经过层层变换,最终从出口流出。但仅有隐式返回还不够,现实代码中我们经常需要在函数体中途提前返回,或者定义一个什么都不返回的函数。本节补全这三种返回形态:显式 return、尾表达式、以及单元类型 ()。
1. 显式 return:在任意位置提前退出
return 关键字可以在函数体的任意位置立即结束执行,并将指定的值返回给调用方,其语法为:
return 值;
下面这段代码体现了一个典型的提前返回场景——在循环中查找目标元素,找到就立刻返回,不必等循环自然结束:
// 在切片中查找第一个等于 target 的元素,返回其索引
fn find_index(numbers: &[i32], target: i32) -> Option<usize> {
for (index, &value) in numbers.iter().enumerate() {
if value == target {
return Some(index); // 找到目标,立即返回,函数在此终止
}
}
None // 循环结束仍未找到,属于函数体末尾的尾表达式
}
fn main() {
let data = [10, 20, 30, 40];
match find_index(&data, 30) {
Some(i) => println!("找到 30,位于索引 {}", i),
None => println!("未找到"),
}
}
运行这段代码,输出为 找到 30,位于索引 2。当 value == target 时,return Some(index) 立刻生效,循环和函数体一并终止——这正是显式返回的核心价值:它打破了函数体自上而下的线性执行流。
return expr 本身是发散表达式,类型为 !,会立刻退出当前函数。惯用写法作为表达式语句时带分号,如 return 5;;但分号不是 return 语法的一部分,在尾位置可写 return 5。let x = return 5; 甚至语法和类型上可以成立,只是后续代码不可达,绝不应拿它证明“return 没有值”。
2. 尾表达式:隐式返回的「默认路径」
尾表达式(tail expression)是函数体最后一行的表达式——它不带分号,其值自动成为函数的返回值。在 Rust 的社区写作风格中,绝大多数函数都以尾表达式收尾,而非 return。
// 接收三个整数,返回其中最大的一个
fn max_of_three(a: i32, b: i32, c: i32) -> i32 {
let mut largest = a;
if b > largest {
largest = b;
}
if c > largest {
largest = c;
}
largest // 尾表达式:不带分号,值被隐式返回
}
fn main() {
println!("{}", max_of_three(3, 9, 5)); // 输出 9
}
这里的 largest 就是函数体最后一条语句——它不带分号,因此是表达式,它的值就是函数的返回值。
那么,尾表达式与 return 的边界在哪里?一个实用的经验法则是:正常路径用尾表达式,异常或提前结束的路径用 return。前者让「值怎么流出函数」一目了然,后者用于打断常规流程。两种风格在 Rust 生态中都是主流,不存在孰优孰劣之分。
尾表达式不止局限于单行值——任何表达式都可以充当尾表达式,包括代码块 {}、if 表达式,甚至 match 表达式。这引出了 Rust 最具辨识度的写法之一:
// 返回两个整数中较大的一个
fn max(a: i32, b: i32) -> i32 {
if a >= b {
a // if 表达式的分支值
} else {
b // if 表达式的另一个分支值
}
// 整个 if 是函数体的尾表达式,它的值就是函数的返回值
}
fn main() {
println!("{}", max(7, 2)); // 输出 7
}
注意每个分支的末尾都没有分号——因为整个 if 表达式是尾表达式,它的每个分支都必须产生值。这种写法将「决策逻辑」与「返回值」合二为一,让代码极其紧凑。if 表达式的完整语义将在下一节(关于控制流的内容)中系统展开。
3. 单元类型 ():什么都不返回的函数
并非每个函数都必须产生一个有意义的返回值。例如打印日志、写入文件、修改数据结构——这些操作的目的是副作用,而非产出值。对于这类函数,Rust 规定:当函数的返回类型被省略时,它默认返回单元类型 ()。
单元类型是一种特殊的类型,它只有一个值,这个值就写作 ()。你可以把它理解为一个「零字节的占位标记」——它代表「这里有一个值,只是这个值不携带任何信息」。
// 函数签名中省略了返回类型,等价于显式写成 -> ()
fn announce(title: &str, content: &str) {
println!("《{}》", title);
println!(" {}", content);
// 函数体末尾没有任何尾表达式,隐式返回 ()
}
fn main() {
let result = announce("Rust 入门", "函数返回值的三种形态"); // 打印两行文本
// result 的类型是 (),它唯一存在的意义就是「占据一个位置」
println!("announce 返回值的类型是:{:?}", result); // 输出 ()
}
关于 () 有两个重要的推论:
- 函数体末尾如果是一个带分号的语句(如 println!(…)),函数隐式返回 ()。细心的读者会发现,上例中的 announce 正是如此——最后一行语句带有分号,因此它不是表达式,函数隐式返回 ()。
- return;(不带值)等价于 return ();,常用于提前退出不返回值的函数。因为 () 是返回类型的默认值,所以 return 后省略值是完全合法的。
// 打印两个整数之差,但保证输出非负
fn print_abs_difference(a: i32, b: i32) {
if a > b {
println!("差值为 {}", a – b);
} else {
println!("差值为 {}", b – a);
}
// 没有尾表达式,函数返回 ()
}
fn main() {
print_abs_difference(10, 3); // 输出:差值为 7
print_abs_difference(3, 10); // 输出:差值为 7
}
小结
一个 Rust 函数的返回值由三部分规则共同决定:return 负责提前终止并返回指定值;函数体末尾的尾表达式(不带分号)负责在正常流程结束时隐式返回;省略返回类型或没有尾表达式时,函数默认返回单元类型 ()。掌握这套规则后,你已经可以准确地「预测」任何一段函数体的返回行为——这对于你阅读别人的 Rust 代码和写出具备清晰语义的自有代码,都至关重要。
有了返回值这套基础,下一节先把函数绑定到类型上,区分带 receiver 的方法与普通关联函数。if、match 和循环如何成为表达式,则在紧随其后的控制流篇系统展开。
方法语法初识
函数是独立的、漂浮在模块空间中的构造单元——到目前为止,我们写的所有函数都是这种形态。但在真实项目中,数据与操作它的函数往往紧密耦合:一个 Rectangle 结构体,总伴随着计算面积、判断是否包含某个点等操作。如果让这些函数散落各处,靠命名前缀(如 rectangle_area(rect))勉强维系关联,代码很快就会失去组织性。Rust 给出的答案是 方法(method)——通过 impl 块,将函数「挂载」到类型上,让数据与其行为在语法层面融为一体。
1. impl 块:将函数与类型绑定
impl(implementation 的缩写)块是为某个类型定义方法的核心语法。其基本结构如下:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle { … } 表示花括号内定义的是该类型的固有关联项。只有带 receiver 的关联函数才是方法;没有 receiver 的是关联函数。receiver 通常写成 self、&self、&mut self,也可以使用 self: Box<Self>、self: Arc<Self> 等显式形式,因此不能简单记成“第一参数永远是 self”。方法调用通常使用点语法:
let rect = Rectangle { width: 30, height: 50 };
println!("面积: {}", rect.area()); // 输出: 面积: 1500
rect.area() 这一行,rect 自动作为 self 传入方法。对比普通函数调用 area(&rect),方法语法将接收者前置,代码读起来像自然语言:「这个矩形,计算它的面积」。这种语法糖的价值在链式调用时尤为明显——value.method1().method2() 让数据流向一目了然。
2. self 参数的三种形态
self 不是普通参数——它有三种形态,对应三种不同的所有权语义。选错形态,编译器会直接报错。
| self | 拿走实例所有权 | 消费型操作,如销毁实例并返回其内部数据 |
| &self | 只读借用 | 查询型操作,如读取字段、计算面积 |
| &mut self | 可变借用 | 修改型操作,如调整字段值 |
其中最常用的是 &self——它不夺取所有权,调用后实例依然可用。以 &self 定义方法时,无论调用者是 rect 还是 &rect,编译器都会自动处理引用,你只需写 rect.area()。
self(无引用)则代表该方法将消费这个实例。以 String 类型为例,into_bytes(self) 正是通过拿走字符串的所有权,直接取出其底层字节数组——此时原变量已失效。
&mut self 则用于需要修改实例内部状态的场景:
impl Rectangle {
fn scale(&mut self, factor: u32) {
self.width *= factor;
self.height *= factor;
}
}
let mut rect = Rectangle { width: 30, height: 50 };
rect.scale(2); // 必须用 mut 声明 rect,且需要可变借用
println!("宽: {}, 高: {}", rect.width, rect.height); // 宽: 60, 高: 100
这里 rect 必须声明为 mut,因为 scale 需要可变借用。这一要求并非噪声——它迫使你在编译期明确「谁拥有修改权」,从而规避整类数据竞争问题。
此外还有第四种形式:&mut self 的底层其实是 self: &mut Self,而 &self 是 self: &Self。Self(注意大写 S)是 impl 块内的类型别名——它指代「当前正在实现的那个类型」。在 impl Rectangle 块内,Self 就是 Rectangle。这个别名在关联函数中尤为关键。
3. 关联函数:不依赖实例的入口
self 是方法的标志,但有时我们需要一个不依赖任何实例、由类型本身直接调用的函数——这就是关联函数(associated function)。它的定义特征是:第一参数不是 self。
最常见的关联函数是构造函数。Rust 没有专门的 constructor 关键字,惯例是用 new 来命名:
impl Rectangle {
fn new(width: u32, height: u32) -> Self {
Rectangle { width, height }
}
}
// 调用方式:类型名 + 双冒号
let rect = Rectangle::new(30, 50);
注意语法上的关键区别:
- 方法:rect.area()——通过实例调用,使用点号
- 关联函数:Rectangle::new(30, 50)——通过类型调用,使用双冒号 ::
:: 语法在 Rust 中表示「从命名空间路径中取值」——这也解释了为什么 String::from、Vec::new 这些标准库中的常见调用都是关联函数而非方法。它们不依赖某个具体实例,而是作为类型的「工厂」存在。
构造函数中用 Self 作为返回类型,与直接写 Rectangle 完全等价。优势在于:如果未来类型改名或重构,impl 块内部的 Self 无需任何修改。在更复杂的泛型场景(本章不展开)中,Self 的简洁性会进一步放大。
至此,我们已经告别了「裸函数」时代。impl 块、self 三形态、Self 别名与 :: 调用语法,构成了 Rust 对象式编程的基础骨架。但方法只是 impl 块的一种用途——你是否想过,impl 还能为同一个类型定义多次?以及多个类型之间能否共享相同的行为?这些问题的答案,将引向 trait 与泛型——但那是后续章节的议题了。在进入控制流之前,先记住这个核心心智模型:方法让数据和行为在语法上同构,而所有权三形态则让每一次数据访问的代价在编译期就一目了然。
函数项类型与函数指针
我们已经掌握了如何定义函数、如何组织表达式、以及如何通过方法与类型绑定。但还有一个此前始终被刻意回避的问题:函数本身,算不算一种值? 答案是肯定的——在 Rust 中,函数名本身就是一个值,它拥有自己的类型。这个设计打开了一扇独特的大门:既然函数是值,就可以被赋值、被传递、被存储。这不仅让「函数作为参数」成为可能,更在生态系统中催生了装饰器、策略模式、回调机制等一大批经典设计范式。
1. 函数项:函数名本身就是一个类型
在 Rust 中,每一个具名函数都拥有一个独特的类型,称为函数项类型(function item type)。这个类型无法在代码中直接写出名字——它是编译器为每个函数生成的唯一类型。但它的行为可以验证:
fn square(x: i32) -> i32 {
x * x
}
fn main() {
// 函数名 square 本身是一个值,可以绑定到变量上
let f = square;
// 通过该变量调用它
let result = f(5);
println!("{}", result); // 输出: 25
}
注意一个微妙的细节:函数项类型是零大小的(zero-sized)。square 这个值不占用任何内存空间——它不携带数据、没有字段、没有状态,纯粹是一个「可调用的标记」。在编译后的机器码中,调用 f(5) 直接就是调用 square 的地址,不存在任何间接层。
函数项类型唯一重要的性质是:它的类型是唯一的。定义两个签名完全相同的函数,它们的类型依然不同:
fn add_one(x: i32) -> i32 { x + 1 }
fn add_two(x: i32) -> i32 { x + 2 }
let a = add_one;
let b = add_two;
// a 的类型是 add_one 的函数项类型
// b 的类型是 add_two 的函数项类型
// 它们不相等——即使签名看起来一模一样
2. fn 指针:统一不同函数的共同签名
既然每个函数项类型都是唯一的,我们如何把「任意一个 i32 -> i32 的函数」作为参数传递?答案是函数指针(function pointer),写作 fn(i32) -> i32。注意:这里的小写 fn 是一个类型,而非定义函数的关键字。
fn add_one(x: i32) -> i32 { x + 1 }
fn add_two(x: i32) -> i32 { x + 2 }
fn apply_twice(f: fn(i32) -> i32, x: i32) -> i32 {
f(f(x)) // 将函数连续应用两次
}
fn main() {
let result = apply_twice(add_one, 10);
println!("{}", result); // 输出: 12 (10 -> 11 -> 12)
let result = apply_twice(add_two, 10);
println!("{}", result); // 输出: 14 (10 -> 12 -> 14)
}
这里发生了隐式转换:当 add_one 被传入 apply_twice 时,编译器将它的函数项类型自动**强转(coerce)**为 fn(i32) -> i32 函数指针。这个转换无需显式书写,Rust 在函数调用的参数位置自动完成。
函数指针与函数项类型的本质区别在于:
- 函数项类型:零大小、编译期已知、类型唯一。调用时是直接跳转,零开销。
- 函数指针:一个指针大小的数据(8 字节),指向函数的机器码地址。调用时通过指针间接跳转,有极其微小的间接层开销。
当你需要一个变量来在多个函数之间切换时,函数指针是其用武之地:
fn main() {
let operation: fn(i32, i32) -> i32;
let mode = "add";
if mode == "add" {
operation = |a, b| a + b; // 暂不深入闭包,这里仅示意
} else {
operation = |a, b| a * b;
}
// 此刻 operation 指向哪个函数,取决于运行时分支
let value = operation(3, 4);
println!("{}", value);
}
3. 高阶函数:函数作为参数与返回值
当一个函数接收另一个函数作为参数,或返回一个函数时,我们称之为高阶函数(higher-order function)。这是函数式编程的基石,Rust 对此提供了完整的支持——尽管它的函数式语法不如 Haskell 或 ML 那样纯粹,但足以驾驭绝大多数工程场景。
一个典型的高阶函数是「数组的映射」:
fn map_vector(vec: &[i32], f: fn(i32) -> i32) -> Vec<i32> {
vec.iter().map(|&x| f(x)).collect()
}
fn double(x: i32) -> i32 { x * 2 }
fn triple(x: i32) -> i32 { x * 3 }
fn main() {
let nums = vec![1, 2, 3, 4];
let doubled = map_vector(&nums, double);
let tripled = map_vector(&nums, triple);
println!("{:?}", doubled); // [2, 4, 6, 8]
println!("{:?}", tripled); // [3, 6, 9, 12]
}
map_vector 不关心具体执行什么变换,它只负责遍历与收集。变换逻辑由调用方通过函数指针注入。这种「控制反转」让 map_vector 变得通用——任何 i32 -> i32 的函数都可以无缝接入。
高阶函数的另一个经典应用是策略模式。假设一个排序函数需要比较规则,比较逻辑作为参数传入:
fn bubble_sort<T>(arr: &mut [T], compare: fn(&T, &T) -> std::cmp::Ordering) {
let len = arr.len();
for i in 0..len {
for j in 0..len – 1 – i {
if compare(&arr[j], &arr[j + 1]) == std::cmp::Ordering::Greater {
arr.swap(j, j + 1);
}
}
}
}
fn ascending(a: &i32, b: &i32) -> std::cmp::Ordering {
a.cmp(b)
}
fn descending(a: &i32, b: &i32) -> std::cmp::Ordering {
b.cmp(a)
}
fn main() {
let mut nums = [3, 1, 4, 1, 5];
bubble_sort(&mut nums, ascending);
println!("{:?}", nums); // [1, 1, 3, 4, 5]
bubble_sort(&mut nums, descending);
println!("{:?}", nums); // [5, 4, 3, 1, 1]
}
排序算法的骨架固定不变,只有比较策略随参数注入而切换。bubble_sort 既不需要知道元素的具体类型(通过泛型 T),也不需要知道比较逻辑的细节(通过 compare 参数)。这正是高阶函数带来的解耦威力。
至此,我们已经掌握了 Rust 函数的三种形态:函数定义、带 receiver 的方法,以及可强制转换为函数指针的函数项。函数项具有每个函数各自唯一、不可直接命名的零大小类型;当放入统一容器或跨 FFI 边界时才常被强制为 fn(A) -> R 指针。非捕获闭包也可强制为兼容的函数指针,但捕获环境的闭包需要后文的 Fn* trait。
按本专栏顺序,下一篇不是闭包,而是控制流:它将把块表达式、尾表达式和发散表达式组合成真实的分支与循环。闭包安排在所有权、trait 与生命周期之后,因为只有先理解捕获值如何移动或借用,才能正确解释闭包类型。





