Rust_Chapter_3_所有权

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
3
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);

像这样的代码,最终会输出一个错误,s1已经是无效的了
当移动发生时,值的所有权已经不在原本的所有者手上了,这意味着原本的s1不是任何数据的所有者,故而不在作用域结束时调用drop,而s2接管了s1的数据,成为了所有者,就会在出作用域的时候进行drop
只对所有者(具有所有权的变量)进行drop,是通过编译器在编译时检查与推导实现的,一个变量是否在作用域结束时触发drop,在编译期就能够知道

变量和数据交互的方式:克隆

当你确实需要去深度拷贝String堆上的数据,而不仅仅是栈数据时,就可以使用一个名为clone的方法
下面是一个实际使用clone方法的例子:

1
2
3
let 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Foo {
a: String,
}
impl Drop for Foo {
fn drop(&mut self) {
println!("{}", self.a);
}
}
fn test() -> Foo {
Foo {
a: String::from("test"),
}
}
fn main() {
test();
println!("Running!");
}
//输出结果如下:
//test
//Running!

根据上面这个例子,如果说函数返回值没被接受,所有权没被转移,则在被调用者函数末尾被析构

当然这种返回参数的所有权的方法是不推荐的,由于函数参数的移动问题,就产生了引用。

和C++的对比

其实在作用域结束后发生drop和C++的析构函数没有什么区别,只是由于Rust存在所有权机制,可以在编译期就检查出它为你添加的drop函数应该放在何处
C++对于析构函数的态度是,每个局部变量在出作用域之后都会发生语义上的析构,而Rust利用所有权机制,同一时刻只存在一个所有者具有值的所有权,同时只有所有者出作用域时drop,使得不会发生额外的析构。这意味着编译器检查时不会出现drop俩两次,释放两次内存的情况
C++对于变量处理的态度是一致的,而Rust区分是否具有所有权,是否为所有者。事实上即使是对于C++11添加的移动语义,其也只是在移动构造函数等地方把移动的对象内部的指针变成nullptr,实质上在作用域最后仍然需要对移动后的对象进行析构,只不过C++允许delete nullptr罢了
对于C++中指针/引用空悬等问题,是交由后面会提到的Rust的生命周期来解决的

引用

这个概念应该是来自于C/C++

1
2
3
4
5
6
7
8
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}

引用是获取变量使用权但不获取其所有权的一种方式,引用指向的是被引用变量的地址,而不是内存中数据的地址,实际上我们拥有的是引用的所有权,所以 Drop 的也是引用变量而已。和C++一致可以用&引用,用*解引用。

借用是指:当没有定义引用变量,而是直接使用&var,将变量的引用作为参数传入函数时,此时就发生了借用。

我们可以声明可变引用,但可变引用在使用上有一个很大的限制:对于特定作用域中的特定数据来说,一次只能声明一个可变引用,利用这个性质,我们可以避免数据竞争,安全的写入原本的数据

引用的规则

让我们简要地概括一下本节对引用的讨论:

  • 在任何一段给定的时间里,你要么只能拥有一个可变引用,要
    么只能拥有任意数量的不可变引用。
  • 引用总是有效的。

切片

slice是对数据类型中一部分值的不可变引用
小小用一下书里给的一些例子展示一下

1
2
3
4
5
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
let slice = &s[..];

由于slice本身就是引用,所以在定义了slice后,对原本值的修改都会被 Rust 编译器检查到。