(四)Rust所有权
本文最后更新于 2024年6月26日 下午
所有权(Ownership)是Rust最独特的特性,对其他语言也有着深刻的影响。这个特性在不使用垃圾收集器的情况下,保证内存安全性,因而理解所有权是如何工作至关重要。这篇文章将会讲解所有权,其相关特性,例如:借用(borrowing),切片(slices),内存排布(data layout in memory)将会在后续文章中介绍。
所有权(Ownership)
所有权是Rust程序控制管理内存的一系列规则。所有的程序都必须在运行期间管理使用的内存,一些语言使用垃圾收集器定期查找不再使用的内存并释放,在另一些语言中,编程人员必须显式的声明和释放内存。Rust使用了不同于上述的新方式,通过所有权系统的一系列需要被编译器检查的规则实现,如果违反了规则,程序将不会通过编译,因此所有权特性并不会削减程序运行时性能。
所有权概念对于很多编程人员来说是一个新概念,需要花费一些时间去熟悉。但是好消息是,一旦你熟悉了Rust和所有权系统,开发安全且高效的程序将会变得轻而易举。
栈和堆
许多编程语言并不要求编程人员经常考虑栈和堆。但在像 Rust 这样的系统编程语言中,值是在栈还是堆上对语言的行为有重要影响。这里简单介绍栈和堆,为后续介绍所有权特性提供背景知识。
栈和堆都是可供代码在运行时使用的内存,但它们的结构方式不同。堆栈按照获取值的顺序存储值,并按照相反的顺序删除值,简称后进先出。存储在栈上的所有数据都必须具有已知的固定大小,大小未知或大小可能更改的数据必须存储在堆上。
栈 | 堆 | |
---|---|---|
组织性 | 按照顺序存储,先入后出 | 由分配器分配,组织性差 |
存储速度 | 快(无需分配空间) | 慢 |
访问速度 | 快(栈顺序存储,有利于提高Cache命中率) | 慢 |
执行函数调用时,函数的参数值(可能包括指向堆上数据的指针)和函数的局部变量被压入堆栈,当函数结束时将会出栈。
栈上的内存在程序执行过程中自动管理,对于堆上的内存,所有权系统将会帮助跟踪,减少复制,清理。
所有权规则
- Rust中每一个值都有一个拥有者。
- 同一时刻,一个值只能有一个拥有者。
- 当拥有者超出作用域,值将会被释放。
变量作用域
下面用一个字符串例子,解释变量作用域的含义:
1 |
|
字符串s
被指向了一个硬编码的字符串,字符串从声明后一直有效直到作用域结束。
String类型
为了方便理解Rust的内存回收机制,需要一个更为复杂的类型。String
类型是一个很好的例子,它的内存被分配在堆上,而且在各种语言中也是常见的类型。下面例子将通过from
方法创建一个String
。
1 |
|
内存和分配
1 |
|
在上面例子中,当s
离开作用域后,Rust会自动调用一个特殊函数drop
,用来对内存释放。
在C++中,这种形式的资源释放形式叫做Resource Acquisition Is Initialization (RAII).
这种模式对Rust代码的编写产生了深远的影响,现在看起来可能很简单,但在一些复杂场景下,例如想要使用多个变量使用堆上的同一数据时,代码的行为会出乎意料。下面探讨其中一些情况。
移动对变量和数据影响
Rust中不同变量可以以不同方式影响相同数据。下面一个integer
例子
1 |
|
将值5
绑定至变量x
,复制变量x
并且绑定它到y
。现在有两个变量x
和y
,它们的值均为5
。因为integer
是拥有已知固定长度的基本类型,因此会有两个5
被压入堆栈。
将integer
替换为String
会发生什么?
1 |
|
这看起来非常相似,第二行对s1
的值拷贝然后将它绑定给s2
,但真实情况是这样吗?
下面这幅图能够帮助String
d到底做了什么。首先,String
由三部分组成,一个指向字符串内容的指针,字符串长度值和字符串容量值。这些数据将会被存储在栈上,然而字符串内容数据将会存储在堆上。
当将s1
赋值给s2
时,String
的数据被拷贝了,意味着拷贝了指针,长度和容量,并且被储存在栈上,实际的字符串内容没有被拷贝,就像下图。
前面说到,当变量离开作用域后,Rust自动调用drop
函数释放堆内存。但是上图中,s1
和s2
均指向了相同的堆内存,当s2
和s1
均离开作用域后,内存是否会被释放两次?
实际上,为了确保内存安全,在let s2=s1;
之后,Rust认为s1
不再有效。因此当s1
离开作用域后,Rust无需释放内存。尝试在s2
创建之后使用s1
,这将无法通过编译。
1 |
|
其他语言有浅拷贝和深拷贝,在String
例子中复制指针,长度和容量,不复制数据,看起来像是浅拷贝。但是Rust也使第一个变量失效,因此Rust中将其叫做移动,而不是浅拷贝。在String
例子中,可以说s1
被移动到了s2
,就像下面图中展示的那样。
拷贝对变量和数据的影响
Rust出于性能考虑,对String
默认只拷贝栈上数据,不拷贝堆数据,确实需要拷贝堆数据可以使用clone
方法。
1 |
|
拷贝后s1
,s2
和数据关系如下图所示
所有权和函数
将值传递给函数的机制与将值分配给变量时的机制类似。将变量传递给函数时会移动还是复制,将与前文中赋值规则一致。
1 |
|
返回值和作用域
返回值能够传递所有权,下面是一个例子:
1 |
|
当只想使函数获取值,而不转移所有权,可以尝试使用函数返回值将其所有权转移出来,也可以使用引用(后续会提到)。下面是使用元组将所有权转移出函数的例子:
1 |
|