Mojo 不强制使用值语义或引用语义。它同时支持这两种语义并允许每种类型定义它是如何创建、复制和移动的(如果有的话)。因此,如果你正在构建自己的类型,你可以实现支持值语义、引用语义或两者的一些组合。尽管如此, Mojo 的设计是默认使用值语义的参数行为,并提供了对引用语义的严格控制,以避免内存错误。

对于引用语义的控制是通过值所有权模型提供的,但在介绍其语法和规则之前,重要的是你理解值语义的原则。通常,这意味着每个变量对某个值具有唯一访问权限,任何超出该变量作用域的代码都无法修改其值。

值语义简介

在最基本的情况下,共享值语义类型意味着创建值的副本。这也被称为“按值传递”。例如,考虑以下代码:

  1. x = 1
  2. y = x
  3. y += 1
  4. print(x)
  5. print(y)
  1. 1
  2. 2

我们将x的值赋给y,这通过对x进行复制来创建y的值。当我们对y进行递增操作时,x的值不会改变。每个变量具有对值的独占所有权。

而如果一个类型使用引用语义,那么y将指向与x相同的值,对其中一个进行递增操作将影响两者的值。xy都不会“拥有”这个值,任何变量都可以引用它并对其进行修改。

下面是另一个带有函数的示例:

  1. def add_one(y: Int):
  2. y += 1
  3. print(y)
  4. x = 1
  5. add_one(x)
  6. print(x)
  1. 2
  2. 1

同样,y的值是一个副本,函数无法修改原始的x值。

如果你熟悉Python,到目前为止,这可能已经很熟悉了,因为上面的代码在Python中的行为相同。然而,Python并不具备值语义。这会变得复杂,但让我们考虑一种情况,你调用一个Python函数并传递一个指向堆分配值的指针的对象。Python实际上会给该函数一个对你的对象的引用,这允许函数对堆分配的值进行修改。如果你不小心,这可能会导致严重的错误,因为函数可能错误地假定它对该对象具有唯一所有权。

在 Mojo 中,所有函数参数的默认行为都是使用值语义。如果函数想要修改传入参数的值,那么它必须明确声明,从而避免意外的变更。

首先,传递给def函数的所有 Mojo 类型都是按值传递的,这保持了Python中预期的可变性行为。然而,与Python不同的是,函数对该值有真正的所有权,通常是因为它是一个副本。

例如,即使 Mojo 的Tensor类型在堆上分配值,当你将一个实例传递给def函数时,它会创建所有值的唯一副本。因此,如果我们在函数中修改该参数,原始值不会改变:

  1. def update_tensor(t: Tensor[DType.uint8]):
  2. t[1] = 3
  3. print(t)
  4. t = TensorDType.uint8
  5. t[0] = 1
  6. t[1] = 2
  7. update_tensor(t)
  8. print(t)
  1. Tensor([[1, 3]], dtype=uint8, shape=2)
  2. Tensor([[1, 2]], dtype=uint8, shape=2)

如果这是Python代码,函数将修改原始对象,因为Python共享对原始对象的引用。

deffn中的值语义

上述参数是可变的,因为默认情况下,def函数通过拥有其参数来获得所有权(通常是一个副本)。而fn函数则默认以不可变引用的方式接收参数。这是一种内存优化,以避免进行不必要的复制。

例如,让我们创建另一个使用fn声明的函数。在这种情况下,默认情况下,y参数是不可变的,因此如果函数想要修改本地作用域中的值,它需要进行一个本地副本:

  1. fn add_two(y: Int):
  2. # y += 2 # 这会导致编译错误,因为`y`是不可变的
  3. # 我们可以创建一个显式副本:
  4. var z = y
  5. z += 2
  6. print(z)
  7. x = 1
  8. add_two(x)
  9. print(x)
  1. 3
  2. 1

这一切都符合值语义,因为每个变量都维护着对其值的唯一所有权。

fn函数接收y值的方式是一种“看而不可修改”的值语义方法。这也是处理内存密集型参数时更节省内存的方法,因为 Mojo 只在我们明确进行复制时才进行复制。

因此,deffn参数的默认行为完全是值语义的:参数要么是副本,要么是不可变引用,并且来自调用方的任何活动变量都不会受到函数的影响。

但我们也必须允许引用语义(可变引用),因为这是我们构建高性能和节省内存的程序的方式(复制所有内容会变得非常昂贵)。挑战在于以一种不干扰值语义的可预测性和安全性的方式引入引用语义。

在 Mojo 中,我们做到的不是强制每个变量都具有“独占访问”某个值,而是确保每个值都有一个“独占所有者”,并在其所有者的生命周期结束时销毁该值。

在下一页关于值所有权的内容中,您将学习如何修改默认的参数约定,并安全地使用引用语义,以便每个值一次只有一个所有者。

Python风格的引用语义

如果您始终使用严格的类型声明,则可以跳过此部分,因为它仅适用于未带类型声明的使用def函数的 Mojo 代码(或以object声明的值)。

正如我们在本页开头所说, Mojo 不强制使用值语义或引用语义。每种类型的作者决定如何创建、复制和移动其类型的实例(参见值生命周期)。因此,为了与Python保持兼容性, Mojo 的object类型被设计为支持Python风格的函数参数传递,这与 Mojo 中的其他类型不同。

Python的参数传递约定称为“按对象引用传递”。这意味着当将变量传递给Python函数时,实际上是将一个对该对象的引用作为值传递(因此它不严格遵循引用语义)。

将对象引用“作为值”传递意味着参数名称只是一个容器,它 acts like an alias to the original object。如果在函数内部重新分配参数,它不会影响调用方的原始值。然而,如果修改对象本身(例如在列表上调用append()),这个更改在函数外部对原始对象可见。

例如,这是一个接收列表并对其进行修改的Python函数:

  1. %%python
  2. def modify_list(l):
  3. l.append(3)
  4. print("func:", l)
  5. ar = [1, 2]
  6. modify_list(ar)
  7. print("orig:", ar)
  1. func: [1, 2, 3]
  2. orig: [1, 2, 3]

在这个例子中,看起来列表是“按引用传递”的,因为l修改了原始值。

然而,如果Python函数将一个值分配给l,它不会影响原始值:

  1. %%python
  2. def change_list(l):
  3. l = [3, 4]
  4. print("func:", l)
  5. ar = [1, 2]
  6. change_list(ar)
  7. print("orig:", ar)
  1. func: [3, 4]
  2. orig: [1, 2]

这展示了Python中参数如何将对象引用“作为值”:函数可以修改原始值,但也可以将新对象分配给参数名称。

在 Mojo 中的按对象引用传递

尽管我们还没有完成实现object类型来表示任何 Mojo 类型,但我们的意图是这样做,并为def函数中的所有动态类型启用如上所述的“按对象引用传递”。

这意味着您可以通过简单地像Python一样编写 Mojo 代码来实现动态类型和“按对象引用传递”的行为:

  1. 使用def函数声明。
  2. 不声明参数类型。

TODO Mojo 尚未完全兼容Python,并且在支持Python的所有类型和行为之前还有很多工作要做。因此,这也是一个需要大量文档的主题。