前言#
前天我们用已有知识实现了一个minigrep的匹配小工具,然后昨天我们优化了下这个小项目
今天该继续往下学了
函数式语言的特性:迭代器(Iterators)和闭包(Closures)#
rust语言的设计是参考了很多语言和技术的,其中函数式编程(functional programming)对rust的意义是非常重大的。比如把函数作为参数/返回值传递等。
闭包#
rust中对闭包的定义是:一个匿名函数可以捕获它的环境。
有点拗口。
它可以被用于以下几个地方
- 作为参数
- 作为返回值
- 作为变量在其它地方被调用
在看例子之前,我们有一个疑问,那就是为什么我们非得用闭包才能捕获上下文中的数据?用函数不行吗?
函数有时候可以,但是有时候不行,我们来看个错误。

a是一个动态的变量,而B是一个常量会被hardcode进binary中。 从这里可以知道函数中并不能获取上下文中的动态变量,因为这会导致额外的开销,与其让函数变得复杂,还不如再抽一种类型出来。
这也就是为什么rust中闭包是一个单独的概念了,而在js中,闭包就是依赖于函数的。
ok,我们来看下闭包的例子
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = Some(ShirtColor::Red);
let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}我们准备免费赠送一批衣服,这堆衣服里有红蓝cp两种颜色,如果用户指定了颜色 && 这个颜色是存在的 && 还有库存的,那么他就可以获得。反之没有指定就会默认选择销量最好的那一款。
我们在giveaway方法中写了一个闭包,作为unwrap_or_else的参数。
unwrap_or_else这个方法我们之前用到过

可以看到如果有值就返回值,没有则执行传进去的闭包,然后将闭包的返回值作为返回值。
所以有一点需要注意就是你这个闭包的值得和Some的值是一致的。
扯远了。
||之间相当于(),可以放参数,闭包体是{},只有单语句的时候可以不写。
闭包的规范是|xx| { ... }。
找不到用户想要的颜色时,它就会去执行这个most_stocked的method。
其实说到底,就是类似js中的callback。
我们来看下闭包和函数的区别
闭包(closure)和函数(function)的区别#
- 闭包不会强制要求你写参数的类型和返回值的类型。
毕竟fn函数是面向用户/其它开发者的,所以需要说清楚传入传出是啥。
而闭包不需要,它的参数和返回值类型会被编译器推断出来(除了极少数部分需要声明),因为它和它所在的上下文是有关联的,大部分情况下会在该上下文中被执行,所以也就能推断出来。
- 闭包的语法和函数的差不多
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;第一个是一个fn,接受一个u32,然后返回+1。
第二个到第四个都是闭包,都是正常的写法。
其中三和四需要在上下文中被使用,这样编译器才能推断出来它的类型。这和我们之前学习vector的时候很像,我们可以直接let v = Vec::new(),然后不定义它的类型,但是需要在上下文中将至少一个数据传入到这个vector里。
捕获引用(capturing references)或者移动所有权(moving ownership)#
前面说过,闭包可以捕获它所在环境中的数据。
类比函数中的参数,它有以下三种方式来捕获:
- 不可变借用
- 可变借用
- 拥有所有权
rust编译器会通过闭包体里是怎么对待被捕获的数据的来推断使用哪种方式来捕获。默认自然是不可变借用,然后可变借用再然后是拥有所有权。
来看个例子
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
let only_borrows = || println!("From closure: {:?}", list);
println!("Before calling closure: {:?}", list);
only_borrows();
println!("After calling closure: {:?}", list);
}我们写了一个only_borrows的闭包,这个闭包引用了list这个vector,但是仅仅只是标准输出了这个list,所以这个时候就没有必要可变引用或者获取所有权,直接改为不可变引用即可。
编译后会变成类似这样
let only_borrows = || println!("From closure: {:?}", &list);顺便说下,调用也是和函数一样的直接xxx()。
我们再来看个例子
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {:?}", list);
}
这次我们直接往list中push了一个数字7,这时不可变借用已经不能用了,所以这个时候就会使用可变引用的方式。
这里需要注意,在闭包被调用和定义之间,不能对可变引用的数据做引用或者迁移所有权处理,道理相信大家现在也都清楚了,事关内存安全。

然后我们再来看个例子
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
thread::spawn(move || println!("From thread: {:?}", list))
.join()
.unwrap();
}我们新开了一个线程,然后把闭包传入到里面去了。虽然还是标注输出list,但是这个时候不用move不行,为什么呢?新开的线程可能早于或者晚于main主线程销毁,这个时候引用指向的内存也会被销毁。而我们的main线程还在引用这个list引用,这样就有问题了。依旧是一个内存安全问题。
关键字move,放在闭包的参数声明的||之前,表示引用的数据的所有权被move到闭包里了。
移动闭包中的数据以及Fn trait#
前面说了,rust捕获环境数据的时候有三种方式,而不同的方式以及如何处理数据会影响这个闭包实现不同的trait。
通俗点的说就是对数据的不同捕获方式和对数据的处理方式会使这个闭包去implement不同的trait。
所以目前trait我们已知可被impl的有三种: struct、fn、closure。
不过closure不需要我们去手动impl,rust编译器会在编译的阶段帮我们去实现不同的trait。
闭包能实现的trait有以下三种
FnOnce: 所有闭包默认都会实现的一个trait,毕竟只要是闭包那就得能被调用。如果一个闭包只是把捕获的数据抛出去,那么这个闭包就只会实现这个trait。和trait的名字一样,这个闭包只会被调用一次,毕竟捕获的数据都抛出去了,没东西了自然就挂了。FnMut: 和第一个相反,实现这个trait的不会去抛出数据,它可能会改变这个捕获的数据。所以它可能被调用N次。Fn: 实现这个trait的闭包不会去抛出捕获的数据,也不会改变捕获的数据,并且也有可能是捕获不到数据的。这种不会影响环境所以它们可以在同一时刻可以被调用很多次。
这三个trait是嵌套的,如果实现了Fn这个trait,那么就一定实现了FnMut这个trait,而实现了FnMut,则一定实现了FnOnce。
所以一个闭包可能实现了两个甚至这三个trait。
另外,函数也是可以实现这三个trait的。
我们来看下例子
我们给unwrap_or_else方法传入的就是一个实现FnOnce这个trait的闭包

为什么呢?
并不是因为它返回了一个值,这个值不一定是捕获的值。另外这个闭包可能在里面对捕获的值做了改变,当然也有可能不改。所以对unwrap_or_else这个方法来说,传入的闭包的可能性太多了,不能确定。能确定的就只有这个闭包一定实现了FnOnce这个trait。
如果我们在使用这个闭包的时候并不需要捕获数据,那么完全是可以用函数传入的。
来看个例子
fn main () {
let mut v: Vec<i32> = create_vec().unwrap_or_else(|| Vec::new());
v.push(1);
}
fn create_vec<T> () -> Option<T> {
None
}这时我们的闭包没有捕获任何的环境数据,所以这个时候它实现的trait包括FnOnce和Fn。
我们来改下代码
fn main () {
let mut v: Vec<i32> = create_vec().unwrap_or_else(Vec::new);
v.push(1);
}
fn create_vec<T> () -> Option<T> {
None
}
这个时候我们传入的就不是闭包了,而是一个method,非常的简洁。
扯远了。
我们回到闭包会实现的三个trait中。
再来看个例子
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| r.width);
println!("{:#?}", list);
}
我们创建了一个数组,数据是一系列矩形。
矩形有宽高两个字段。
然后我们调用数组的sort_by_key这个方法根据矩形的width来从小到大排序。

可以看到数据按照width来排序了。
我们传给sort_by_key这个方法的数据是一个闭包,它接受一个参数是矩形的引用。 看情况它应该是一个impl了FnOnce的trait,没有其他的了。
但实际上并不是,实际上它还实现了FnMut这个trait。

官方的理由是:这个闭包被调用了N次。
就这一点它确实不能是FnOnce,因为如果这里面发生捕获数据的抛出,那么它只能抛一次,遍历的第二次就没东西抛了。
比如下面这个例子
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("by key called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{:#?}", list);
}
为什么会报错呢,就是因为这个闭包把它辛辛苦苦捕获到的数据全量扔给了sort_operations这个人渣,然后它卷款跑路了。
第二次自然就炸了。
但是为什么不是Fn这个trait呢?
这个咱也不知道啊。
可能有在遍历过程中需要改动其它数据的场景吧。
比如这样
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut num_sort_operations = 0;
list.sort_by_key(|r| {
num_sort_operations += 1;
r.width
});
println!("{:#?}, sorted in {num_sort_operations} operations", list);
}计算有几个矩形。。。。
至于Fn这个trait,对于闭包来说不太常用,而在函数中就很常见了。。
总结#
今天学的闭包,有三种捕获数据的方式以及三种可实现的trait。
参考#
- ^rust-13 https://doc.rust-lang.org/book/ch13-00-functional-features.html#functional-language-features-iterators-and-closures
- ^Rust-closures https://doc.rust-lang.org/book/ch13-01-closures.html#closures-anonymous-functions-that-capture-their-environment
- ^rust-13.1-capturing-reference-or-moving-ownership https://doc.rust-lang.org/book/ch13-01-closures.html#capturing-references-or-moving-ownership
- ^rust-13.1-moving-values-out-of-closures-and-fn-trait https://doc.rust-lang.org/book/ch13-01-closures.html#moving-captured-values-out-of-closures-and-the-fn-traits
编辑于 2022-12-28 11:27・IP 属地广东
