引言
当我学习新知识并希望将其“存储”在大脑中时,我需要能够解释它,或者至少用它创造一些具体的东西。在本文中,我将讨论内存及其管理方式,但不涉及人类记忆。我的目标是让大家更容易理解Rust 🦀,这一当下最热门的编程语言之一,是如何处理机器内存的。
Rust语言的内存管理
Rust作为一种编程语言,介于以下两者之间:
低级语言:需要手动管理内存(例如在C语言中使用 malloc
和free
)。高级语言:内存自动分配,并由垃圾回收机制(gc)定期清理。
“那么,如果我不需要手动管理内存,也没有人来处理它,那它是如何工作的呢?”这就引出了Rust的所有权机制。
数据类型与内存分配
在深入探讨所有权概念之前,让我们先看看Rust的一些数据类型,以及它们如何在内存中表示和分配。
greet
变量看似只是一个字符串,但在Rust中,字符串是带有一些额外保证、限制和功能的向量。向量或vec<T>
是动态大小的数据集合。count
变量没有使用任何构造函数或类型注解,因此它将被设计为i32
。i32
是一种固定大小的数据类型,具体来说是32位有符号整数。
这两个变量的动态和固定大小特性帮助我们理解它们在内存中的分配方式:动态的部分会在堆栈和堆内存部分分配,由(智能)指针链接。固定的部分只会在堆栈内存部分分配。
Rust的所有权机制
Rust的所有权是一组由Rust编译器验证的规则,确保内存的正确性和高效使用:
程序中的每个值都有一个所有者; 在任何时刻,程序中的每个值都有且只有一个所有者。
在上面的例子中,count
变量是值10的所有者。
“但是我们的程序不只是由两行代码组成;在代码执行的任何时刻,所有权如何保持唯一呢?”所有权可以被转移或借用。
值访问与所有权转移
考虑以下示例:
在这个场景中,我们处理两个i32
固定大小的变量。两者都将在堆栈中分配,没有涉及指针或引用到堆。first_counter
是数值10的所有者,但second_counter
变量的所有者是谁呢?是first_counter
吗?
不,second_counter
是另一个数值10的所有者。编译器没有处理任何动态大小变量,包括指针或引用:它只会在堆栈中复制数值10,赋予second_counter
所有权。first_counter
和second_counter
是不同的变量,因此println!
函数可以正确访问它们的值。
动态大小变量的情况
如果字符串的行为相同,我们将面临这样的情况:无法安全释放内存。为了解决这个问题,Rust应用了所有权转移。
hi
值的所有权从first_greet
转移到了second_greet
,因此:
无法访问 first_greet
;内存得到了高效且安全的管理。
就像《星际穿越》中重力跨越维度一样,所有权在变量、作用域和函数之间移动。在上面的例子中,hi
值的所有权从变量greet
转移到了print_len
函数。编译器不再允许访问greet
。
解决方法可以是:
将所有权返回给主函数; 使用引用借用 greet
值(称为借用)。
借用
后者在可读性和使用场景方面更好。一个简单的print_len
函数不应该仅仅为了取悦编译器而返回一个值。魔法得以实现是因为&
操作符。
你可以在print_len
函数签名中找到它,表示:
print_len
期望一个字符串的引用;使用 greet
字符串引用调用print_len
不会转移所有权,因此println!
对greet
值的访问被认为是安全的,程序将成功编译。
结语
现在是时候暂停我们的旅程了。在这一点上,可能会有很多问题产生。为了使本文可读且有趣,许多细节如可变性及其对所有权/借用的影响、解引用等被省略。
如果你对Rust语言有兴趣,建议查阅Rust官方书籍以深入了解这门编程语言。