所有权和借用

Rust的内存安全性是其最为显著的特征。在以往,内存安全几乎都是通过GC的方式实现,但是GC会引来性能、内存占用的问题。

Rust因此采用了所有权系统。

所有权

所有的程序都得和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重。

在语言的演变中,产生了三种流派

  • GC: 程序运行的时候不断寻找不再使用的内存,eg: java go
  • 手动管理: 程序员自己去申请和释放内存,eg: C C++
  • 所有权系统: 编译器在编译期就能知道内存的使用情况,eg: Rust

例子

1
2
3
4
5
6
int* foo(){
int a ;
a = 100;
char *c = "xyz";
return &a;
}

a和c都是局部变量,但是局部变量a存在栈中,离开作用域之后,a申请的栈上内存都会被回收,从而造成了悬空指针的问题。

变量c是常量字符串,存储在常量区域, "xyz" 只有在整个程序结束后才能回收这片内存。

所以内存安全问题,一直都是程序员很头痛的问题。

Stack和Heap

栈

栈按照顺序存储值,并以相反顺序取出值,也叫做后进先出。

增加顺序叫做进栈,移出数据叫做出栈。

因为上述的实现方式,栈中的所有数据都必须占用已知且固定大小的内存空间。假设数据大小未知,那么在取出数据的时候无法得到想要的。

堆

对于大小未知或者可能变化的数据,我们需要将它存储在堆上。

当向堆上放入数据的时候,需要请求一定大小的内存空间,操作系统在堆上某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该地址的指针,该过程被称为在堆上分配内存。

接着,指针会被放到栈中,因为指针的大小是固定的。后续,我们通过栈中的指针来找到堆上的实际位置进而访问该数据

由上可知,堆是一种缺乏组织的数据结构

性能区别

栈上分配内存更快。因为在堆上放的时候需要先找到一块足够存放数据的内存空间。接着做一些记录为下一次分配做准备。

所有权和堆栈

当你的代码调用一个函数,传递给函数的参数依次被压入栈,调用结束时,这些值将按照相反的顺序被移除。

因为堆上数据缺乏组织,因此跟踪这些数据何时分配和释放时非常重要的。否则堆上的数据将内存泄漏

所有权原则

  1. Rust中每一个值都被一个变量所拥有,该变量被称为值的所有者。
  2. 一个值只能同时被一个变量拥有,一个值只能拥有一个所有者。
  3. 当所有者离开作用域时,该值将被丢弃。

变量作用域

作用域时一个变量在程序中有效的范围

1
let s = "hello"; // s 进入作用域

变量s绑定到了一个字符串字面值,该字符串字面值是硬编码到程序代码中的。
s 变量从声明的点开始直到当前作用域的结束都是有效的。

简单介绍一下string

我们已经见过字符串字面值 let s : &str = "hello" , s是被硬编码进程序的字符串值。字符串字面值是很方便的,但是它并不适用于所有场景

  • 字符串值是不可变的,因为被硬编码到了程序中
  • 并非所有字符串都能在编写代码的时候得知

例如,字符串是需要程序运行的时候由用户动态输入然后存储在内存中的,这种情况下,字符串字面值就完全无用武之地。

Rust提供了动态字符串类型 String, 该类型被分配在堆上,因此可以动态收缩,也就能存储在编译时大小未知的文本。

可以使用下面的方法基于字符串字面量来创建String类型

1
let s = String::from("hello");

:: 是一种调用运算符,表示调用String类型的from关联函数。因为string存储在堆上,因此它是动态的。可以这样修改:

1
2
3
4
5
6

let mut s = String::from("hello");

s.push_str(", world!"); // push_str() 在字符串后追加字面值

println!("{}", s); // 这段代码会打印 `hello, world!`

变量绑定背后的数据交互

转移所有权

1
2
let x = 5;
let y = x;

这段代码并没有发生所有权的转移: 代码先将5绑定到变量x,接着拷贝x值给y,最终x和y都等于5. 因为整数是rust基本数据类型,是固定大小的简单值。因此这两个值都是通过自动拷贝的方式来赋值的。

1
2
3

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

String是一个复杂类型,由存储在栈中的堆指针,字符串长度,容量共同组成,其中堆指针是最重要的。它指向了真实存储字符串的堆内存。至于长度和容量,就是: 容量指堆内存分配空间的大小,长度是目前已经使用的大小。

例如vector. 我们一般都会存一个capacity,一个size.用来实际存储数据的大小和分配的空间大小。

总之,String类型指向了一个堆上的空间,这里存储着它的真实数据,下面对上面的代码分成两种情况讨论

  1. 拷贝String 和 存储在堆上的字节数组。 如果该语句是拷贝所有数据(深拷贝) 那么无论是 String 本身 还是底层的堆上数据,都会被全部拷贝。
    这对于性能而言会造成非常大的影响。

  2. 只拷贝String本身。这样很快但是如果这样的话,s1和s2都会指向同一块堆内存,这样就会出现问题。因为当s1和s2都离开作用域时,Rust会尝试释放这块堆内存两次,从而造成了悬空指针的问题。

因此,rust这样解决问题。
当 s1被赋给了s2之后,rust会认为s1已经不再有效了,s1的所有权被转移给了s2。也就是说,s1不再是一个有效的变量,不能再使用它。

1
2
3
4
5

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

println!("{}", s1); // 这段代码会报错

想必读者都听说过: 浅拷贝和深拷贝。那么拷贝指针,长度和容量而不拷贝数据,这听起来就像是浅拷贝。但是又因为Rust同时使用第一个变量s1无效了,因此这个操作被称为移动(move),而不是浅拷贝。

上面的例子可以解读为s1被移动到了s2中。

再看一个

1
2
3
4
5
6

fn main(){
let x: &str = "hello";
let y = x;
println!("{},{}",x,y);
}

这段代码实际上不会报错。因为和String有一个本质的区别,string中 s1持有了通过 String::from() 分配的堆内存,而在这里,x只是引用了存储在二进制可执行文件中的字符串。并没有持有所有权

因此 let y = x 只是对这个值进行了拷贝

克隆(深拷贝)

Rust不会自动创建数据的深拷贝,因此,任何自动的复制都不是深拷贝。可以被认为对运行时的性能影响比较小

如果我们确实需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用clone方法

1
2
3
4
let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

如果性能无关紧要,可以使用clone简化编程体验。

但是对于执行较为频繁的代码,最好别用。

拷贝(浅拷贝)

1
2
3
4
let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);

原因是,整形这样的基本类型在编译的时候是已知大小的,会被存储在栈上,所以拷贝实际值是极快的。

Rust有一个叫做Copy的特征,可以用在类似整形这样在栈中存储的类型。

如果一个类型拥有了Copy特征,一个旧的变量在被赋值给其他变量之后仍然可用。

那么什么类型是可以copy的呢? 一般认为基本类型的组合是可以的。不需要分配内存或者形式资源的类型是可以copy的。

  • 整数类型
  • 布尔类型
  • 浮点类型
  • 字符类型
  • 元组类型,只要元组中的类型都是Copy的,那么这个元组类型也是Copy的
  • 不可变引用 &T

函数传值和返回

将值传给函数,一样会发生移动 or 复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

fn main(){
let s = String::from("hello");
takes_ownership(s); // s的值移动到了函数里, 所以之后不能再使用s

println!("{}",s); // 这段代码会报错

let x = 5; // x是copy的
makes_copy(x); // 因为i32是Copy的,所以之后仍然可以使用x
}

fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。没有特殊的操作

同样的,函数的返回值也具有所有权,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

fn main(){
let s1 = gives_ownership(); // gives_ownership 将返回值移动给 s1

let s2 = String::from("hello"); // s2 进入作用域

let s3 = takes_and_gives_back(s2); // s2 被移动到 takes_and_gives_back 中, 它也将返回值移动给 s3
}

fn gives_ownership() -> String { // gives_ownership 将返回值移动给调用它的函数
let some_string = String::from("hello"); // some_string 进入作用域
some_string // 返回 some_string 并移出给调用的函数
}

fn takes_and_gives_back(some_string: String) -> String { // some_string 进入作用域
some_string // 返回 some_string 并移出给调用的函数
}