【Rust 进阶教程】 01 闭包与所有权
0x00 开篇
从本篇文章开始,我们进入 Rust 进阶教程
的学习,在这个模块中有新知识也有旧知识。对于就知识,我们会对它作更深入的理解。对于新知识,有关并发编程、模块化编程等等也都将一起介绍。
0x01 Fn、FnMut、FnOnce
前面已经介绍过闭包的基本概念了,那你有没有考虑过被闭包捕获变量的类型是引用还是所有权吗?
fn main() {
let hello = "hello rust".to_string();
let c = || {
// hello 在这里是什么呢?
println!("{}", hello);
};
c();
println!("{}", hello);
}
首先我们先来了解下这三个 trait Fn、FnMut、FnOnce
的概念。这三个trait 的区别在于它们是以哪种方式来捕获外部的变量。
Fn
: 可以被多次调用。这种闭包不能改变捕获变量的值,可以使用 &
来捕获变量的引用。对于 Copy
类型,则默认会以不可变引用的方式来捕获它。
fn main() {
let x = 10;
let c1 = || {
println!("{}", x);
};
c1();
c1();
println!("{}", x);
}
// 运行结果
// 10
// 10
// 10
FnMut
: 可以被多次调用。这种闭包可以改变捕获变量的值。
fn main() {
let mut y = 10;
let mut c2 = || {
y += 10;
};
c2();
c2();
println!("{}", y);
}
// 运行结果
// 30
FnOnce
: 只能被调用一次。这种闭包在调用后会将捕获变量的所有权移动到闭包内部,可以使用 move
关键字来捕获变量的所有权。因为闭包会移动变量的所有权,所以在使用完后就不能再次访问这些变量了。
fn main() {
let z = "rust".to_string();
let c3 = move || {
println!("{}", z);
};
c3();
// 无法再次调用
// c3();
// 无法再次使用 z
// println!("{}", z);
}
0x02 解读源码 Fn、FnMut、FnOnce
Fn
、FnMut
、FnOnce
是三个 trait
。先看源码:
pub trait Fn<Args: Tuple>: FnMut<Args> {
/// Performs the call operation.
#[unstable(feature = "fn_traits", issue = "29625")]
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
pub trait FnMut<Args: Tuple>: FnOnce<Args> {
/// Performs the call operation.
#[unstable(feature = "fn_traits", issue = "29625")]
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait FnOnce<Args: Tuple> {
/// The returned type after the call operator is used.
#[lang = "fn_once_output"]
#[stable(feature = "fn_once_output", since = "1.12.0")]
type Output;
/// Performs the call operation.
#[unstable(feature = "fn_traits", issue = "29625")]
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
我们可以看到,他们之间的关系是继承关系。
上面我们也只是通过概念来判断它们属于哪种类型,接下来我们用代码实际验证下:
我们创建 3 个函数(如下),如果编译通过,说明它的类型是没有问题的。
fn is_Fn<F>(_: &F) where F: Fn() -> () {}
fn is_FnMut<F>(_: &F) where F: FnMut() -> () {}
fn is_FnOnce<F>(_: &F) where F: FnOnce() -> () {}
实现代码如下:
fn main() {
let x = 5;
let c1 = || {
println!("{}", x);
};
let mut y = 6;
let mut c2 = || {
y += 1;
println!("{}", y);
};
let z = "rust".to_string();
let c3 = move || {
println!("{}", z);
};
is_Fn(&c1);
is_FnMut(&c1);
is_FnOnce(&c1);
is_FnMut(&c2);
is_FnOnce(&c2);
is_FnOnce(&c3);
}
// 运行结果
// 编译通过
由于Fn
、FnMut
、FnOnce
之间的继承关系。这就表明,c1
可以正常调用这三个函数,c2
可以调用 is_FnMut
和 is_FnOnce
。c3
只能调用 is_FnOnce
。
0x03 闭包到底是什么?(扩展阅读)
说了这么多,那么闭包导致是什么呢?我们也从源码中找到了些许答案。在 rustc_middle/sty.rs
中解释道:通常一个闭包可以被建模为一个结构体。
// rustc_middle/sty.rs
struct Closure<'l0...'li, T0...Tj, CK, CS, U>(...U);
'l0...'li
表示这个结构体具有 'l0
到 'li
这些生命周期参数。
T0...Tj
表示这个结构体还有 T0
到 Tj
这些类型参数。
CK
表示闭包的种类,是 ClousreKind
的缩写。有 Fn
、 FnMut
、FnOnce
三种。定义如下:
// rustc_middle/ty/closure.rs
pub enum ClosureKind {
// 这里也提示顺序很重要,Fn 是 FnMut 的 subtrait。Fn < FnMut < FnOnce。
Fn,
FnMut,
FnOnce,
}
CS
表示闭包的签名,是 Closure Signatures
的缩写,类似于函数的签名,看做是 fn()
类型。例如,fn(u32, u32) -> u32
意味着闭包实现了CK<(u32, u32), Output = u32>
。
U
可访问的参数的类型,在编程语言中通常简写为 upvar
是 upward-exposed variable
的缩写。表示在外部作用域中声明并在内部作用域中引用的变量。
那假设现在有一个闭包 c
:
let mut x: i32 = 5;
let mut c = || {
x += 5;
x
}
c();
那我们就可以将其视作一个闭包结构体类型,那上面的:
struct Closure<'a> {
x: i32
}
impl<'a> FnMut<()> for Closure<'a> {
type Output = i32;
fn call_mut(self) -> i32 {
self.s += 5;
self.s
}
}
// 最终的闭包调用形式类似
Closure {x: 5};
由于篇幅的问题,我这里也仅仅是浅浅的介绍了下闭包的定义。如果你想更深入的了解,可以自己看下源码。
0x04 小结
关于闭包,我们主要要分清使用的是哪种闭包,以及不同闭包的区别。最后再总结下吧:
Copy
类型: 当捕获变量时以不可变引用捕获。- 非
Copy
类型: 捕获变量时,可以通过不可变引用来捕获,也可以移动该值的所有权。
另外可变绑定类型,如果在闭包内需求对其进行修改操作,则需要使用可变 &mut T
来捕获。
转载自:https://juejin.cn/post/7227296961358856249