(四)Rust所有权

本文最后更新于 2024年6月26日 下午

所有权(Ownership)是Rust最独特的特性,对其他语言也有着深刻的影响。这个特性在不使用垃圾收集器的情况下,保证内存安全性,因而理解所有权是如何工作至关重要。这篇文章将会讲解所有权,其相关特性,例如:借用(borrowing),切片(slices),内存排布(data layout in memory)将会在后续文章中介绍。

所有权(Ownership)

所有权是Rust程序控制管理内存的一系列规则。所有的程序都必须在运行期间管理使用的内存,一些语言使用垃圾收集器定期查找不再使用的内存并释放,在另一些语言中,编程人员必须显式的声明和释放内存。Rust使用了不同于上述的新方式,通过所有权系统的一系列需要被编译器检查的规则实现,如果违反了规则,程序将不会通过编译,因此所有权特性并不会削减程序运行时性能。

所有权概念对于很多编程人员来说是一个新概念,需要花费一些时间去熟悉。但是好消息是,一旦你熟悉了Rust和所有权系统,开发安全且高效的程序将会变得轻而易举。

栈和堆

许多编程语言并不要求编程人员经常考虑栈和堆。但在像 Rust 这样的系统编程语言中,值是在栈还是堆上对语言的行为有重要影响。这里简单介绍栈和堆,为后续介绍所有权特性提供背景知识。

栈和堆都是可供代码在运行时使用的内存,但它们的结构方式不同。堆栈按照获取值的顺序存储值,并按照相反的顺序删除值,简称后进先出。存储在栈上的所有数据都必须具有已知的固定大小,大小未知或大小可能更改的数据必须存储在堆上。

组织性 按照顺序存储,先入后出 由分配器分配,组织性差
存储速度 快(无需分配空间)
访问速度 快(栈顺序存储,有利于提高Cache命中率)

执行函数调用时,函数的参数值(可能包括指向堆上数据的指针)和函数的局部变量被压入堆栈,当函数结束时将会出栈。

栈上的内存在程序执行过程中自动管理,对于堆上的内存,所有权系统将会帮助跟踪,减少复制,清理。

所有权规则

  1. Rust中每一个值都有一个拥有者。
  2. 同一时刻,一个值只能有一个拥有者。
  3. 当拥有者超出作用域,值将会被释放。

变量作用域

下面用一个字符串例子,解释变量作用域的含义:

1
2
3
4
{						// s尚未被声明,无效
let s = "hello"; // 从此行开始,s有效
// 对s执行处理操作
} // 作用域结束,s被释放

字符串s被指向了一个硬编码的字符串,字符串从声明后一直有效直到作用域结束。

String类型

为了方便理解Rust的内存回收机制,需要一个更为复杂的类型。String类型是一个很好的例子,它的内存被分配在堆上,而且在各种语言中也是常见的类型。下面例子将通过from方法创建一个String

1
2
3
4
5
let mut s = String::from("hello");

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

println!("{s}"); // println!宏将会打印 `hello, world!`

内存和分配

1
2
3
4
{						// s尚未被声明,无效
let s = "hello"; // 从此行开始,s有效
// 对s执行处理操作
} // 作用域结束,s被释放

在上面例子中,当s离开作用域后,Rust会自动调用一个特殊函数drop,用来对内存释放。

在C++中,这种形式的资源释放形式叫做Resource Acquisition Is Initialization (RAII).

这种模式对Rust代码的编写产生了深远的影响,现在看起来可能很简单,但在一些复杂场景下,例如想要使用多个变量使用堆上的同一数据时,代码的行为会出乎意料。下面探讨其中一些情况。

移动对变量和数据影响

Rust中不同变量可以以不同方式影响相同数据。下面一个integer例子

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

将值5绑定至变量x,复制变量x并且绑定它到y。现在有两个变量xy,它们的值均为5。因为integer是拥有已知固定长度的基本类型,因此会有两个5被压入堆栈。

integer替换为String会发生什么?

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

这看起来非常相似,第二行对s1的值拷贝然后将它绑定给s2,但真实情况是这样吗?

下面这幅图能够帮助Stringd到底做了什么。首先,String由三部分组成,一个指向字符串内容的指针,字符串长度值和字符串容量值。这些数据将会被存储在栈上,然而字符串内容数据将会存储在堆上。

当将s1赋值给s2时,String的数据被拷贝了,意味着拷贝了指针,长度和容量,并且被储存在栈上,实际的字符串内容没有被拷贝,就像下图。

前面说到,当变量离开作用域后,Rust自动调用drop函数释放堆内存。但是上图中,s1s2均指向了相同的堆内存,当s2s1均离开作用域后,内存是否会被释放两次?

实际上,为了确保内存安全,在let s2=s1;之后,Rust认为s1不再有效。因此当s1离开作用域后,Rust无需释放内存。尝试在s2创建之后使用s1,这将无法通过编译。

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

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

其他语言有浅拷贝和深拷贝,在String例子中复制指针,长度和容量,不复制数据,看起来像是浅拷贝。但是Rust也使第一个变量失效,因此Rust中将其叫做移动,而不是浅拷贝。在String例子中,可以说s1被移动到了s2,就像下面图中展示的那样。

拷贝对变量和数据的影响

Rust出于性能考虑,对String默认只拷贝栈上数据,不拷贝堆数据,确实需要拷贝堆数据可以使用clone方法。

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

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

拷贝后s1s2和数据关系如下图所示

所有权和函数

将值传递给函数的机制与将值分配给变量时的机制类似。将变量传递给函数时会移动还是复制,将与前文中赋值规则一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let s = String::from("hello"); // s 进入作用域

takes_ownership(s); // 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 离开作用域,什么都不发生

返回值和作用域

返回值能够传递所有权,下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 离开作用域,执行drop
// s2 被移动,什么都不做
// s1 离开作用域,执行drop

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

let some_string = String::from("yours"); // some_string 进入作用域

some_string // some_string 被返回,并移动给外面调用函数
}

// 这个函数获取一个`String`并且返回一个
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

a_string // a_string 被返回,并移动给外面调用函数
}

当只想使函数获取值,而不转移所有权,可以尝试使用函数返回值将其所有权转移出来,也可以使用引用(后续会提到)。下面是使用元组将所有权转移出函数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let s1 = String::from("hello");

let (s2, len) = calculate_length(s1);

println!("The length of '{s2}' is {len}.");
}

fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回字符串的长度

(s, length)
}

参考

Rust docs


(四)Rust所有权
https://www.happyallday.cn/Rust/RustLang_04_Ownership/
作者
DevoutPrayer
发布于
2024年6月26日
更新于
2024年6月26日
许可协议