本文中的变量,指的是通过如下代码定义的常量 a 和变量 b。 实例指的是绑定到 a 的
i32
类型在 stack 内存的数据,和绑定到 b 变量的String
类型在 stack 内存和 heap 内存中的数据。
let a = 0_u32;
let mut b = "Hello".to_string();
先说说使用场景
- move、copy 的应用场景,主要是在变量赋值、函数调用的传入参数、函数返回值、闭包的变量捕获。
- clone 需要显式调用。
- drop 是在变量的作用范围结束时,被自动调用。
- 闭包中使用了外部变量,就会有闭包捕获。
move 语义
rust 中的类型,如果没有实现Copy
trait,那么在此类型的变量赋值、函数入参、函数返回值都是 move 语义。这是与 c++ 的最大区别,从 c++11 开始,右值引用的出现,才有了 move 语义。但 rust 天生就是 move 语义。
如下的代码中,变量 a 绑定的String
实例,被 move 给了 b 变量,此后a变量就是不可访问了(编译器会帮助我们检查)。然后 b 变量绑定的String
实例又被 move 到了 f1 函数中,,b 变量就不可访问了。f1 函数对传入的参数做了一定的运算后,再将运算结果返回,这是函数 f1 的返回值被 move 到了 c 变量。在代码结尾时,只有 c 变量是有效的。
fn f1(s: String) -> String {
s + " world!"
}
let a = String::from("Hello");
let b = a;
let c = f1(b);
注意,如上的代码中,String
类型没有实现Copy
trait,所以在变量传递的过程中,都是 move 语义。
copy 语义
rust 中的类型,如果实现了Copy
trait,那么在此类型的变量赋值、函数入参、函数返回值都是copy语义。这也是 c++ 中默认的变量传递语义。
看看类似的代码,变量a绑定的i32
实例,被 copy 给了b变量,此后a、b变量同时有效,并且是两个不同的实例。然后 a 变量绑定的i32
实例又被 copy 到了 f1 函数中,a变量仍然有效。传入 f1 函数的参数 i 是一个新的实例,做了一定的运算后,再将运算结果返回。这时函数f1的返回值被 copy 到了 c 变量,同时f1函数中的运算结果作为临时变量也被销毁(不会调用 drop,如果类型实现了Copy
trait,就不能有Drop
trait)。传入 b 变量调用 f1 的过程是相同的,只是返回值被 copy 给了d 变量。在代码结尾时,a、b、c、d 变量都是有效的。
fn f2(i: i32) -> i32 {
i + 10
}
let a = 1_i32;
let b = a;
let c = f1(a);
let d = f1(b);
这里再强调下,i32
类型实现了Copy
trait,所以整个变量传递过程,都是copy语义。
clone 语义
move 和 copy 语义都是隐式的,clone 需要显式的调用。
参考类似的代码,变量 a 绑定的String
实例,在赋值前先 clone 了一个新的实例,然后将新实例 move 给了 b 变量,此后 a、b 变量同时有效。然后 b 变量在传入 f1 函数前,又 clone 一个新实例,再将这个新实例 move 到 f1 函数中。f1 函数对传入的参数做了一定的运算后,再将运算结果返回,这里函数 f1 的返回值被 move 到了 c 变量。在代码结尾时,a、b、c 变量都是有效的。
fn f1(s: String) -> String {
s + " world!"
}
let a = String::from("Hello");
let b = a.clone();
let c = f1(b.clone());
在这个过程中,在隐式 move 前,变量clone出新实例并将新实例move出去,变量本身保持不变。
drop 语义
rust 的类型可以实现Drop
trait,也可以不实现Drop
trait。但是对于实现了Copy
trait 的类型,不能实现Drop
trait。也就是说Copy
和Drop
两个 trait 对同一个类型只能有一个,鱼与熊掌不可兼得。
变量在离开作用范围时,编译器会自动销毁变量,如果变量类型有Drop
trait,就先调用Drop::drop
方法,做资源清理,一般会回收heap内存等资源,然后再收回变量所占用的 stack 内存。如果变量没有Drop
trait,那就只收回 stack 内存。
正是由于在Drop::drop
方法会做资源清理,所以Copy
和Drop
trait 只能二选一。如果类型实现了Copy
trait,在 copy 语义中并不会调用Clone::clone
方法,不会做 deep copy,那就会出现两个变量同时拥有一个资源(比如说是 heap 内存等),在这两个变量离开作用范围时,会分别调用Drop::drop
方法释放资源,这就会出现 double free 错误。
copy 与 clone 语义区别
先看看两者的定义:
pub trait Clone: Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) {
*self = source.clone()
}
}
pub trait Copy: Clone {
// Empty.
}
Clone
是Copy
的 super trait,一个类型要实现Copy
就必须先实现Clone
。
再留意看,Copy
trait 中没有任何方法,所以在 copy 语义中不可以调用用户自定义的资源复制代码,也就是不可以做 deep copy。copy 语义就是变量在stack内存的按位复制,没有其他任何多余的操作。
Clone
中有 clone 方法,用户可以对类型做自定义的资源复制,这就可以做 deep copy。在 clone 语义中,类型的Clone::clone
方法会被调用,程序员在Clone::clone
方法中做资源复制,同时在Clone::clone
方法返回时,变量的 stack 内存也会被按照位复制一份,生成一个完整的新实例。
自定义类型实现Copy
和Clone
trait
Clone
trait,对于任何自定义类型都可以实现。Copy
trait只有自定义类型中的field全部实现了Copy
trait,才可以实现Copy
trait。
如下代码举例,struct S1
中的 field 分别是i32
和usize
类型,都是有Copy
trait,所以S1
可以实现Copy
trait。你可以通过#[derive(Copy, Clone)]
方式实现,也可以自己写代码实现。
struct S1 {
i: i32,
u: usize,
}
impl Copy for S1 {}
impl Clone for S1 {
fn clone(&self) -> Self {
// 此处是S1的copy语义调用。
// 正是i32和usize的Copy trait,才有了S1的Copy trait。
*self
}
}
但是对于如下的struct S2
,由于S2
的field中有String
类型,String
类型没有实现Copy
trait,所以S2
类型就不能实现Copy
trait。S2
中也包含了E1
类型,E1
类型没有实现Clone
和Copy
trait,但是我们可以自己实现S2
类型的Clone
trait,在Clone::clone
方法中生成新的E1
实例,这就可以 clone 出新的S2
实例。
enum E1 {
Text,
Digit,
}
struct S2 {
u: usize,
e: E1,
s: String,
}
impl Clone for S2 {
fn clone(&self) -> Self {
// 生成新的E1实例
let e = match self.e {
E1::Text => E1::Text,
E1::Digit => E1::Digit,
};
Self {
u: self.u,
e,
s: self.s.clone(),
}
}
}
注意,在这种情况下,不能通过#[derive(Clone)]
自动实现S2
类型的Clone
trait。只有类型中的所有 field 都有Clone
,才可以通过#[derive(Clone)]
自动实现Clone
trait。
闭包捕获变量
与闭包关联的是三个 trait 的定义,分别是FnOnce
、FnMut
和Fn
,定义如下:
pub trait FnOnce<Args> {
type Output;
fn call_once(self, args: Args) -> Self::Output;
}
pub trait FnMut<Args>: FnOnce<Args> {
fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait Fn<Args>: FnMut<Args> {
fn call(&self, args: Args) -> Self::Output;
}
注意三个 trait 中方法的 receiver 参数,FnOnce
是self
参数,FnMut
是&mut self
参数,Fn
是&self
参数。
原则说明如下:
- 如果闭包只是对捕获变量的非修改操作,闭包捕获的是
&T
类型,闭包按照Fn
trait 方式执行,闭包可以重复多次执行。 - 如果闭包对捕获变量有修改操作,闭包捕获的是
&mut T
类型,闭包按照FnMut
trait 方式执行,闭包可以重复多次执行。 - 如果闭包会消耗掉捕获的变量,变量被 move 进闭包,闭包按照
FnOnce
trait 方式执行,闭包只能执行一次。
对于实现Copy
trait 和没有实现Copy
trait 对类型,具体参考如下对代码说明。
1. 类型实现了Copy
,闭包中是&T
操作
如下的代码,f 闭包对 i 变量,没有修改操作,此处捕获到的是&i
,所以 f 就是按照Fn
trait 方式执行,可以多次执行f。
fn test_fn_i8() {
let mut i = 1_i8;
let f = || i + 1;
// f闭包对i是immutable borrowed,是Fn trait
let v = f();
// f闭包中只是immutable borrowed,此处可以再做borrowed。
dbg!(&i);
// f可以调用多次
let v2 = f();
// 此时,f闭包生命周期已经结束,i已经没有borrowed了,所以此处可以mutable borrowed。
i += 10;
assert_eq!(2, v);
assert_eq!(2, v2);
assert_eq!(11, i);
}
2. 类型实现了Copy
,闭包中是&mut T
操作
如下的代码,f 闭包对 i 变量,有修改操作,此处捕获到的是&mut i
,所以f就是按照FnMut
trait 方式执行,注意 f 本身也是mut
,可以多次执行 f。
fn test_fn_mut_i8() {
let mut i = 1_i8;
let mut f = || {
i += 1;
i
};
// f闭包对i是mutable borrowed,是FnMut trait
let v = f();
// i已经被mutable borrowed,就不能再borrowed了。
// dbg!(&i);
// f可以调用多次
let v2 = f();
// 此时,f闭包生命周期已经结束,i没有mutable borrowed了,所以此处可以mutable borrowed。
i += 10;
assert_eq!(2, v);
assert_eq!(3, v2);
assert_eq!(13, i);
}
3. 类型实现了Copy
,闭包使用move
关键字,闭包中是&mut T
操作
如下的代码,f 闭包对i变量,有修改操作,并且使用了move
关键字。由于i8
实现了Copy
trait,此处 i 会 copy 一个新实例,并将新实例 move 到闭包中,在闭包中的实际是一个新的i8
变量。f 就是按照FnMut
trait 方式执行,注意 f 本身也是mut
,可以多次执行 f。
重点说明,此处move
关键字的使用,强制 copy 一个新的变量,将新变量move进闭包。
fn test_fn_mut_i8_move() {
let mut i = 1_i8;
let mut f = move || {
i += 1;
i
};
// i8有Copy trait,f闭包中是move进去的新实例,新实例不会被消耗,是FnMut trait
let v = f();
// i8有Copy trait,f闭包中是move进去的新实例,i没有borrowed,所以此处可以mutable borrowed。
i += 10;
// f可以调用多次
let v2 = f();
assert_eq!(2, v);
assert_eq!(3, v2);
assert_eq!(11, i);
}
4. 类型没有实现Copy
,闭包中是&T
操作
如下的代码,f 闭包对 s 变量,没有修改操作,此处捕获到的是&s
,f 按照Fn
trait 方式执行,可以多次执行 f。
fn test_fn_string() {
let mut s = "Hello".to_owned();
let f = || -> String {
dbg!(&s);
"world".to_owned()
};
// f闭包对s是immutable borrowed,是Fn trait
let v = f();
// f闭包中是immutable borrowed,此处是第二个immutable borrowed。
dbg!(&s);
// f可以调用多次
let v2 = f();
// f闭包生命周期结束,s已经没有borrowed,所以此处可以mutable borrowed
s += " moto";
assert_eq!("world", &v);
assert_eq!("world", &v2);
assert_eq!("Hello moto", &s);
}
5. 类型没有实现Copy
,闭包中是&mut T
操作
如下的代码,f 闭包对 s 变量,调用push_str(&mut self, &str)
方法修改,此处捕获到的是&mut s
,f是按照FnMut
trait 方式执行,注意 f 本身是mut
,f 可以多次执行 f。
fn test_fn_mut_string() {
let mut s = "Hello".to_owned();
let mut f = || -> String {
s.push_str(" world");
s.clone()
};
// f闭包对s是mutable borrowed,是FnMut trait
let v = f();
// s是mutable borrowed,此处不能再borrowed。
// dbg!(&s);
// f可以多次调用
let v2 = f();
// f闭包生命周期结束,s已经没有borrowed,所以此处可以mutable borrowed
s += " moto";
assert_eq!("Hello world", &v);
assert_eq!("Hello world world", &v2);
assert_eq!("Hello world world moto", &s);
}
6. 类型没有实现Copy
,闭包使用move
关键字,闭包中是&mut T
操作
如下的代码,f 闭包对 s 变量,调用push_str(&mut self, &str)
方法修改,闭包使用move
关键字,s 被move 进闭包,s 没有被消耗,f 是按照FnMut
trait 方式执行,注意f本身是mut
,f 可以多次执行。
fn test_fn_mut_move_string() {
let mut s = "Hello".to_owned();
let mut f = move || -> String {
s.push_str(" world");
s.clone()
};
// s被move进f闭包中,s没有被消耗,是FnMut trait
let v = f();
// s被move进闭包,s不能被borrowed
// dbg!(&s);
// f可以多次调用
let v2 = f();
// s被move进闭包,s不能被borrowed,但是可以绑定新实例
s = "moto".to_owned();
assert_eq!("Hello world", &v);
assert_eq!("Hello world world", &v2);
assert_eq!("moto", &s);
}
7. 类型没有实现Copy
,闭包中是&mut T
操作,捕获的变量被消耗
如下的代码,f 闭包对 s 变量,调用push_str(&mut self, &str)
方法修改,s 被闭包消耗,此处捕获到的是 s 本身,s 被 move 到闭包中,闭包外部 s 就不可见了。f 是按照FnOnce
trait 方式执行,不可以多次执行 f。
fn test_fn_once_string() {
let mut s = "Hello".to_owned();
let f = || -> String {
s.push_str(" world");
s // s被消耗
};
// s被move进f闭包中,s被消耗,是FnOnce trait
let v = f();
// s变量已经被move了,不能再被borrowed
// dbg!(&s);
// f只能调用一次
// let v2 = f();
// s被move进闭包,s不能被borrowed,但是可以绑定新实例
s = "moto".to_owned();
assert_eq!("Hello world", v);
assert_eq!("moto", &s);
}
8. 类型没有实现Copy
,闭包使用move
关键字,闭包中是T
操作,捕获的变量被消耗
如下的代码,f 闭包对 s 变量,调用into_boxed_str(self)
方法,s 被闭包消耗,此处捕获到的是 s 本身,s 被 move 到闭包中,闭包外部 s 就不可见了。f 是按照FnOnce
trait 方式执行,不可以多次执行 f。
本例中move
关键字不是必须的。
fn test_fn_once_move_string() {
let mut s = "Hello".to_owned();
let f = move || s.into_boxed_str();
// s被move进f闭包中,s被消耗,是FnOnce trait
let v = f();
// s变量已经被move了,不能再被borrowed
// dbg!(&s);
// f只能调用一次
// let v2 = f();
// s被move进闭包,s不能被borrowed,但是可以绑定新实例
s = "moto".to_owned();
assert_eq!("Hello", &*v);
assert_eq!("moto", &s);
}
最后总结
rust 中 move、copy、clone、drop 语义和闭包捕获是 rust 中基本的概念,代码过程中随时要清楚每个变量的变化。这会让自己的思路更清晰,rustc 也会变得温柔驯服。
今天的文章Rust 中 move、copy、clone、drop 语义和闭包捕获 Fn,FnMut,FnOnce分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/22578.html