深入浅出 Rust笔记 Series 3
系列导航
这是我在阅读范长春的《深入浅出 Rust》时做的笔记,绝大部分内容源自此书,另有小部分内容源自 Rust 圣经。这两份资料是我入门 Rust 的主要材料。
第三部分 高级抽象
chap 22 闭包
-
闭包(closure)是一种匿名函数,具有“捕获”外部变量的能力。闭包有时候也被称作 lambda 表达式。它有两个特点:(1)可以像函数一样被调用;(2)可以捕获当前环境中的变量。
1
2
3
4
5
6
7fn main() {
let add = | a :i32, b:i32 | -> i32 { return a + b; } ;
// 闭包的参数和返回值类型都是可以省略的
// let add = |a, b| { a + b };
let x = add(1,2);
println!("result is {}", x);
} -
变量捕获:closure 的原理与 C++11 的 lambda 非常相似。当一个 closure 创建的时候,编译器帮我们生成了一个匿名 struct 类型,通过自动分析 closure 的内部逻辑,来决定该结构体包括哪些数据,以及这些数据该如何初始化。
- 在保证能编译通过的情况下,编译器会自动选择一种对外部影响最小的类型存储。对于被捕获的类型为 T 的外部变量,在匿名结构体中的存储方式选择为:尽可能先选择&T 类型,其次选择&mut T 类型,最后选择 T 类型。
-
move 关键字:闭包前加上 move 关键字,所有的变量捕获全部使用 by value 的方式,所有被捕获的外部变量所有权一律转移进闭包。一般用于闭包需要传递到函数外部(escaping closure)的情况。
-
Fn/FnMut/FnOnce:闭包被调用的时候,不需要执行某个成员函数,而是采用类似函数调用的语法来执行。这是因为它自动实现了编译器提供的几个特殊的 trait,Fn 或者 FnMut 或者 FnOnce。
1
2
3
4
5
6
7
8
9
10
11
12pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
pub trait FnMut<Args> : FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait Fn<Args> : FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}这几个 trait 的主要区别在于,被调用的时候 self 参数的类型。FnOnce 被调用的时候,self 是通过 move 的方式传递的,因此它被调用之后,这个闭包的生命周期就已经结束了,它只能被调用一次;FnMut 被调用的时候,self 是&mut Self 类型,有能力修改当前闭包本身的成员,甚至可能通过成员中的引用,修改外部的环境变量;Fn 被调用的时候,self 是&Self 类型,只有读取环境变量的能力。
- 对于一个闭包,编译器是如何选择 impl 哪个 trait 呢?答案是,编译器会都尝试一遍,实现能让程序编译通过的那几个。闭包调用的时候,会尽可能先选择调用
fn call(&self,args:Args)
函数,其次尝试选择fn call_mut(&self,args:Args)
函数,最后尝试使用 fn call_once(self,args:Args)
函数。 - 为自定义类型实现 Fn trait 要使用 nightly 版本的 Rust
- 对于一个闭包,编译器是如何选择 impl 哪个 trait 呢?答案是,编译器会都尝试一遍,实现能让程序编译通过的那几个。闭包调用的时候,会尽可能先选择调用
-
每个闭包,编译器都会为它生成一个匿名结构体类型;即使两个闭包的参数和返回值一致,它们也是完全不同的两个类型,只是都实现了同一个 trait 而已。
-
闭包与泛型约束
1
2
3
4fn call_with_closure<F>(some_closure: F) -> i32
where F : Fn(i32) -> i32 {
some_closure(1)
}- 其中泛型参数 F 的约束条件是 F:Fn(i32)->i32。这里 Fn(i32)-> i32 是针对闭包设计的专门的语法,而不是像普通 trait 那样使用 Fn<i32,i32> 来写。这样设计为了让它们看起来跟普通函数类型 fn(i32)->i32 更相似。除了语法之外,Fn FnMut FnOnce 其他方面都跟普通的泛型一致。
-
向函数中传递闭包的两种方式(闭包作为函数参数)
-
通过泛型的方式。这种方式会为不同的闭包参数类型生成不同版本的函数,实现静态分派。
1
2
3
4
5
6// 这里是泛型参数。对于每个不同类型的参数,编译器将会生成不同版本的函数
fn static_dispatch<F>(closure: &F)
where F: Fn(i32) -> i32
{
println!("static dispatch {}", closure(42));
} -
通过 trait object 的方式。这种方式会将闭包装箱进入堆内存中,向函数传递一个胖指针,实现运行期动态分派。
1
2
3
4
5// 这里是 `trait object``Box<Fn(i32)->i32>`也算`trait object`
fn dynamic_dispatch(closure: &Fn(i32)->i32)
{
println!("dynamic dispatch {}", closure(42));
}
-
-
闭包作为函数的返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 如果我们希望一个闭包作为函数的返回值,那么就不能使用泛型的方式
// 了。因为如果泛型类型不在参数中出现,而仅在返回类型中出现的话,会要
// 求在调用的时候显式指定类型,编译器才能完成类型推导。可是调用方根本
// 无法指定具体类型,因为闭包类型是匿名类型,用户无法显式指定。所以下
// 面这样的写法是编译不过的:
fn test<F>() -> F
where F: Fn(i32)->i32
{
return | i | i * 2;
}
// 但下面这种是可以的
fn test<F>(arg: F) -> F
where F: Copy
{
arg
}- 静态分派。我们可以用一种新的语法
fn test() -> impl Fn(i32)-> i32
来实现。 - 动态分派。就是把闭包装箱进入堆内存中,使用
Box<dyn Fn(i32) -> i32>
这种 trait object 类型返回
- 静态分派。我们可以用一种新的语法
chap 23 动态分派和静态分派
-
Rust 可以同时支持“静态分派”(static dispatch)和“动态分派”(dynamic dispatch)。所谓“静态分派”,是指具体调用哪个函数,在编译阶段就确定下来了。Rust 中的“静态分派”靠泛型以及 impl trait 来完成。对于不同的泛型类型参数,编译器会生成不同版本的函数,在编译阶段就确定好了应该调用哪个函数。所谓“动态分派”,是指具体调用哪个函数,在执行阶段才能确定。Rust 中的“动态分派”靠 Trait Object 来完成。Trait Object 本质上是指针,它可以指向不同的类型;指向的具体类型不同,调用的方法也就不同。
-
Traitobject:向 Trait 的指针。&dyn Trait、&mut dyn Trait、Box
、const dyn Trait*、*mut dyn Trait 以及 Rc 等等都是 Trait Object。 1
2
3
4pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}- 自己在想:Trait Object 中指向实际数据的指针有什么用?
answer:访问 trait 中方法的 self 参数 - Rust 的动态分派和 C++ 的动态分派,内存布局有所不同。在 C++ 里,如果一个类型里面有虚函数,那么每一个这种类型的变量内部都包含一个指向虚函数表的地址。而在 Rust 里面,对象本身不包含指向虚函数表的指针,这个指针是存在于 trait object 指针里面的。如果一个类型实现了多个 trait,那么不同的 trait object 指向的虚函数表也不一样。
- 自己在想:Trait Object 中指向实际数据的指针有什么用?
-
object safe。有以下条件之一则不是 object safe,不能创建 trait object
- 当 trait 有 Self:Sized 约束时。如果不希望一个 trait 通过 trait object 的方式使用,可以为它加上 Self:Sized 约束。同理,如果我们想阻止一个函数在虚函数表中出现,可以专门为该函数加上 Self:Sized 约束。
- 当函数中有 Self 类型作为参数(除了 self)或者返回类型时。Rust 规定,如果函数中除了 self 这个参数之外,还在其他参数或者返回值中用到了 Self 类型,那么这个函数就不是 object safe 的。这样的函数是不能使用 trait object 来调用的。这样的方法是不能在虚函数表中存在的。
- 当函数第一个参数不是 self 时。如果有“静态方法”,那这个“静态方法”是不满足 object safe 条件的。这个条件几乎是显然的,编译器没有办法把静态方法加入到虚函数表中。如果一个 trait 中存在静态方法,而又希望通过 trait object 来调用其他的方法,那么我们需要在这个静态方法后面加上 Self:Sized 约束,将它从虚函数表中剔除。
- 当函数有泛型参数时。通过 trait object 调用成员的方法是通过 vtable 虚函数表来进行查找并调用。现在需要被查找的函数成了泛型函数,而泛型函数在 Rust 中是编译阶段自动展开的。这里有一个根本性的冲突问题。Rust 选择的解决方案是,禁止使用 trait object 来调用泛型函数,泛型函数是从虚函数表中剔除了的。这个行为跟 C++ 是一样的。C++ 中同样规定了类的虚成员函数不可以是 template 方法。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!