跳转到内容

Rust 闭包

闭包(closure)是一种小小的、没有名字的函数,你可以把它存进变量里,也可以把它传来传去。它和普通函数最大的不同在于:闭包能“捕获”它被创建时所在作用域里的变量。

fn main() {
let add_one = |x: i32| x + 1;
println!("{}", add_one(5));
}

输出:

6
▶ 动手试一试

竖线 |x: i32| 代替了普通函数用的圆括号。把两者放在一起对比一下:

fn add_one_fn(x: i32) -> i32 { x + 1 } // 普通函数
let add_one_closure = |x: i32| x + 1; // 同样的功能,写成闭包

Rust 通常还能推断出参数类型和返回类型,所以你会经常看到写得更短的闭包:

let add_one = |x| x + 1;

没有参数的闭包就用一对空竖线:let say_hi = || println!("hi");,调用时写 say_hi()。

如果不止一个表达式,就像函数那样用花括号把闭包体包起来:

fn main() {
let describe = |n: i32| {
let kind = if n % 2 == 0 { "偶数" } else { "奇数" };
format!("{n} 是{kind}")
};
println!("{}", describe(7));
}

输出:

7 是奇数
▶ 动手试一试

format! 的用法和 println! 一样,只不过它不打印文本,而是把文本作为 String 交还给你。这里这个 String 是花括号里的最后一个表达式,所以它就是闭包的返回值。

下面是普通函数没有的本事:闭包可以直接使用它被定义的地方的变量,不需要你显式地传进去:

fn main() {
let discount = 10;
let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90
println!("{}", apply_discount(50)); // 40
}

输出:

90
40
▶ 动手试一试

discount 并不是闭包的参数,它只是待在 main 的作用域里,闭包伸手过去就直接用上了。普通的 fn 做不到这一点,它需要把 discount 作为参数传进去。

闭包还能修改它捕获的变量,前提是这个变量是 mut 的。另外还有一条规则:闭包本身也必须存放在一个 mut 变量里。调用它会改变某些东西,Rust 希望这一点在代码里一眼就能看出来。

fn main() {
let mut count = 0;
let mut increment = || {
count += 1;
println!("计数现在是 {count}");
};
increment();
increment();
increment();
println!("最终计数:{count}");
}

输出:

计数现在是 1
计数现在是 2
计数现在是 3
最终计数:3
▶ 动手试一试

如果忘了给 increment 加 mut,编译器会准确地告诉你哪里不对:calling increment requires mutable binding due to mutable borrow of count(因为可变借用了 count,调用 increment 需要可变绑定)。

这里有个值得了解的小陷阱。只要这个闭包还会被调用,它就拥有修改 count 的权利,所以其他人都不能碰 count。如果你在两次 increment() 调用之间加一句 println!("{count}");,就会得到 error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable。最后一次调用 increment() 之后,闭包的任务完成了,count 又完全归你使用。所以打印“最终计数”的那一行没有问题。这条规则背后的完整原理,你会在借用那一页学到。

有时闭包需要完全拿走它捕获的值的所有权,比如要把它交给另一个线程的时候。这时就在竖线前面写上 move:move || { ... }。你会在并发那一页看到它的实际用法。

单独使用闭包的情况并不多。它真正有用的地方,是把自定义的行为传给另一个函数,尤其是你会在迭代器那一页学到的迭代器方法:

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
}

输出:

[2, 4, 6, 8, 10]
▶ 动手试一试

这里的 |n| n * 2 就是一个直接传给 .map() 的闭包。这种模式(用一个小闭包驱动 .map() 或 .filter() 这样的方法)才是你日常真正会用到闭包的地方。

创建一个变量 tax_percent,值为 20。然后写一个闭包 with_tax,它接收一个价格,利用捕获到的 tax_percent 返回价格加上税之后的结果。分别打印价格 50 和 120 的结果。提示:税额是 price * tax_percent / 100。

查看答案
fn main() {
let tax_percent = 20;
let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50 加税后是 {}", with_tax(50));
println!("120 加税后是 {}", with_tax(120));
}

输出:

50 加税后是 60
120 加税后是 144
▶ 动手试一试

tax_percent 从来没有被传进去。闭包从 main 里捕获了它,而这正是普通 fn 做不到的。

写一个闭包 next_id:第一次调用返回 1,第二次返回 2,第三次返回 3,依此类推。调用它三次,并打印每个 ID。提示:在闭包外面放一个 mut 计数器,在闭包里给它加 1,再把计数器作为闭包的最后一个表达式,这样它就会被返回。

查看答案
fn main() {
let mut last_id = 0;
let mut next_id = || {
last_id += 1;
last_id
};
println!("第一个用户的 ID 是 {}", next_id());
println!("第二个用户的 ID 是 {}", next_id());
println!("第三个用户的 ID 是 {}", next_id());
}

输出:

第一个用户的 ID 是 1
第二个用户的 ID 是 2
第三个用户的 ID 是 3
▶ 动手试一试

last_id 和 next_id 都需要 mut:前者是因为它的值会变,后者是因为调用闭包会改变它捕获的东西。

  • 闭包是一种小小的匿名函数:|params| expression。
  • 类型通常可以推断出来,所以闭包一般比等价的 fn 更短。
  • 和普通函数不同,闭包能捕获它被定义时所在作用域的变量。
  • 修改捕获变量的闭包必须存放在 mut 变量里,而且在它被使用期间,会一直借用着那个变量。
  • 闭包最有用的地方是传给其他函数,尤其是 .map() 这样的迭代器方法。

小测验

1. 闭包用什么符号代替函数参数的圆括号?

2. 闭包能做到而普通 fn 做不到的是什么?

3. 本页说闭包在实际中最常用在哪里?

4. 一个闭包对捕获的变量执行 count += 1,还需要什么?

得分: 0 / 4