序
上一期我们讲到了生命周期,了解了一些简单生命周期的应用,这一期我们来谈谈我们前端友好的内容:迭代器
与闭包
。
闭包 Closure
谈起闭包,前端小伙伴就来精神了,这玩意我们熟呀。一会我们学完在对比对比看看是不是和你想象的闭包一样。
闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值.
rust 闭包使用||
开始 相当于 javascript 剪头函数()=>
的作用。
Rust 闭包在形式上借鉴了 Smalltalk
和 Ruby
语言,与函数最大的不同就是它的参数是通过 |parm1|
的形式进行声明,如果是多个参数就 |param1, param2,...|
, 下面给出闭包的形式定义:
|param1, param2,...| {
语句1;
语句2;
返回表达式
}
如果只有一个返回表达式的话,定义可以简化为:
|param1| 返回表达式
上例中还有两点值得注意:
- 闭包中最后一行表达式返回的值,就是闭包执行后的返回值,因此
action()
调用返回了intensity
的值10
let action = ||...
只是把闭包赋值给变量action
,并不是把闭包执行后的结果赋值给action
,因此这里action
就相当于闭包函数,可以跟函数一样进行调用:action()
闭包的类型推导
与函数相反,闭包并不会作为 API 对外提供,因此它可以享受编译器的类型推导能力,无需标注参数和返回值的类型。
为了增加代码可读性,有时候我们会显式地给类型进行标注,出于同样的目的,也可以给闭包标注类型:
但不标注会更简洁,可以自己取舍。下面就是一个例子
三种不同的闭包展示了三种不同的省略使用方式:
- 省略参数
- 返回值类型
- 花括号对
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 ;
注意:虽然类型推导很好用,但是它不是泛型,当编译器推导出一种类型后,它就会一直使用该类型:
#![allow(unused)]
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
首先,在 s
中,编译器为 x
推导出类型 String
,但是紧接着 n
试图用 5
这个整型去调用闭包,跟编译器之前推导的 String
类型不符,就会报错。
结构体中的闭包
我们可以利用结构体配合闭包,实现数据缓存。
我们来实现一个简易缓存,功能是获取一个值,然后将其缓存起来,那么可以这样设计:
- 一个闭包用于获取值
- 一个变量,用于存储该值
Fn(u32) -> u32
这里出现了一个我们看起来好像陌生的东西,我们前面学到了where
字句,可以约束泛型。显然这里也是,
#![allow(unused)]
fn main() {
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
query: T,
value: Option<u32>,
}
}
可以看得出这一长串是 T
的特征约束,再结合之前的已知信息:query
是一个闭包,大概可以推测出,Fn(u32) -> u32
是一个特征,用来表示 T
是一个闭包类型。
那为什么不用具体的类型来标注 query
呢?原因很简单,每一个闭包实例都有独属于自己的类型,即使于两个签名一模一样的闭包,它们的类型也是不同的,因此你无法用一个统一的类型来标注 query
闭包。
而标准库提供的 Fn
系列特征,再结合特征约束,就能很好的解决了这个问题. T: Fn(u32) -> u32
意味着 query
的类型是 T
,该类型必须实现了相应的闭包特征 Fn(u32) -> u32
。
特征 Fn(u32) -> u32
从表面来看,就对闭包形式进行了显而易见的限制:该闭包拥有一个u32
类型的参数,同时返回一个u32
类型的值。
闭包也是一种特殊的函数,所以Fn 特征不仅仅适用于闭包,还适用于函数,因此上面的
query
字段除了使用闭包作为值外,还能使用一个具名的函数来作为它的值
接着我们完善一下
#![allow(unused)]
fn main() {
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
}
}
}
}
}
上面的缓存有一个很大的问题:只支持 u32
类型的值,若我们想要缓存 &str
类型,显然就行不通了,因此需要将 u32
替换成泛型 E
,有兴趣可以自己试试看。
捕获作用域中的值
捕获作用域中的值.我们看下面的例子,闭包中可以直接使用x
变量。
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
对于函数来说,就算你把函数定义在 main
函数体中,它也不能访问 x
:
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool {
z == x
}
let y = 4;
assert!(equal_to_x(y));
}
报错如下:
error[E0434]: can't capture dynamic environment in a fn item // 在函数中无法捕获动态的环境
--> src/main.rs:5:14
|
5 | z == x
| ^
|
= help: use the `|| { ... }` closure form instead // 使用闭包替代
如上所示,编译器准确地告诉了我们错误,甚至同时给出了提示:使用闭包来替代函数,这种聪明令我有些无所适从,总感觉会显得我很笨。
闭包对内存的影响
当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。
javascript不合理私用闭包会造成内存泄漏
三种 Fn 特征
闭包捕获变量有三种途径,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用,因此相应的 Fn
特征也有三种:
-
FnOnce
,该类型的闭包会拿走被捕获变量的所有权。Once
顾名思义,说明该闭包只能运行一次:fn fn_once<F>(func: F) where F: FnOnce(usize) -> bool, { println!("{}", func(3)); println!("{}", func(4)); } fn main() { let x = vec![1, 2, 3]; fn_once(|z|{z == x.len()}) }
仅实现
FnOnce
特征的闭包在调用时会转移所有权,所以显然不能对已失去所有权的闭包变量进行二次调用。因为
F
没有实现Copy
特征,所以会报错,那么我们添加一个约束,试试实现了Copy
的闭包:where F: FnOnce(usize) -> bool + Copy
func
的类型F
实现了Copy
特征,调用时使用的将是它的拷贝,所以并没有发生所有权的转移。如果你想强制闭包取得捕获变量的所有权,可以在参数列表前添加
move
关键字,这种用法通常用于闭包的生命周期大于捕获变量的生命周期时,例如将闭包返回或移入其他线程。#![allow(unused)] fn main() { use std::thread; let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Here's a vector: {:?}", v); }); handle.join().unwrap(); }
-
FnMut
,它以可变借用的方式捕获了环境中的值,因此可以修改该值:fn main() { let mut s = String::new(); let mut update_string = |str| s.push_str(str); update_string("hello"); println!("{:?}",s); }
注意:想要在闭包内部捕获可变借用,需要把该闭包声明为可变类型
-
Fn
特征,它以不可变借用的方式捕获环境中的值 让我们看看下面这个例子
fn main() {
let mut s = String::new();
let update_string = |str| s.push_str(str);
exec(update_string);
println!("{:?}",s);
}
fn exec<'a, F: Fn(&'a str)>(mut f: F) {
f("hello")
}
运行代码就会报错:我们的闭包实现的是 FnMut
特征,需要的是可变借用,但是在 exec
中却给它标注了 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
特征就完全正确。
move 和 Fn
在上面,我们讲到了 move
关键字对于 FnOnce
特征的重要性,但是实际上使用了 move
的闭包依然可能实现了 Fn
或 FnMut
特征。
因为,一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们。move
本身强调的就是后者,闭包如何捕获变量:
fn main() {
let s = String::new();
let update_string = move || println!("{}",s);
exec(update_string);
}
fn exec<F: FnOnce()>(f: F) {
f()
}
上面的闭包中使用了 move
关键字,所以我们的闭包捕获了它,但是由于闭包对 s
的使用仅仅是不可变借用,因此该闭包实际上还实现了 Fn
特征。
细心的读者肯定发现我在上段中使用了一个 还
字,这是什么意思呢?因为该闭包不仅仅实现了 FnOnce
特征,还实现了 Fn
特征,将代码修改成下面这样,依然可以编译:
fn main() {
let s = String::new();
let update_string = move || println!("{}",s);
exec(update_string);
}
fn exec<F: Fn()>(f: F) {
f()
}
三种 Fn 的关系
实际上,一个闭包并不仅仅实现某一种 Fn
特征,规则如下:
- 所有的闭包都自动实现了
FnOnce
特征,因此任何一个闭包都至少可以被调用一次 - 没有移出所捕获变量的所有权的闭包自动实现了
FnMut
特征 - 不需要对捕获变量进行改变的闭包自动实现了
Fn
特征
闭包作为函数返回值
看到这里,相信大家对于如何使用闭包作为函数参数,已经很熟悉了,但是如果要使用闭包作为函数返回值,该如何做?
先来看一段代码:
#![allow(unused)]
fn main() {
fn factory() -> Fn(i32) -> i32 {
let num = 5;
|x| x + num
}
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
}
Rust 要求函数的参数和返回类型,必须有固定的内存大小,这里会发生错误
绝大部分类型都有固定的大小,但是不包括特征,因为特征类似接口,对于编译器来说,无法知道它后面藏的真实类型是什么,因为也无法得知具体的大小。
我们做一些限制:
#![allow(unused)]
fn main() {
fn factory(x:i32) -> impl Fn(i32) -> i32 {
let num = 5;
if x > 1{
move |x| x + num
} else {
move |x| x - num
}
}
}
在前面我们提到过trait
作为返回值,所有这里同样适用,impl Trait
的返回方式有一个非常大的局限,就是你只能返回同样的类型,所以这里还是会报错,就算签名一样的闭包,类型也是不同的
还记得我们上次错误处理时候遇到的Box<dny Error>
吗,这里我们也可以用类似的,使用特征对象
#![allow(unused)]
fn main() {
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闭包 vs JavaScript闭包
那我们来说一下Rust和JavaScript的闭包有什么区别:
相同点:
-
两种语言的闭包都定义了作为函数对象的代码块。
-
闭包都可以在定义时访问其定义环境的变量。
不同点:
- Rust闭包和JavaScript闭包对其捕获的变量的所有权和生命周期处理方式不同。
-
Rust闭包的所有权和生命周期限制更为严格,Rust编译器会检查闭包捕获的变量的生命周期是否符合规定。一般情况下,Rust闭包会复制其捕获的变量,从而拥有自己的内存所有权并避免了内存问题。而在JavaScript中,不需要担心所有权和生命周期的问题,因为JS使用的是垃圾回收机制,它会自动释放变量的内存空间。
-
Rust闭包可以使用move关键字获取其捕获的变量所有权,这意味着闭包可以移动变量,使变量在闭包被转移时不再可用。而JavaScript没有类似的特性。
- Rust闭包使用外部标记 #[derive(Copy, Clone)] 来实现复制其捕获的变量,而JavaScript没有类似的语言机制。
- Rust闭包捕获的每个变量必须显式复制或移动,否则不能超过闭包的定义范围,因为Rust编译器需要在编译时知道变量的生命周期。这种复制和移动会增加一些复杂性,但也可以有效避免一些内存问题。而JavaScript闭包捕获的变量没有显示复制或移动的过程。
- Rust闭包使用命名参数传递捕获的变量,而JavaScript使用变量名。
- Rust闭包中,捕获变量可以通过传递其名称给闭包来实现。而在JavaScript中,捕获到的变量可以直接使用变量名访问。
Rust闭包对变量所有权和生命周期的控制更加严格,同时也需要显式复制或移动闭包捕获的变量。而JavaScript闭包则由于使用了垃圾回收机制,对变量所有权和生命周期没有那么严格的限制。
结语
关于rust的闭包我们就讲完了,对于JavaScript来说,我们需要做的可能比较少,rust是一门对安全性要求严格的语言,语法上还是会有很大的区别。