所有权

1 定义

所有权(ownership)Rust用于如何管理内存的一组规则。所有程序都必须管理其运行时使用计算机内存的方式。

一些语言中具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。

Rust则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序。

关于栈与堆的概念这里不再详解,在此有非常详细与形象的说明,读者自行学习。

访问栈数据的速度是更快的,但是会有一定的限制,所以合理的分配方法对于提升程序效率来说,是非常重要的。

https://kaisery.github.io/trpl-zh-cn/ch04-01-what-is-ownership.html

所有权遵循以下的规则:

  • 每一个值都有一个所有者(owner)
  • 每一个值在任一时刻有且只有一个所有者。
  • 当所有者(变量)离开作用域,这个值将被丢弃。

2 作用域

作用域是一个项(item)在程序中有效的范围。

假设有这样一个变量:

fn main(){ // s 在这里无效,它尚未声明
// 从此处起,s 是有效的
let s = "hello";
} // 此作用域已结束,s 不再有效

变量与数据的交互方式具有以下两种方式:移动(move)与克隆(clone)

移动

例2.1:

fn main(){
let x = 5;
let y = x;
}

对于简单的数据类型来说,其值已知切固定,编译器可以将其硬编码,因此此时会将xy都加入到栈中,也就是调用xy有效。

实际上,对于以下的数据类型,都满足此操作,被称为Copy的,或者称为实现了Copy trait

  • 所有整数类型,比如 u32。
  • 布尔类型,bool,它的值是 true 和 false。
  • 所有浮点数类型,比如 f64。
  • 字符类型,char。
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

而对于String类型来说,情况有所不同:

例2.2:

fn main(){
let s1 = String::from("hello");
let s2 = s1;
}

让我们详细了解这个过程中发生了什么。首先对于s1来说,实际上在进行这样的操作:

将实际的字符串数据存放在堆上,具有指向存放字符串内容内存的指针,长度和容量大一组数据存储在栈上。如图2.1所示。

Figure-2.1 字符串赋值(图片来源Rust程序设计语言)

当我们将s1赋值给s2String的数据被复制了,这意味着从栈上拷贝了它的指针、长度和容量,并没有复制指针指向的堆上数据。

Figure-2.2 s1赋值于s2(图片来源Rust程序设计语言)

但是这就存在一个问题:当s2s1离开作用域,它们都会尝试释放相同的内存。这是一个叫做二次释放(double free)的错误,也是之前提到过的内存安全性bug之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。

因此,对于下列的代码,将会报错:

例2.3:

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

println!("{s1}, world!");
}

 --> src/main.rs:5:15
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{s1}, world!");
| ^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

因此,在let s2 = s1;之后,Rust认为s1不再有效,也无需维护其回收的任务,这种操作叫做移动(move)

克隆

如果确实需要复制String堆上的数据,则可以使用clone()这个通用函数。

例2.4:

fn main(){
let s1 = String::from("hello");
let s2 = s1.clone();

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

此时可以正常执行。

3 所有权与函数

向函数传递参数的时候可能会发生克隆或者移动,如下例所示:

例3.1:

fn main() {
let s = String::from("hello"); // s 进入作用域

takes_ownership(s); // s 的值移动到函数里,这里不再有效

let x = 5; // x 进入作用域

makes_copy(x); // x 应该移动函数里,但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里,x 先移出了作用域,然后是 s。但因为 s 的值已被移走,没有特殊之处

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 移出作用域。没有特殊之处

函数返回值可以转移所有权,如下例所示:

例3.2:

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

} // 这里,s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,所以什么也不会发生。s1 离开作用域并被丢弃

fn gives_ownership() -> String { // gives_ownership 会将返回值移动给调用它的函数

let some_string = String::from("yours"); // some_string 进入作用域。
some_string // 返回some_string,并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}

4 引用(reference)

若要使用一个函数处理String,处理完后仍然需要使用这个值,可以使用引用的方式进行:

例4.1:

fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize { // s 是 String 的引用
s.len()
}// 在这里,s 离开了作用域。但因为它并不拥有引用值的所有权,所以什么也不会发生

这段代码中,&符号就是引用,允许使用值而不获取所有权。

Figure-4.1 引用示意图(图片来源Rust程序设计语言)

我们将创建一个引用的行为称为借用(borrowing)

引用分为可变引用于不可变引用,默认为不可变引用,创建可变引用只需要在&符号后加上mut即可:

例4.2:

fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
println!("{}", r1);
}

非常严肃的问题是,对于可变引用,需要遵守以下的规定:

  • 不可以同时对一个变量实现多个可变引用
  • 不可以同时对一个变量实现可变引用不可变引用
  • 允许同时对一个变量实现多个不可变引用

还有一个悬垂引用(Dangling References)的概念,也就是通过释放内存导致指向其的指针生成一个错误的悬垂指针(Dangling Pointers)

例4.3:

fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String {
let s = String::from("hello");

&s // 返回字符串 s 的引用
}// 由于所有权还在函数中,这里 s 离开作用域并被丢弃。其内存被释放,导致返回指针丢失。

Rust编译器将会检查悬垂引用的错误。

5 Slice类型引用

slice允许引用集合中一段连续的元素序列,而不用引用整个集合,它没有所有权。

fn main(){
let s = String::from("hello world");

let hello = &s[0..5];
// let hello = &s[..5];
let world = &s[6..11];
// let world = &s[6..];
}

&str类型实际上就是一个指向二进制程序特定位置的slice类型,这就是为什么&str是一个不可变引用