rust语言基础学习: rust所有权之Move和Copy语义
2020-07-08
昨天学习了Rust的所有权规则,Rust的所有权和生命周期是Rust同其他编程语言的主要区别所在。Rust的所有权规则有以下3条:
- Rust中的每个值都有一个所有者
- 一个值在同一时刻只能有一个所有者
- 当所有者离开作用域时,其拥有的值将被丢弃
在Rust中一个值在同一时刻只能有一个所有者,因为如果允许共享所有权,就会带来使用和释放上的问题,就只能选择其他编程语言管理内存的方式。 那么什么情况下会发生所有权不唯一的问题呢?有以下三种情况:
- 一个变量赋给另一个变量
- 变量作为参数传递给另一个函数调用
- 返回值从函数返回
以上三种情况,其实都可以理解为一个变量赋给另一个变量。那么在这些情况下,Rust是如何保证单一所有权的呢?Rust主要使用了Copy和Move这两个语义来保证单一所有权。
Move语义 #
在学习Copy和Move这两个语义之前,还是先看一下昨天学习Rust所有权规则时的示例代码:
1fn main() {
2 let word = String::from("hello");
3 let ch = 'e';
4 if let Some(i) = find(word, ch) {
5 println!("i = {}", i)
6 }
7}
8
9fn find(s: String, c: char) -> Option<usize> {
10 s.find(c)
11}
我们昨天学习了String类型在 编译时无法确定占用内存大小且在运行时其大小可能会发生变化,所以其被分配到了堆上。胖指针word中保存了堆内存中数据的地址,即word引用堆内存上的数据。当执行到第4行,调用find函数时,传参时word胖指针中的地址等值被移动到了find函数中的s中,main函数中的word变量将会失效,Rust的编译器保证在随后的代码中无法再使用word变量,这个程序进程的内存布局示意图如下:
这里word
到s
传参赋值就是Move
的语义,即word被移动
到了s中,这样就保证了堆内存中的数据在同一时刻只有一个所有者。
此时,细心的你会在这个图上发现,为什么上图中ch
到c
的传参赋值不是Move的语义呢,为什么在随后的代码中变量ch
没有失效呢?
反过来想,如果ch
到c
传参也是Move的语义的话,那代码就会变的很复杂,因为ch的数据是存储在栈上的简单数据(char类型),显然在这里不希望发生所有权的转移,基于这一点Rust又提供了Copy
的语义。
Copy语义 #
Rust中提供了一个std::marker::Copy
trait。如果一个类型实现了这个Copy trait,那么这个类型的变量赋给这个类型的其他变量时,就会使用Copy语义。Copy语义指的是在赋值、传参或函数返回时,值会自动按位拷贝(浅拷贝)。示例代码中的ch变量的char类型就实现了Copy trait,因此ch
到c
的传参时自动按位浅拷贝,从内存示意图也可以看栈内存中参数c
的数据拷贝自变量ch
的数据,这样新旧数据的所有者还是原来的变量,没有发生所有权转移。
那么还有哪些类型实现了Copy trait呢?可以从Copy trait的文档https://doc.rust-lang.org/std/marker/trait.Copy.html#implementors找到答案。整理归纳如下:
- 所有的整形类型,例如
i32
- 布尔类型
bool
- 所有的浮点类型,例如
f64
- 字符类型
char
- 元组,当且仅当其包含的类型也都实现了Copy trait,例如
(i32, i32)
实现了Copy,但(i32, String)
就没有 - 数组,当且仅当其内部元素类型实现了Copy trait
- ……
Drop trait #
学习了Move和Copy的语义之后,再来看一下所有权规则中的当所有者离开作用域时,其拥有的值将被丢弃
,进一步理解这句话。
- 对于分配在栈内存上的数据,当其所有者变量离开作用域时,栈内存上数据的也就不存在了(出栈)。
- 对于分配在堆内存上的数据,当其所有者变量离开作用域时,Rust会自动调用
drop
函数清理变量的堆内存。这个drop函数是哪里来的呢?这就引出了std::ops::Drop
trait。
实现Drop trait的类型要实现一个drop
函数,当其所有者变量离开作用域时就会自动调用该方法。例如std::vec::Vec
就实现了Drop trait:
1#[stable(feature = "rust1", since = "1.0.0")]
2unsafe impl<#[may_dangle] T, A: Allocator> Drop for Vec<T, A> {
3 fn drop(&mut self) {
4 unsafe {
5 // use drop for [T]
6 // use a raw slice to refer to the elements of the vector as weakest necessary type;
7 // could avoid questions of validity in certain cases
8 ptr::drop_in_place(ptr::slice_from_raw_parts_mut(self.as_mut_ptr(), self.len))
9 }
10 // RawVec handles deallocation
11 }
12}
Rust不允许自身或其任何部分实现了Drop trait的类型使用Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用Copy trait,将会出现一个编译时错误。例如下面的代码就会出现编译错误:
1#[derive(Copy,Clone)]
2struct User {
3 name: String // compile error
4}
这段代码定义了一个struct,但因为字段的name是String,String内部实现是Vec<u8>
类型,Vec
实现了Drop trait,此时对User类型使用Copy trait的话就会编译报错。
因为虽然User类型的变量被分配到栈上,但它的字段name的值是分配在堆上的,所以User的变量间赋值需要时Move语义,而不能是Copy语义。
总结 #
本节我们在Rust所有权规则的基础上,进一步学习了Move语义和Copy语义:
- Move语义: 变量赋值、传参、函数返回
可能
(未实现Copy trait的类型)会导致Move,发生所有权转移。所有权转移后之前的变量将失效,之后将无法使用。 - Copy语义: 如果类型实现了Copy trait,那么赋值、传参、函数返回就会使用Copy语义,对应的值会被按位拷贝(浅拷贝),产生新的值。
- Rust不允许自身或其任何部分实现了Drop trait的类型使用Copy trait。