闭包的定义
闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值,例如:
fn main() {
let x = 1;
let sum = |y| x + y;
assert_eq!(3, sum(2));
}
上面的代码展示了非常简单的闭包 sum
,它拥有一个入参 y
,同时捕获了作用域中的 x
的值,因此调用 sum(2)
意味着将 2(参数 y
)跟 1(x
)进行相加,最终返回它们的和为3
当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。
闭包与函数最大的不同就是它的参数是通过 |parm1|
的形式进行声明,如果是多个参数就 |param1, param2,...|
, 闭包的形式定义为:
|param1, param2,...| {
语句1;
语句2;
返回表达式
}
如果只有一个返回表达式的话,定义可以简化为:|param1| 返回表达式
- 闭包中最后一行表达式返回的值,就是闭包执行后的返回值
let action = ||...
只是把闭包赋值给变量action
,并不是把闭包执行后的结果赋值给action
,因此这里action
就相当于闭包函数,可以跟函数一样进行调用:action()
- 闭包并不会作为 API 对外提供,无需标注参数和返回值的类型。 为了增加代码可读性,有时可以显式地给类型进行标注 。如果你只对参数进行了声明而没有使用,则需要显式标注类型
虽然闭包的类型推导很好用,但是它不是泛型,当编译器推导出一种类型后,它就会一直使用该类型:
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5); //报错
在 s
中,编译器为 x
推导出类型 String
,但是紧接着 n
试图用 5
这个整型去调用闭包,跟编译器之前推导的 String
类型不符,因此报错
结构体中的闭包
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
query: T,
value: Option<u32>,
}
impl<T> Cacher<T>
where
T: Fn(u32) -> u32,
{
fn new(query: T) -> Cacher<T> {
Cacher {
query,
value: None,
}
}
// 先查询缓存值 `self.value`,若不存在,则调用 `query` 加载
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.query)(arg);
self.value = Some(v);
v
}
}
}
}
Fn(u32) -> u32
是一个特征,用来表示 T
是一个闭包类型,该闭包拥有一个u32
类型的参数,同时返回一个u32
类型的值。query
的类型是 T
,该类型必须实现了相应的闭包特征 Fn(u32) -> u32
函数参数中的闭包
闭包捕获变量有三种途径,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用,因此相应的 Fn
特征也有三种:
1、转移所有权:
fn fn_once<F>(func: F) //func就是闭包,其类型F满足如下特征
where
F: FnOnce(usize) -> bool, //正确:F: FnOnce(usize) -> bool + Copy,
{
println!("{}", func(3));
println!("{}", func(4)); //F只实现了FnOnce特征,要转移所有权,因此只能使用一次func
}
fn main() {
let x = vec![1, 2, 3];
fn_once(|z|{z == x.len()}) //{z == x.len()}就是func 入参式usize类型,返回值式bool
}
//输出
true
false
FnOnce
特征的闭包在调用时会转移所有权,因为 F
没有实现 Copy
特征,所以会报错,那么我们添加一个约束,实现Copy
闭包就能顺利运行。如果你想强制闭包取得捕获变量的所有权,可以在参数列表前添加 move
关键字:
use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
2、可变借用:
FnMut特征以可变借用的方式捕获了环境中的值,除了申明变量本身是可变的,还需要把该闭包声明为可变类型,以便在闭包内部捕获可变借用,不然会编译出错:
fn main() {
let mut s = String::new();
let update_string = |str| s.push_str(str); //正确:let mut update_string = |str| s.push_str(str);
update_string("hello");
println!("{:?}",s);
}
也可以修改成如下形式:
fn main() {
let mut s = String::new();
let update_string = |str| s.push_str(str);
exec(update_string);
println!("{:?}",s);
}
fn exec<'a, F: FnMut(&'a str)>(mut f: F) {
f("hello")
}
这段代码中update_string
没有使用mut关键字修饰,但exec(mut f: F)
表明我们的exec
接收的是一个可变类型的闭包。事实上,FnMut
只是trait的名字,声明变量为FnMut
和要不要mut没啥关系,FnMut
是推导出的特征类型,mut
是rust语言层面的一个修饰符,用于声明一个绑定是可变的。Rust从特征类型系统和语言修饰符两方面保障了我们的程序正确运行。在使用FnMut
类型闭包时需要捕获外界的可变借用,因此我们常常搭配mut
修饰符使用,但二者是相互独立的。这段代码的流程是:在main
函数中,首先创建了一个可变的字符串s
,然后定义了一个可变类型闭包update_string
,该闭包接受一个字符串参数并将其追加到s
中。接下来调用了exec
函数,并将update_string
闭包的所有权移交给它。最后打印出了字符串s
的内容,update_string
没有实现Copy
特征,因此也只能调用一次,但并不是所有闭包都没有实现Copy
特征,闭包自动实现Copy
特征的规则是:只要闭包捕获的类型都实现了Copy
特征的话,这个闭包就会默认实现Copy
特征。例如取得的是字符串字面量s
的不可变引用,则是能Copy
的。而如果拿到的是s
的所有权或可变引用,都是不能Copy:
//不可变引用
let s = String::new();
let update_string = || println!("{}",s);
// 拿所有权
let s = String::new();
let update_string = move || println!("{}", s);
exec(update_string);
// exec2(update_string); // 不能再用了
// 可变引用
let mut s = String::new();
let mut update_string = || s.push_str("hello");
exec(update_string);
// exec1(update_string); // 不能再用了
3、不可变借用:
Fn
特征以不可变借用的方式捕获环境中的值
fn main() {
let s = "hello, ".to_string();
let update_string = |str| println!("{},{}",s,str);
exec(update_string);
println!("{:?}",s);
}
fn exec<'a, F: Fn(String) -> ()>(f: F) {
f("world".to_string())
}
因为无需改变 s
,因此闭包中只对 s
进行了不可变借用,那么在就在exec
中将其标记为 Fn
特征,而如果在上一例中,将fn exec<'a, F: FnMut(&'a str)>(mut f: F) { 中的FnMut(&'a str)替换成Fn(&'a str)就会报错,因为闭包实现的是 FnMut
特征,需要的是可变借用,但是在 exec
中却给它标注了 Fn
特征,因此产生了不匹配。
三种Fn关系:
一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们。move
本身强调的就是如何捕获变量。例如,下面的闭包中使用了 move
关键字,所以我们的闭包捕获了它,但是由于闭包对 s
的使用仅仅是不可变借用,因此该闭包实际上还实现了 Fn
特征。
fn main() {
let s = String::new();
let update_string = move || println!("{}",s);
exec(update_string);
}
fn exec<F: FnOnce()>(f: F) { //也可以写成:fn exec<F: Fn()>(f: F) {
f()
}
因此,一个闭包并不仅仅实现某一种 Fn
特征,规则如下:
- 所有的闭包都自动实现了
FnOnce
特征,因此任何一个闭包都至少可以被调用一次 - 没有移出所捕获变量的所有权的闭包自动实现了
FnMut
特征 - 不需要对捕获变量进行改变的闭包自动实现了
Fn
特征
关于第二条规则,有如下示例:
fn main() {
let mut s = String::new();
let update_string = |str| -> String {s.push_str(str); s };
exec(update_string);
}
fn exec<'a, F: FnMut(&'a str) -> String>(mut f: F) {
f("hello");
}
此例中,闭包从捕获环境中移出了变量 s
的所有权,因此这个闭包仅自动实现了 FnOnce
,未实现 FnMut
和 Fn
。从特征约束能看出来 Fn
的前提是实现 FnMut
,FnMut
的前提是实现 FnOnce
,因此要实现 Fn
就要同时实现 FnMut
和 FnOnce,
在实际项目中,建议先使用 Fn
特征,然后编译器会告诉你正误以及该如何选择。
函数返回值中的闭包
Rust 要求函数的参数和返回类型,必须有固定的内存大小,例如 i32
就是 4 个字节,引用类型是 8 个字节,总之,绝大部分类型都有固定的大小,但是不包括特征,因为特征类似接口,对于编译器来说,无法知道它后面藏的真实类型是什么,因为也无法得知具体的大小。因此不能直接使用用Fn(i32) -> i32
特征来代表 |x| x + num
。需使用impl Fn(i32) -> i32
的返回值形式,说明我们要返回一个闭包类型,它实现了 Fn(i32) -> i32
特征,但这种impl Trait
的返回方式只能返回同样的类型,对于不同的闭包类型,需用特征对象,Box
的方式即可实现:
fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
let num = 5;
if x > 1{
Box::new(move |x| x + num)
} else {
Box::new(move |x| x - num)
}
}