Rust 闭包
闭包(closure)是一种小小的、没有名字的函数,你可以把它存进变量里,也可以把它传来传去。它和普通函数最大的不同在于:闭包能“捕获”它被创建时所在作用域里的变量。
闭包的基本语法
Section titled “闭包的基本语法”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 是花括号里的最后一个表达式,所以它就是闭包的返回值。
捕获外部作用域的变量
Section titled “捕获外部作用域的变量”下面是普通函数没有的本事:闭包可以直接使用它被定义的地方的变量,不需要你显式地传进去:
fn main() { let discount = 10; let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90 println!("{}", apply_discount(50)); // 40}输出:
9040discount 并不是闭包的参数,它只是待在 main 的作用域里,闭包伸手过去就直接用上了。普通的 fn 做不到这一点,它需要把 discount 作为参数传进去。
修改捕获变量的闭包
Section titled “修改捕获变量的闭包”闭包还能修改它捕获的变量,前提是这个变量是 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 || { ... }。你会在并发那一页看到它的实际用法。
闭包在实际中用在哪里
Section titled “闭包在实际中用在哪里”单独使用闭包的情况并不多。它真正有用的地方,是把自定义的行为传给另一个函数,尤其是你会在迭代器那一页学到的迭代器方法:
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() 这样的方法)才是你日常真正会用到闭包的地方。
1. 含税价格
Section titled “1. 含税价格”创建一个变量 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 加税后是 60120 加税后是 144tax_percent 从来没有被传进去。闭包从 main 里捕获了它,而这正是普通 fn 做不到的。
2. ID 生成器
Section titled “2. ID 生成器”写一个闭包 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 是 3last_id 和 next_id 都需要 mut:前者是因为它的值会变,后者是因为调用闭包会改变它捕获的东西。
- 闭包是一种小小的匿名函数:
|params| expression。 - 类型通常可以推断出来,所以闭包一般比等价的
fn更短。 - 和普通函数不同,闭包能捕获它被定义时所在作用域的变量。
- 修改捕获变量的闭包必须存放在
mut变量里,而且在它被使用期间,会一直借用着那个变量。 - 闭包最有用的地方是传给其他函数,尤其是
.map()这样的迭代器方法。
小测验
1. 闭包用什么符号代替函数参数的圆括号?
2. 闭包能做到而普通 fn 做不到的是什么?
3. 本页说闭包在实际中最常用在哪里?
4. 一个闭包对捕获的变量执行 count += 1,还需要什么?
得分: 0 / 4
- 闭包 — Rust 程序设计语言:官方教程中讲闭包、类型推断和
Fntrait 的章节。 - 捕获引用或移动所有权 — Rust 程序设计语言:闭包捕获变量的三种方式,以及什么时候该用
move。 - 闭包 — 通过例子学 Rust:对比闭包和函数的简短可运行示例。
- 捕获 — 通过例子学 Rust:更多关于闭包读取、修改和获取变量所有权的示例。
- Closure expressions — The Rust Reference(英文):闭包语法和捕获的精确规则。