Rust所有权与借用
学习笔记,对标cpp理解下rust所有权以及借用的概念,顺便提下比较特殊的切片(DST)
所有权
rust中每个值都有一个所有者变量,并且同一时间只有能一个所有者,当值的所有者变量超出作用域,值的内存会被释放。
下面的代码String
的所有权从s1
转移到了s2
,发生了Move,此时再访问s1
是非法的。参考
|
|
下面的代码i32
的所有权没有从x
转移到y
,而是y
复制了x
,发生了Copy
|
|
Move本质上也是浅拷贝:比如String
内部实现是有一个指针指向了保存的字符串,所有权转移,其实只是拷贝了这个指针的值,并没有拷贝这个字符串。这时s1
和s2
的内存空间都保存着这个指针地址,由于所有权的存在,编译器保证了访问s1
是非法的,所以s1
虽然还指向字符串,但是什么都做不了,保证了安全。
关于内存释放:由于只有在所有者生命完结后,才会发生释放,所有权保证同一时间只有一个所有者,所以字符串所在地址并不会被释放两次。
这里我想对标cpp:cpp实现类似高效转移使用的是右值引用与移动构造函数。在s2
的移动构造函数中把s1
的指针偷过来,然后把s1
的指针指向一个空字符串的地址或标记其无效。s1
是作为右值引用传过来的,在语义上是将亡值,所以可以修改s1
内部结构。但是,cpp没有所有权概念,编译器不会阻止你继续访问s1
。这很安全(个屁
rust在什么情况下Move什么情况下Copy,取决于类型是否实现了Copy
Trait。上面i32
本来已经很小了,也没东西可以浅拷贝(就4个字节折腾啥),所以i32
是拷贝语义。
rust基本类型几乎都实现了Copy
Trait:
|
|
对于tuple、array,如果元素都实现了Copy
,也会传拷贝。对于复杂类型,如果一个类型的某个部分实现了Drop
Trait,那么这个类型无法实现Copy
;如果组成部分都实现了Copy
,复杂类型也可以实现Copy
。
所有权转移可以发生在赋值、传参、函数返回。
引用&借用
下面的代码中b
并没有拿走所有权,而是通过&
取得了a
的引用。
|
|
b
是对a
的引用也可以描述为b
借用了a
,rust引用的底层可以对标为其他语言中的指针,只不过rust的引用带了生命周期和借用检查所以很安全。如cpp中的指针只是记录值了一个内存地址,与一个整型并没有啥差别,可以被保存被带到任何地方,容易发生内存泄漏。而rust编译器会保证引用的生命周期不会超过其指向的值的生命周期。
引用分为不可变引用与可变引用,获取可变引用使用let b = &mut a
,前提是a
是可变的才能获取可变引用,可变引用与不可变引用的关系类似读写锁:
- 可以同时存在多个不可变引用(读锁)
- 可变引用与不可变引用不能同时存在(读、写锁互斥)
- 只能同时有一个可变引用(写锁)
切片
切片很特殊,用来引用数组中的连续元素序列。
- 字符串切片 -
&str
let s = String::from("hello world"); let hello = &s[0..5];
let s: &str = "xxx"; let s2: &str = &s[..];
- 字符串切片特殊点是范围只能取有效的utf8字符边界
- 数组切片 -
&[T]
let a = [1, 2, 3, 4, 5]; let slice = &a[1..3];
切片用[start..end]来确定引用范围,区间左闭右开[start,end)
。范围还可以简写为[..2]
、[3..]
、[..]
,省略表示取边界。
切片是个胖指针,会保存目标集合的指针,与引用范围。
切片之所以特殊,需要说下rust的动态大小类型,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类型,就无法声明对应类型变量