闭包的定义
闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值,例如:
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)
}
}
本文详细解释了闭包在Rust中的概念,包括其定义、特性(如Fn、FnOnce、FnMut)、如何捕获变量(所有权转移、可变借用和不可变借用),以及在函数参数和返回值中的使用。特别关注了Rust中trait和implTrait在闭包中的应用。
1114

被折叠的 条评论
为什么被折叠?



