Rust_Chapter_3_所有权
所有权是Rust的一个关键概念,我时常会想这个东西我会不会叙述不清楚,截至今天我已经把Chapter_7读完了,也没有来写这一份所有权的md,就是如此。
简述所有权
Rust采用了所有权这种机制,在编译器就避开各种阻碍,精确调控垃圾回收机制,从而在不需要类似C/C++手动回收垃圾的机制和类似Java自动GC的机制下,保留高性能,又内存安全。
所有权规则
- Rust中的每一个值都有一个对应的变量作为它的所有者
- 在同一时间内,值有且仅有一个所有者
- 当所有者离开自己的作用域时,它持有的值就会被释放掉
这三条概念需要慢慢体悟
变量作用域
变量从声明的位置开始直到当前作用域结束都是有效的1
2
3
4{//由于变量s在这里还没有声明,所以它是不可用的
let s = "Hello";//从这里变量s开始变得可用
//执行与s相关的操作
}//作用域到这里结束,变量s变得不可用
String类型
同C++类型一致,String类型所指向的数据必然要创建在堆上,那这势必会引发”析构”的问题,当然在rust里我们不这么称呼
与C/C++不同,Rust提供了另一套解决方案:内存会自动地在拥有它的变量离开作用域后进行释放
审视上面的代码,有一个很适合用来回收内存给操作系统的地方:变量s离开作用域的地方。Rust在变量离开作用域时,会调用一个叫作drop的特殊函数。String类型的作者可以在这个函数中编写释放内存的代码(仍然是要手动书写drop函数的)。记住,Rust会在作用域结束的地方(即“}”处)自动调用drop函数。
其实String
本身就是智能指针,这将在本blog的第14章讲解,对于结构体而言,自身被析构时会调用自己内部成员的drop,这和cpp也没啥区别,当然结构体也是后话了,主要是目前我们没有cpp指针的概念,这些而默认智能指针的存在,使得析构一个栈对象和堆对象差别不大了(智能指针这个栈对象drop时,堆对象也会随之drop)
变量和数据交互的方式:移动
为保证重复释放内存不会出现,rust采用一种方法,即默认状态下赋值语句是移动而非拷贝赋值1
2
3let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
像这样的代码,最终会输出一个错误,s1已经是无效的了
当移动发生时,值的所有权已经不在原本的所有者手上了,这意味着原本的s1不是任何数据的所有者,故而不在作用域结束时调用drop,而s2接管了s1的数据,成为了所有者,就会在出作用域的时候进行drop
只对所有者(具有所有权的变量)进行drop,是通过编译器在编译时检查与推导实现的,一个变量是否在作用域结束时触发drop,在编译期就能够知道
变量和数据交互的方式:克隆
当你确实需要去深度拷贝String堆上的数据,而不仅仅是栈数据时,就可以使用一个名为clone的方法
下面是一个实际使用clone方法的例子:1
2
3let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
这段代码在Rust中完全合法,它显式复制了堆上的数据
trait: COPY
Rust提供了一个名为Copy的trait,它可以用于整数这类完全存储在栈上的数据类型(我们会在第10章详细地介绍trait)。一旦某种类型拥有了Copy这种trait,那么它的变量就可以在赋值给其他变量之后保持可用性。如果一种类型本身或这种类型的任意成员实现了Drop这种trait,那么Rust就不允许其实现Copy这种trait。尝试给某个需要在离开作用域时执行特殊指令的类型实现Copy这种trait会导致编译时错误
所有权与函数
将值传递给函数在语义上类似于对变量进行赋值。将变量传递给函数将会触发移动或复制,就像是赋值语句一样
可以通过函数的返回值的移动来转移所有权,可以直接将实参返回来将其所有权重新移交给原变量。
1 | struct Foo { |
根据上面这个例子,如果说函数返回值没被接受,所有权没被转移,则在被调用者函数末尾被析构
当然这种返回参数的所有权的方法是不推荐的,由于函数参数的移动问题,就产生了引用。
和C++的对比
其实在作用域结束后发生drop和C++的析构函数没有什么区别,只是由于Rust存在所有权机制,可以在编译期就检查出它为你添加的drop函数应该放在何处
C++对于析构函数的态度是,每个局部变量在出作用域之后都会发生语义上的析构,而Rust利用所有权机制,同一时刻只存在一个所有者具有值的所有权,同时只有所有者出作用域时drop,使得不会发生额外的析构。这意味着编译器检查时不会出现drop俩两次,释放两次内存的情况
C++对于变量处理的态度是一致的,而Rust区分是否具有所有权,是否为所有者。事实上即使是对于C++11添加的移动语义,其也只是在移动构造函数等地方把移动的对象内部的指针变成nullptr
,实质上在作用域最后仍然需要对移动后的对象进行析构,只不过C++允许delete nullptr罢了
对于C++中指针/引用空悬等问题,是交由后面会提到的Rust的生命周期来解决的
引用
这个概念应该是来自于C/C++
1 | fn main() { |
引用是获取变量使用权但不获取其所有权的一种方式,引用指向的是被引用变量的地址,而不是内存中数据的地址,实际上我们拥有的是引用的所有权,所以 Drop 的也是引用变量而已。和C++一致可以用&引用,用*解引用。
借用是指:当没有定义引用变量,而是直接使用&var,将变量的引用作为参数传入函数时,此时就发生了借用。
我们可以声明可变引用,但可变引用在使用上有一个很大的限制:对于特定作用域中的特定数据来说,一次只能声明一个可变引用,利用这个性质,我们可以避免数据竞争,安全的写入原本的数据
引用的规则
让我们简要地概括一下本节对引用的讨论:
- 在任何一段给定的时间里,你要么只能拥有一个可变引用,要
么只能拥有任意数量的不可变引用。 - 引用总是有效的。
切片
slice是对数据类型中一部分值的不可变引用
小小用一下书里给的一些例子展示一下
1 | let s = String::from("hello"); |
由于slice本身就是引用,所以在定义了slice后,对原本值的修改都会被 Rust 编译器检查到。