昨天学习了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变量,这个程序进程的内存布局示意图如下:

rust-ownership-showcase-3.png

这里words传参赋值就是Move的语义,即word被移动到了s中,这样就保证了堆内存中的数据在同一时刻只有一个所有者。 此时,细心的你会在这个图上发现,为什么上图中chc的传参赋值不是Move的语义呢,为什么在随后的代码中变量ch没有失效呢? 反过来想,如果chc传参也是Move的语义的话,那代码就会变的很复杂,因为ch的数据是存储在栈上的简单数据(char类型),显然在这里不希望发生所有权的转移,基于这一点Rust又提供了Copy的语义。

Copy语义

Rust中提供了一个std::marker::Copy trait。如果一个类型实现了这个Copy trait,那么这个类型的变量赋给这个类型的其他变量时,就会使用Copy语义。Copy语义指的是在赋值、传参或函数返回时,值会自动按位拷贝(浅拷贝)。示例代码中的ch变量的char类型就实现了Copy trait,因此chc的传参时自动按位浅拷贝,从内存示意图也可以看栈内存中参数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:

 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。

参考