没有数据,程序就什么都不是,所有现代编程语言都将数据存储在两个地方之一:调用堆栈和堆(有时也存储在 CPU 寄存器中,但我们不会在这里讨论)。但是,每种语言读取和写入数据的方式略有不同——有时非常不同。因此,在以下部分中,我们将解释 Mojo 如何管理程序中的内存以及这会如何影响您编写 Mojo 代码的方式。
栈(Stack)和堆(Heap)概览
一般而言,所有编程语言使用调用堆栈的方式都相同:当调用一个函数时,编译器会在堆栈上分配一块内存,该内存的大小正好是存储执行逻辑和固定大小的 本地值所需的大小。当调用另一个函数时,其数据同样会添加到堆栈顶部。当一个函数完成时,堆栈中的所有数据都会被销毁,以便其他代码可以使用内存。
请注意,我们说过只有“固定大小的本地值”存储在堆栈中。动态大小的值可以在运行时改变大小,而是存储在堆中,堆是一块更大的内存区域,允许在运行时进行动态内存访问。从技术上讲,这种值的本地变量仍然存储在调用堆栈中,但其值是指向堆上实际值的固定大小指针。
此外,需要比函数的生命周期更长的值(例如在函数之间传递且不应复制的数组)存储在堆中,因为堆内存可以从调用堆栈中的任何位置访问,即使创建它的函数已从堆栈中删除。这种情况(堆分配的值被多个函数使用)是大多数内存错误发生的地方,也是不同编程语言的内存管理策略差异最大的地方。
内存管理策略
由于内存有限,因此程序必须尽快从堆中删除未使用的数据(“释放”内存)。确定何时释放该内存非常复杂。
一些编程语言试图利用“垃圾收集器”进程来隐藏内存管理的复杂性,该进程跟踪所有内存使用情况并定期释放未使用的堆内存(也称为自动内存管理)。这种方法的一个显著好处是它减轻了开发人员手动内存管理的负担,通常可以避免更多错误并提高开发人员的工作效率。然而,它会产生性能成本,因为垃圾收集器会中断程序的执行,并且可能无法很快回收内存。
其他语言要求您手动释放堆上分配的数据。如果操作正确,这将使程序执行得更快,因为垃圾收集器不会消耗任何处理时间。然而,这种方法的挑战在于程序员会犯错误,尤其是当程序的多个部分需要访问同一内存时——很难知道程序的哪个部分“拥有”数据并必须释放它。程序员可能会在程序完成之前意外释放数据(导致“释放后使用”错误),或者他们可能会释放两次数据(“双重释放”错误),或者他们可能永远不会释放数据(“泄漏内存”错误)。诸如此类的错误可能会给程序带来灾难性的后果,而且这些错误通常很难追踪,因此,从一开始就避免发生它们就显得尤为重要。
Mojo 使用第三种方法,称为“所有权”,该方法依赖于程序员在传递值时必须遵循的一组规则。这些规则确保每个内存块一次只有一个“所有者”,并且内存会相应地释放。通过这种方式,Mojo 会自动为您分配和释放堆内存,但它以一种确定性的方式进行,并且不会出现诸如释放后使用、双重释放和内存泄漏等错误。此外,它这样做的性能开销非常低。
Mojo 的价值所有权模型在编程效率和强大的内存安全性之间实现了完美平衡。它只需要您学习一些新语法和一些有关如何在程序中共享内存访问的规则。
但在我们解释 Mojo 的值所有权模型的规则和语法之前,您首先需要了解值语义。
值语义
Mojo 不强制执行值语义或引用语义。它支持这两种语义,并允许每种类型定义其创建、复制和移动的方式(如果有的话)。因此,如果您正在构建自己的类型,则可以实现它以支持值语义、引用语义或两者兼而有之。也就是说,Mojo 的设计参数行为默认为值语义,并且它为引用语义提供了严格的控制,以避免内存错误。
值所有权模型提供了对引用语义的控制,但在我们讨论其语法和规则之前,重要的是要了解值语义的原理。一般来说,这意味着每个变量对一个值都有唯一的访问权限,并且该变量范围之外的任何代码都不能修改其值。
值语义介绍
在最基本的情况下,共享值语义类型意味着您创建了该值的副本。这也称为“按值传递”。例如,请参考以下代码:
x = 1 y = x y += 1 print(x) print