昨天学习了Rust的所有权规则,Rust的所有权和生命周期是Rust同其他编程语言的主要区别所在。Rust的所有权规则有以下3条:
- Rust中的每个值都有一个所有者
- 一个值在同一时刻只能有一个所有者
- 当所有者离开作用域时,其拥有的值将被丢弃
在Rust中一个值在同一时刻只能有一个所有者,因为如果允许共享所有权,就会带来使用和释放上的问题,就只能选择其他编程语言管理内存的方式。 那么什么情况下会发生所有权不唯一的问题呢?有以下三种情况:
- 一个变量赋给另一个变量
- 变量作为参数传递给另一个函数调用
- 返回值从函数返回
以上三种情况,其实都可以理解为一个变量赋给另一个变量。那么在这些情况下,Rust是如何保证单一所有权的呢?Rust主要使用了Copy和Move这两个语义来保证单一所有权。
Move语义
在学习Copy和Move这两个语义之前,还是先看一下昨天学习Rust所有权规则时的示例代码:
fn main() {
let word = String::from("hello");
let ch = 'e';
if let Some(i) = find(word, ch) {
println!("i = {}", i)
}
}
fn find(s: String, c: char) -> Option<usize> {
s.find(c)
}
我们昨天学习了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::Droptrait。
实现Drop trait的类型要实现一个drop函数,当其所有者变量离开作用域时就会自动调用该方法。例如std::vec::Vec就实现了Drop trait:
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<#[may_dangle] T, A: Allocator> Drop for Vec<T, A> {
fn drop(&mut self) {
unsafe {
// use drop for [T]
// use a raw slice to refer to the elements of the vector as weakest necessary type;
// could avoid questions of validity in certain cases
ptr::drop_in_place(ptr::slice_from_raw_parts_mut(self.as_mut_ptr(), self.len))
}
// RawVec handles deallocation
}
}
Rust不允许自身或其任何部分实现了Drop trait的类型使用Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用Copy trait,将会出现一个编译时错误。例如下面的代码就会出现编译错误:
#[derive(Copy,Clone)]
struct User {
name: String // compile error
}
这段代码定义了一个struct,但因为字段的name是String,String内部实现是Vec<u8>类型,Vec实现了Drop trait,此时对User类型使用Copy trait的话就会编译报错。
总结
本节我们在Rust所有权规则的基础上,进一步学习了Move语义和Copy语义:
- Move语义: 变量赋值、传参、函数返回可能(未实现Copy trait的类型)会导致Move,发生所有权转移。所有权转移后之前的变量将失效,之后将无法使用。
- Copy语义: 如果类型实现了Copy trait,那么赋值、传参、函数返回就会使用Copy语义,对应的值会被按位拷贝(浅拷贝),产生新的值。
- Rust不允许自身或其任何部分实现了Drop trait的类型使用Copy trait。
参考
- https://kaisery.github.io/trpl-zh-cn/ch04-01-what-is-ownership.html
- https://doc.rust-lang.org/std/marker/trait.Copy.html
- https://doc.rust-lang.org/std/ops/trait.Drop.html