函数与闭包
一、 函数 (Functions)
函数是 Rust 代码的静态骨架。它们必须在编译时拥有明确的签名。
1. 基础语法与显式返回
Rust 的函数参数必须标注类型,返回类型使用 -> 标注。
// 逻辑概括:参数必须显式注明类型,最后一行表达式作为返回值
fn calculate_score(points: i32, multiplier: i32) -> i32 {
if points < 0 {
return 0; // 使用 return 提前退出
}
points * multiplier // 隐式返回(无分号)
}
fn main() {
let score = calculate_score(10, 3);
println!("最终得分: {score}");
}
2. 函数指针 (fn 类型)
函数本身可以作为参数传递,也可以存储在变量中。它的类型是小写的 fn。
fn add_one(x: i32) -> i32 { x + 1 }
fn do_math(f: fn(i32) -> i32, value: i32) -> i32 {
f(value)
}
fn main() {
let result = do_math(add_one, 5);
println!("函数指针调用结果: {result}"); // 6
}
二、 闭包 (Closures)
Rust 中的闭包(closures)是一种匿名函数,可以捕获其环境中的变量。闭包类似于其他语言中的 lambda 表达式,但 Rust 的闭包系统与所有权和借用紧密集成,确保内存安全。闭包可以作为函数参数、返回值,或存储在变量中,常用于迭代器、线程和回调。Rust 闭包实现了Fn trait 家族(Fn、FnMut、FnOnce),根据捕获方式决定其行为。最核心的特性是 捕获环境 。它们通常比函数更简洁,且支持类型推导。
1. 语法与自动推导
闭包不强制写类型,编译器会根据第一次调用的上下文锁定类型。语法:|params| expression 或 { body }。
fn main() {
// 闭包标准语法
let closure_annotated = |x: i32| -> i32 { x + 1 };
// 自动推导简写
let closure_inferred = |x| x + 1;
println!("{}", closure_annotated(1));
println!("{}", closure_inferred(1));
}
2. 捕获方式:不可变、可变、移动
闭包通过三种方式从作用域捕获变量:
- 不可变借用 (
&T) :默认方式。 - 可变借用 (
&mut T) :当闭包内部修改变量时。 - 移动所有权 (
T) :使用move关键字,常用于异步或多线程。
fn main() {
let x = 4;
let equal_to_x = |z| z == x; // 借用 x (&x)
println!("相等?{}", equal_to_x(4)); // 输出: 相等?true
println!("x 仍有效: {}", x); // x 未移动
//可变借用
let mut count = 0;
let mut inc = || {
count += 1; // 自动推导为 可变借用
println!("当前计数: {count}");
};
inc();
inc();
// 强制移动所有权
let text = String::from("hello");
let print_text = move || println!("移动后的文本: {text}");
print_text();
// println!("{text}"); // ❌ 报错:text 已移动到闭包中
}
三、 闭包特征 (Fn, FnMut, FnOnce)
当闭包作为参数传递时,我们需要使用这三个 Trait 来约束它:
FnOnce:调用一次,消耗闭包(可能移动捕获)。FnMut:可多次调用,可修改捕获。Fn:可多次调用,只读捕获。
fn run_once<F>(f: F) where F: FnOnce() {
f();
}
fn main() {
let s = String::from("once");
let consume_s = || drop(s); // 该闭包消费了 s 的所有权
run_once(consume_s);
// run_once(consume_s); // ❌ 报错:闭包已被消费
}
四、 高级进阶:函数与闭包作为返回值
这是 Rust 中最具灵活性的部分。由于闭包没有具体的名字,返回它们需要特殊的处理。
1. 返回普通函数指针 (fn)
适用于逻辑固定、不捕获外部变量的情况。
fn apply<F>(f: F, x: i32) -> i32
where
F: FnOnce(i32) -> i32, // bound FnOnce
{
f(x)
}
fn main() {
let double = |n| n * 2;
println!("结果: {}", apply(double, 5)); // 输出: 结果: 10
}
2. 返回闭包:静态分发 (impl Trait)
这是返回闭包最常用的方式。它效率高(无堆分配),但要求所有分支返回同一种闭包。
fn create_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
// 关键:必须使用 move,将 factor 移入闭包
// 否则 factor 会在函数结束时释放,导致引用失效
move |x| x * factor
}
fn main() {
let double = create_multiplier(2);
println!("3 的两倍是: {}", double(3));
}
3. 返回闭包:动态分发 (Box<dyn Trait>)
如果你需要根据逻辑返回不同的闭包(比如在 if/else 分支中返回不同的闭包代码块),必须使用 Box。
fn get_closure(mode: bool) -> Box<dyn Fn(i32) -> i32> {
if mode {
Box::new(|x| x + 1)
} else {
Box::new(|x| x * 2)
}
}
fn main() {
let f = get_closure(false);
println!("执行结果: {}", f(5)); // 10
}
总结对比
| 特性 | 普通函数 (fn) | impl Trait 闭包 | Box<dyn Trait> 闭包 |
|---|---|---|---|
| 捕获变量 | 不支持 | 支持(需用 move) | 支持(需用 move) |
| 返回类型 | fn(A) -> B | impl Fn(A) -> B | Box<dyn Fn(A) -> B> |
| 内存位置 | 代码段 | 栈 | 堆 |
| 性能 | 极高(静态) | 高(静态) | 略低(动态寻址) |
| 适用场景 | 简单、纯粹的逻辑 | 性能敏感、单一返回路径 | 需要根据条件返回不同闭包 |
💡 核心避坑指南
在返回闭包时,忘记写 move 是新手最常见的错误。
记住 :闭包默认会尝试通过“引用”来捕获环境中的变量。但当函数结束时,这些变量会被销毁,所以闭包必须通过
move把它们“打包带走”,否则你会得到一个“悬垂引用”的报错。