Rust所有权与借用

学习笔记,对标cpp理解下rust所有权以及借用的概念,顺便提下比较特殊的切片(DST)

rust中每个值都有一个所有者变量,并且同一时间只有能一个所有者,当值的所有者变量超出作用域,值的内存会被释放。

下面的代码String的所有权从s1转移到了s2,发生了Move,此时再访问s1是非法的。参考

1
2
let s1 = String::from("hello");
let s2 = s1;

下面的代码i32的所有权没有x转移到y,而是y复制了x,发生了Copy

1
2
let x: i32 = 1;
let y = x;

Move本质上也是浅拷贝:比如String内部实现是有一个指针指向了保存的字符串,所有权转移,其实只是拷贝了这个指针的值,并没有拷贝这个字符串。这时s1s2的内存空间都保存着这个指针地址,由于所有权的存在,编译器保证了访问s1是非法的,所以s1虽然还指向字符串,但是什么都做不了,保证了安全。

关于内存释放:由于只有在所有者生命完结后,才会发生释放,所有权保证同一时间只有一个所有者,所以字符串所在地址并不会被释放两次double free

这里我想对标cpp:cpp实现类似高效转移使用的是右值引用与移动构造函数。在s2的移动构造函数中把s1的指针偷过来,然后把s1的指针指向一个空字符串的地址或标记其无效。s1是作为右值引用传过来的,在语义上是将亡值,所以可以修改s1内部结构。但是,cpp没有所有权概念,编译器不会阻止你继续访问s1这很安全(个屁

rust在什么情况下Move什么情况下Copy,取决于类型是否实现了CopyTrait。上面i32本来已经很小了,也没东西可以浅拷贝(就4个字节折腾啥),所以i32是拷贝语义。

rust基本类型几乎都实现了CopyTrait:

1
2
3
pub trait Copy: Clone {
// Empty. 只是个Marker
}

对于tuple、array,如果元素都实现了Copy,也会传拷贝。对于复杂类型,如果一个类型的某个部分实现了DropTrait,那么这个类型无法实现Copy;如果组成部分都实现了Copy,复杂类型也可以实现Copy

所有权转移可以发生在赋值、传参、函数返回。

下面的代码中b并没有拿走所有权,而是通过&取得了a的引用。

1
2
let a = String::from("123");
let b = &a;

b是对a的引用也可以描述为b借用了a,rust引用的底层可以对标为其他语言中的指针,只不过rust的引用带了生命周期和借用检查所以很安全。如cpp中的指针只是记录值了一个内存地址,与一个整型并没有啥差别,可以被保存被带到任何地方,容易发生内存泄漏。而rust编译器会保证引用的生命周期不会超过其指向的值的生命周期。

引用分为不可变引用immutable references可变引用mutable references,获取可变引用使用let b = &mut a,前提是a是可变的才能获取可变引用,可变引用与不可变引用的关系类似读写锁:

  1. 可以同时存在多个不可变引用(读锁)
  2. 可变引用与不可变引用不能同时存在(读、写锁互斥)
  3. 只能同时有一个可变引用(写锁)

切片很特殊,用来引用数组中的连续元素序列。

  1. 字符串切片string slices - &str
    • let s = String::from("hello world"); let hello = &s[0..5];
    • let s: &str = "xxx"; let s2: &str = &s[..];
    • 字符串切片特殊点是范围只能取有效的utf8字符边界
  2. 数组切片 - &[T]
    • let a = [1, 2, 3, 4, 5]; let slice = &a[1..3];

切片用[start..end]来确定引用范围,区间左闭右开[start,end)。范围还可以简写为[..2][3..][..],省略表示取边界。

切片是个胖指针,会保存目标集合的指针,与引用范围。

切片之所以特殊,需要说下rust的动态大小类型Dynamic Sized Type,DST,DST表示编译期无法获取大小的类型。

从数组说起,数组的类型表示为[T; N]T是元素类型而N是元素个数,所以数组的大小编译期是可以确定的,数组不是DST。注意&[i32; 3]是一个普通的数组引用,而&[i32]才是一个数组切片。

切片是DST,准确来说[i32]才叫做切片,[T]这种类型表示由T组成的切片,这个切片的长度在编译期是不确定的(DST),编译器无法为一个不确定大小的类型分配空间,所以也无法声明DST类型的变量,只能用胖指针&[T]来引用。

&[T]的大小是固定的,里面有用于存储数据地址和长度的空间,这样就可以在运行时获取长度信息。比如要制造切片[1..n],n的大小是编译期间无法得到的,所以只能在运行期间计算n的值,然后初始化胖指针完成引用。

字符串切片str也是DST,对应胖指针是&str,可以把str理解为[T]的特殊形式,主要是用来表示utf8字符串。

除了切片,dyn Trait(Trait对象)也是DST,对应的胖指针是&dyn Trait。(只要是DST类型,就无法声明对应类型变量