编程语言各有各的“大能”,但如果谈到内存管理,Rust的话语权不是一般的高。GC(垃圾回收)?手动分配?对于掌握了Rust奥义的开发者而言,这些词汇简直弱爆了。众所周知,Rust编程语言的主要卖点之一是它的内存安全性。Rust对待内存,非常有自己的个性。与使用垃圾收集器的编程语言(如Haskell、Ruby和Python)不同,Rust为开发人员提供了快速功能,能够以一种独特的方式高效地使用和管理内存。Rust通过使用借用检查器(borrow checker)、所有权(ownership)、借用(borrow)这三个概念来管理和确保跨堆栈和堆的内存安全来管理内存,从而实现内存管理。本文讨论了Rust借用检查器,Rust与其他语言(如Go和C)的内存管理对比,以及Rust借用检查器的缺点。
内存是如何工作的
在讨论Rust如何管理内存之前,先来回顾一下计算机内存是如何工作的。分配给运行程序的计算机内存分为栈和堆。栈是一种线性数据结构,它按顺序存储局部变量,而不用担心内存的分配和重新分配。每个线程都有自己的栈,当线程停止运行时,每个栈都会被释放。数据以后进先出(LIFO)的模式存储——新的数据堆积在旧数据的上面。堆是一种分层数据结构,用于随机存储全局变量,内存分配和重新分配会是一个需要关注的问题。当一个字面量被压入堆栈时,是会有一个确定的内存位置的;这使得分配和重新分配(入栈和出栈)很容易。但是,在堆上分配内存的随机过程会导致使用内存的开销很大,这使得重新分配内存的速度变慢,因为在堆上分配内存时会涉及到复杂的引用记录。局部变量、函数和方法驻留在栈上,其他所有变量驻留在堆上;因为栈有固定的有限大小。Rust通过在堆栈中存储字面量(整数、布尔值等)来有效地处理内存。像结构体和枚举这些类型的变量在编译时由于没有固定的大小,存储在堆中。
所有权(所有权):“值”的主人
所有权是Rust中的一个概念,用来在没有垃圾收集器的情况下保证内存安全。Rust强制执行以下所有权规则:
- 每个值都有一个变量,称为owner(所有者)
- 每个值有且只有一个所有者
- 如果将变量赋值给新的所有者,那么原始值将被删除,否则它现在就会有两个所有者
在程序编译时,Rust编译器在程序编译之前会检查程序是否遵守了这些所有权规则。如果程序遵循所有权规则,则程序编译执行,否则编译失败。
Rust使用借用检查器(borrow checker)来验证所有权规则。借用检查器验证所有权模型以及内存(堆栈或堆)中的值是否超出范围(scope)。如果值超出范围,则释放内存。但这并不意味着访问值的唯一方法是通过原始所有者。这时就引出了”借用”的概念了。
借用(借用):重用有术
为了允许程序重用代码,Rust提供了借用的概念,和指针类似。
所有权可以暂时从所有者处借用,并在借用变量超出范围时归还。可以通过使用&(&)符号传递对所有者变量的引用来借用值。这在函数中非常有用。下面是一个例子:
1. fn list_vectors(vec: &Vec<i32>) {
2. for element in vec {
3. println!(“{}”, element);
4. }
5. }
函数也可以通过使用对变量的可变引用来修改借用变量。普通变量可以通过mut关键字将其设置为可变的,那么可变引用只要在&后添加关键字mut就可以了。当然在进行可变引用之前,变量本身必须是可变的。
1. fn add_element(vec: &mut Vec<i32>) -> &mut Vec<i32> {
2. vec.push(4);
3.
4. return vec
5. }
左右滑动查看完整代码所有权和借用的概念可能看起来没有那么灵活,除非你理解了复制,拷贝,移动的概念,以及它们如何一起工作。
复制所有权
复制通过复制位来复制值。复制仅适用于实现了Copy特征的类型。一些内置类型默认实现Copy特征。在栈中,很容易访问变量并更改所有权,而在堆中复制则不容易,因为位操作涉及位移动和位操作,而栈对于此类操作的组织更有条理。下面是一个在堆中复制值的示例。
1. fn main(){
2. let initial = 6;
3. let later = initial;
4. println!(“{}”, initial);
5. println!(“{}”, later);
6.
7. }
变量initial和later在同一作用域(范围scope)中声明,然后通过赋值将initial的值复制到later中。
虽然变量在相同的范围内,但initial将不再存在。这是在必须重新分配变量的情况下。输出:
试图打印initial变量的值将会引发编译错误,因为借用检查器注意到有变量的所有权转移了。
那如果你想保留这个值呢?Rust提供了克隆变量的能力。
拷贝变量
你可以将值分配给新所有者,同时使用拷贝的方法保留旧所有者中的值。然而,你所拷贝的类型必须提前实现拷贝特征。
1. fn main(){
2. let initial = String::from(“Showing Ownership “);
3. let later = initial.clone();
4. println!(“{} == {} [showing successful cloning] “, initial, later)
5. }
变量initial在变量later的声明中被拷贝,这两个变量驻留在堆中。如果这时被借用,则这两个变量将引用同一个对象;但是,在这种情况下,这两个变量是堆上的新声明,并占用独立的内存地址。
移动所有权
Rust提供了跨作用域更改变量所有权的功能。当函数按值接受参数时,函数中的变量会成为该值的新所有者。如果你不选择移动所有权,可以通过引用传递参数。下面是一个如何将变量的所有权从一个变量转移到另一个变量的示例。
1. fn change_owner(val: String) {
2.
3. println!(“{} was moved from its owner and can now be referenced as val”, val)
4. }
5.
6. fn main() {
7.
8. let value = String::from(“Change Ownership Example”);
9. change_owner(value);
10. }
change_owner函数获得了之前声明的字符串的所有权,并在接受value变量的值作为参数时获得该字符串的所有权。此时试图打印值变量会导致错误。