哈喽,大家好呀,我是呼噜噜,今天我们来聊聊Rust的所有权机制,它可以算是rust学习中最陡峭的部分了,本文将讲解手动内存管理,所有权的优势、规则,所有的转移、复制、克隆、借用与引用等概念

内存管理方式
在计算机编程的世界里,内存管理一直是一个非常棘手的问题,常见的有手动内存管理和垃圾回收
手动内存管理
以C/C++ 为代表的的,手动内存管理。它的优点是,开发者拥有对内存的最高控制权,经验丰富的高手能够实现极致的性能
但“能力越大,也意味着责任越大”,C/C++ 中将内存的生死大权交给了开发者。每一次手动申请 (malloc/new) 和释放 (free/delete) 内存的操作,都是对开发者心智的考验,稍有不慎,程序就会在最意想不到的时候崩溃
其中任何细微失误都有,在未来可能导致严重bug的风险,常见内存危险操作有
- 忘记释放内存,就会导致内存泄漏Memory Leak,导致程序长时间运行后耗尽系统资源
- 使用已经释放的内存,仍然有指针指向它。此时访问该指针会导致未定义行为,这就是悬垂指针Dangling Pointer
- 同一块内存被释放两次,这会导致二次释放Double Free,同样会导致程序崩溃或安全问题
- 多个线程同时访问+操作同一块内存,很有可能导致数据竞争Data Race问题
大家有没有过被这些问题,折磨得夜不能寐!

垃圾回收GC
后面为了将开发者从手动管理的苦海中解救出来,以Java为首的语言(还有C#, Python, Go 等),引入了垃圾回收(Garbage Collection, GC),开发者不再需要关心内存的释放
所谓的垃圾回收机制,就是请了一个"管家",在程序运行中,它会定期自动地在后台,扫描程序的内存,找出哪些内存(对象)已经不再被任何变量引用,然后自动回收它们。
GC能够极大地提高开发效率,保证内存安全。我们开发者不再需要关心内存释放的问题,但是随之而来的代价是
- 较高的性能开销,因为GC它本身就需要消耗CPU的资源去追踪和清理内存
- 不可预测的停顿STW,GC在程序执行中,可能需要暂停整个应用程序的运行,这对于游戏、实时系统等对延迟敏感的应用是不可接受的。
全新的道路:Rust所有权
在很长一段时间,我们开发者在内存管理上面临一个根本性取舍:是选择 C++ 的极致性能与控制力,并承担其风险;还是选择 GC 语言的安全与便捷,而接受性能与确定性的损耗。
对于这一两难境地,现代语言中翘楚,Rust给出了第三条路,开创性地提出了一套所有权Ownership系统,编译器会在编译时会根据一系列规则进行检查,从而在编译期就能解决了内存安全问题,无需运行时垃圾回收,同时获得了 C/C++ 的性能和 GC 语言的安全性
所有权核心规则
所有权有3条核心的规则,需要我们牢记:
- Rust 中的每个值都有一个变量,称为其
所有者owner - 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域(scope)时,这个值将被丢弃(drop),其占用的内存会自动被释放。
所有权的转移Move
对于前2个规则,我们来看一个例子,关于所有权的转移
fn main() {
// s1 是一个 String 类型的值。内存分配在堆上。
// s1 是这块内存的所有者。
let s1 = String::from("hello");
// 当我们将 s1 赋值给 s2 时,发生了“所有权转移”(Move)。
// 现在,s2 成为了 "hello" 这块内存的所有者。
let s2 = s1;
// 如果尝试使用 s1,编译器会直接报错!
// println!("s1 = {}", s1); // <-- 这行代码无法通过编译
// 错误信息: borrow of moved value: `s1`
// s2 拥有所有权,可以正常使用。
println!("s2 = {}", s2);
} // main 函数作用域结束s1 是一个 String 类型的值,String 是一个存储在堆Heap上的数据类型,所以内存分配在堆上,它的大小在编译时是未知的。
当将 s1 赋值给 s2 时,发生了所有权转移Move,s2 成为了 "hello" 这块内存的所有者,同时s1 在此之后立即失效,不能再被使用。在此之后,如果尝试使用 s1,编译器会直接报错!
我们可以推广一下,将值传递给函数,同样会发生所有权的转移或复制
栈上数据的复制 (Copy)
为了读者,能更深入理解所有权转移,我们再来看一个例子:
fn main() {
let x: i32 = 5;
let y: i32 = x;
println!("x = {}, y = {}", x, y);
}这段代码会报错吗?有人会说,报错了,它违反了所有权规则"值在任一时刻有且只有一个所有者"

但笔者可以明确地说,这段代码没有问题, i32 是一个基本类型,它的大小是固定的,存储在栈上
当把 x 赋值给 y 时,Rust 会直接在栈上复制一份数据,这里因为栈上数据的复制非常快,所以没有必要进行所有权转移。所以x 和 y 都是有效的,它们是两份独立的数据
对于一些完全存储在栈上、大小固定的简单类型,例如整数、浮点数、布尔值、字符等,这2类会在赋值时自动复制而不是转移所有权的类型,被称为实现了 Copy trait。
其实这里的区别,是和程序内存两个重要部分: 栈Stack与堆Heap有紧密联系的
- 栈区,它
存储大小已知、固定不变的数据,特点是“后进先出”,数据存取速度非常快,因为操作系统不需要寻找空间,总是在栈顶进行操作。函数调用时的参数、局部变量通常存储在这里。 - 堆区,
存储的是大小未知或可能变化的数据,访问堆上的数据比访问栈要慢,因为需要通过指针间接访问,内存容易碎片化,所以Rust的所有权系统主要为了更好地管理堆内存
如果有小伙伴堆,堆与栈及其区别,感兴趣的,可以去看看笔者之前的文章,内存中堆和栈,到底有什么区别?
变量作用域与自动释放
对于第3个规则,我们来看一个例子,来理解变量作用域与自动释放
fn main() {
// 这里是 main 函数的作用域的开始,变量 s 在此时还不存在
{
// 这是一个内部作用域的开始
let s: String = String::from("hello"); // s 在这里被声明,它是 "hello" 这个 String 值的所有者
// 从此刻起,s 是有效的
println!("s 的值是: {}", s); // 我们可以正常使用 s
} // 这个内部作用域到此结束。
// 因为 s 的所有者离开了作用域,Rust 会自动调用 drop 函数,
// 释放 s 所拥有的 String 内存。
// println!("s 的值是: {}", s); // 如果取消这行代码的注释,编译将会失败!
// 因为 s 已经不再有效,它在内部作用域结束时就被销毁了。
}
// main 函数作用域结束作用域是指一个变量在程序中有效的范围,在 Rust 中,一对花括号括起来的代码区,往往就是一个作用域,当变量s离开所在的作用域,其值会被销毁,这就能保证内存的及时释放,从根本上杜绝了内存泄漏
克隆Clone
如果我们确实需要,深拷贝堆上的数据,而不是移动所有权。在这种情况下,可以使用clone()。默认情况下,Rust不会自动创建数据的深拷贝,相较于所有权的移动,clone更耗资源
{
let s1 = String::from("hello");
let s2 = s1.clone(); // s2 是 s1 数据的一个完整副本
// 现在 s1 和 s2 都有效,它们各自拥有自己的数据
println!("s1 = {}, s2 = {}", s1, s2);
}借用Borrowing与引用References
有时候我们只想读取一下数据,并不想获得其所有权,该怎么办?每次都把所有权转移过去,然后再从函数返回,这样太麻烦了。
Rust还提供了一种"借用"值的方式,称为借用borrowing。让我们在不转移所有权的情况下可以"借用"数据。它的性能损耗比所有权转移还低,类似于C/C++的指针
借用允许我们创建一个指向值的引用Reference,而无需获取其所有权。借用又可分为:
- 不可变借用(Immutable Borrow):
&T。允许你读取数据,但不能修改 - 可变借用(Mutable Borrow):
&mut T。允许你修改数据
借用必须遵守以下的规则:
- 在同一作用域内,一个值可以有任意多个不可变借用。
- 但是只能有一个可变借用
- 不可变借用和可变借用不能同时存在。
我们来看一个例子:
let mut s = String::from("hello");
let r1 = &s; // ok,创建第一个不可变引用
let r2 = &s; // ok,可以有多个不可变引用
// let r3 = &mut s; // err!不能在有不可变引用的同时创建可变引用
// error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
println!("{} and {}", r1, r2); // 不可变引用在这里使用
// 在 r1 和 r2 的作用域结束后,就可以创建可变引用了
let r3 = &mut s;
println!("{}", r3);为啥借用要有这3个规则,就是为了让我们的代码在编译的时候,就避免了“一个指针正在读取数据,而另一个指针同时在写入数据”这种经典的数据竞争问题。

小结
从 C/C++ 的刀耕火种,到 GC 的全自动托管,再到 Rust 的编译期精细化管理,我们看到的是编程语言在追求性能、安全和效率这条路上不断的探索与进化。
Rust的所有权机制初可能有些复杂,可能比较严格。但它其实沉淀了C++多年的经验和教训,是经过深思熟虑的、优雅、且强大的设计
一旦掌握它,我们将迈向高性能、内存安全、无数据竞争、无GC的新世界


作者:小牛呼噜噜
本文到这里就结束啦,感谢阅读,关注同名公众号:小牛呼噜噜,防失联+获取更多技术干货