Mojo 学习 —— 值的生命周期
文章目录
介绍
前面介绍了如何使用 Mojo
的所有权模型来构建高性能的代码,而无需手动管理内存。
然而,Mojo
是为系统编程而设计的,而系统编程通常需要对自定义数据类型进行手动内存管理。
所以,Mojo
也提供了自定义内存管理机制。再次强调,Mojo
没有引用计数器和垃圾回收器。
标准库中的所有数据类型(如 Bool
、Int
和 String
)都是以结构体的形式实现的。所以你自定义的结构体和他们没什么区别。
当然你也可以使用 MLIR
方言提供的底层原语来重写这些类型。
Mojo
语言的强大之处在于,它不仅提供了系统编程的底层工具,同时又在一个框架内帮助你构建安全且易于使用的高层程序。
也就是说,你可以在底层编写所有你想要的 “不安全” 代码,只要你是按照 Mojo
的值语义编写的,那么实例化你的类型/对象的程序员就完全不需要考虑内存管理问题,而且由于值所有权的存在,其行为将是安全和可预测的。
总之,类型的提供者有责任通过实现特定的生命周期方法,如构造函数、复制构造函数、移动构造函数和析构函数,来管理每个值类型的内存和资源。
Mojo
默认不创建任何构造函数,但是会给未定义构造函数的类型添加一个简单的无操作析构函数。
接下来我们将详细介绍如何根据值语义定义这些生命周期方法
lifecycles 和 lifetimes
首先,让我们澄清一些术语:
- 生命周期(
lifecycles
):
值的生命周期是由结构体中的各种魔法方法定义的。每个生命周期事件都由不同的方法处理,例如构造函数(__init__
)、析构函数(__del__
)、复制构造函数(__copyinit__
)和移动构造函数(__moveinit__
)。所有声明为相同类型的值的生命周期相同。
- 生存期(
lifetimes
):
值的生存期是指在程序执行过程中,每个值被认为有效的时间跨度。值的生存期从初始化时开始,到销毁时结束,一般(但不总是)从 __init__
到 __del__
。没有两个值的生存期是完全相同的,因为每个值都是在不同的时间点创建和销毁的(即使两者之间的差异微乎其微)。
在 Mojo
中,一个值的生存期从变量初始化时开始,直到该值最后一次被使用,然后立即将其销毁。并使用 “as soon as possible(ASAP)” 销毁策略,在最后使用该值或对象的子表达式之后运行。
生存期管理的最后一块拼图是值的生命周期:每个值都需要实现关键的生命周期方法,这些方法定义了值是如何创建和销毁的。
值的生存期
在 Mojo
中,一个值的生存期从变量初始化时开始,一直持续到该值最后一次被使用,然后将其销毁。
Mojo
结构体默认是不包含生命周期方法的,所以你可以创建一个没有构造函数的结构体,但你无法实例化它,只能作为静态方法的命名空间。例如
struct NoInstances:
var state: Int
@staticmethod
fn print_hello():
print("Hello world!")
由于没有构造函数,它无法实例化,所以它没有生命周期。
而且 state
字段是没有用的,因为 Mojo
结构体不支持默认字段值,必须在构造函数中进行初始化。没有构造函数,所以无法初始化
但是你还是可以调用结构体中的静态方法:
NoInstances.print_hello()
# Hello world!
构造函数
要创建 Mojo
类型的实例,需要使用 __init__
构造函数方法。构造函数的主要职责是初始化所有字段,而且是必须初始化所有字段。例如
struct MyPet:
var name: String
var age: Int
fn __init__(inout self, name: String, age: Int):
self.name = name
self.age = age
现在可以创建一个实例:
var mine = MyPet("Loki", 4)
MyPet
的实例也可以借用和销毁,但目前还无法复制和移动。
不添加默认行为的好处是,会减少很多意外的情况,你不实现生命周期函数,意味着别人不能使用它的生命周期行为,一旦实现了复制和移动构造函数,就明确决定了如何复制或移动类型。
注意:
Mojo
可以不需要自定义析构函数来销毁对象。只要结构体中的所有字段都是可销毁的(标准库指针除外的所有类型都是可销毁的),那么Mojo
就知道如何在其生命周期结束时销毁该类型。
重载构造函数
像其他函数/方法一样,你可以重载 __init__
构造函数,用不同的参数初始化对象。
例如,你可能需要一个默认构造函数来执行参数的初始化,并且不需要任何参数,然后再使用其他构造函数来接受更多的参数。
例如,以下是重载构造函数的方法:
struct MyPet:
var name: String
var age: Int
fn __init__(inout self):
self.name = ""
self.age = 0
fn __init__(inout self, name: String):
self = MyPet()
self.name = name
请注意,每个构造函数都必须使用 inout
声明 self
参数,保证其是可修改的。
如果要从一个构造函数中调用另一个构造函数,直接像调用普通函数一样,无需使用 self
。
字段初始化
再强调一遍,每个构造函数结束时,所有字段都必须初始化,这是构造函数的唯一要求。
事实上,__init__
构造函数非常聪明,只要所有字段都已初始化,即使在构造函数结束之前,它也会将 self
对象视为已完全初始化。
只要完全初始化之后,self
对象就可以被传递使用。例如
fn use(arg: MyPet):
pass
struct MyPet:
var name: String
var age: Int
fn __init__(inout self, name: String, age: Int, cond: Bool):
self.name = name
if cond:
self.age = age
use(self)
self.age = age
use(self)
构造函数和隐式转换
Mojo
支持隐式的类型转换,当出现以下某种情况时,隐式转换就会发生:
- 将一种类型的值赋值给另一种类型的变量
- 将一种类型的值传递给一个需要不同类型值的函数
在这两种情况下,如果目标类型定义了一个构造函数,而该构造函数只需要一个源类型的非关键字参数,则支持隐式转换。例如
struct Source:
fn __init__(inout self): ...
struct Target:
fn __init__(inout self, s: Source): ...
fn main():
var a = Source()
var b: Target = a
上面的隐式转换相当于直接使用构造函数
var b = Target(a)
用于隐式转换的构造函数可以接受可选参数。下面的构造函数也支持从 Source
到 Target
的隐式转换:
struct Target:
fn __init__(inout self, s: Source, reverse: Bool = False): ...
如果一个类型没有声明自己的构造函数,而是使用 @value
装饰器,并且该类型只有一个字段,则也会发生隐式转换。
这是因为 Mojo
会自动为每个字段创建一个成员构造函数,当只有一个字段时,合成的构造函数就会像转换构造函数一样工作。例如:
@value
struct Target:
var s: Source
如果 Mojo
无法明确地将转换匹配到构造函数,隐式转换可能会失败。
例如,如果 Target
类型有两个不同类型的重载构造函数,而每个构造函数都支持从 Source
类型的隐式转换,那么编译器就有两条同样有效的路径来转换值:
struct A:
fn __init__(inout self, s: Source): ...
struct B:
fn __init__(inout self, s: Source): ...
struct Target:
fn __init__(inout self, a: A): ...
fn __init__(inout self, b: B): ...
var t = Target(Source())
在这种情况下,程序将会报错,无法确定使用哪个构造函数。只要移除 Target
中任何一个构造函数都能解决问题。
如果您想定义一个单参数构造函数,但又不想隐式转换类型,那么您可以定义一个只有关键字参数的构造函数(任何可变参数之后的都是关键字函数):
struct Target:
fn __init__(inout self, *, source: Source): ...
var t = Target(source=a)
今后,可能会提供一种更明确的方法来声明构造函数是否应支持隐式转换。
拷贝构造函数
当 Mojo
遇到赋值运算符 (=
) 时,它就会通过调用该类型的复制构造函数:__copyinit__
方法来复制右边的值。
因此,类型作者有责任实现 __copyinit__
方法,以便返回值的副本。
例如,上面的 MyPet
类型没有复制构造函数,所以这段代码无法编译:
var mine = MyPet("Loki", 4)
var yours = mine # This requires a copy, but MyPet has no copy constructor
为了让它正常工作,我们需要添加复制构造函数,就像这样:
struct MyPet:
var name: String
var age: Int
fn __init__(inout self, name: String, age: Int):
self.name = name
self.age = age
fn __copyinit__(inout self, existing: Self):
self.name = existing.name
self.age = existing.age
注意:
这里的
Self
(大写的S
)指代的是当前类型名称(本例中为MyPet
)的别名。此外,请注意__copyinit__
中的existing
参数是不可变的,因为fn
函数的默认参数约定是借用,我们也不应在这里修改被复制的值的内容。
现在,这段代码可以用来赋值:
var mine = MyPet("Loki", 4)
var yours = mine
与其他语言相比,Mojo
复制行为的不同之处在于 __copyinit__
旨在对类型中的所有字段进行深拷贝。也就是说,它会复制堆分配的值,而不仅仅是复制指针。
但 Mojo
编译器并不会强制执行这一点,因此类型作者有责任用值语义的方式实现 __copyinit__
。
例如,下面是一个新的 HeapArray
类型,它在复制构造函数中执行深拷贝:
struct HeapArray:
var data: Pointer[Int]
var size: Int
var cap: Int
fn __init__(inout self, size: Int, val: Int):
self.size = size
self.cap = size * 2
self.data = Pointer[Int].alloc(self.cap)
for i in range(self.size):
self.data.store(i, val)
fn __copyinit__(inout self, existing: Self):
self.size = existing.size
self.cap = existing.cap
self.data = Pointer[Int].alloc(self.cap)
for i in range(self.size):
self.data.store(i, existing.data.load(i))
fn __del__(owned self):
self.data.free()
fn append(inout self, val: Int):
if self.size < self.cap:
self.data.store(self.size, val)
self.size += 1
else:
print("Out of bounds")
fn dump(self):
print("[", end="")
for i in range(self.size):
if i > 0:
print(", ", end="")
print(self.data.load(i), end="")
print("]")
在 __copyinit__
我们并没有直接简单复制 Pointer
值(这样只会复制指针的值),而是重新开辟了一段内存地址,然后拷贝了所有堆分配的值(这就是深拷贝)。
因此,当我们复制 HeapArray
实例时,每个副本在堆上都有自己的值,因此一个值的变化不会影响另一个值。例如:
fn copies():
var a = HeapArray(2, 1)
var b = a # Calls the copy constructor
a.dump() # Prints [1, 1]
b.dump() # Prints [1, 1]
b.append(2) # Changes the copied data
b.dump() # Prints [1, 1, 2]
a.dump() # Prints [1, 1] (the original did not change)
注意:
当
HeapArray
的生命周期结束时,我们必须使用自定义的__del__
析构函数来释放堆分配的数据,其他字段会在生命周期结束时自动销毁它们。
如果你的类型不为堆分配的数据使用任何指针,对于大多数不显式管理内存的结构体,你只需在结构体定义中添加 @value
装饰器即可。
注意:
当一个值作为函数的
owned
参数,但该值的生命周期在此处还没有结束时,Mojo
会调用复制构造函数。如果值的生命周期该处结束了(通常用转移运算符^
表示),那么Mojo
会调用移动构造函数。
移动构造函数
当复制某些数据类型会严重影响性能时,我们可以将值作为引用共享。如果不再需要原始变量,则将原始变量作废,以避免出现双重释放或释放后使用的错误。
这通常被称为移动操作:存放数据的内存块保持不变(内存实际上没有移动),但指向该内存的指针移动到了一个新变量。
__moveinit__
方法执行消耗性移动:当原变量的生命周期结束时,它将值的所有权从一个变量转移到新变量。
注意:值的所有权转移并不需要移动构造函数。与
Rust
不同的是,所有权转移并不总是移动操作;移动构造函数只是Mojo
转移值所有权功能的一部分。
当移动发生时,Mojo
会立即使原始变量失效,阻止对它的任何访问,并禁用它的析构函数。使原始变量失效对于避免在堆分配的数据上出现内存错误非常重要。
下面为 HeapArray
结构添加移动构造函数:
fn __moveinit__(inout self, owned existing: Self):
print("move")
self.size = existing.size
self.data = existing.data
__moveinit__
的关键特征是它将输入的值视为 owned
,这意味着该方法获得了该值的唯一所有权。
Mojo
只有在执行移动(即所有权转移期间)时才会调用该方法,因此可以保证 existing
参数是对原始值的可变引用,而不是拷贝(与其他可能会将参数声明为 owned
的方法不同,如果调用该方法时没有使用 ^
转移操作符,则可能接收到的是值的拷贝)。
也就是说,只有当原始变量在转移所有权后结束了它的生存期,Mojo
才会调用这个移动构造函数。
下面的示例展示了如何调用 HeapArray
的移动构造函数:
fn moves():
var a = HeapArray(3, 1)
a.dump() # Prints [1, 1, 1]
var b = a^ # Prints "move"; the lifetime of `a` ends here
b.dump() # Prints [1, 1, 1]
#a.dump() # ERROR: use of uninitialized value 'a'
请注意,__moveinit__
执行的是对现有值的浅拷贝(只拷贝指针,而不是在堆上分配新的内存),这对那些具有堆分配值且拷贝代价高昂的类型很有用。
如果想让你的类型不会被复制,可以实现 __moveinit__
方法但不实现 __copyinit__
方法,使其成为 "只移动"类型。
只允许移动的类型可以传递给其他变量,也可以通过任何参数约定(borrowed
, inout
和 owned
)传递给函数。
唯一的问题是,在将只允许移动的类型赋值给新变量或作为 owned
参数传递时,必须使用 ^
转移操作符来结束它的生存期。
注意
对于没有堆分配字段的数据类型,移动构造函数并不能带来真正的好处。在栈上复制简单数据类型(如整数、浮点数和布尔型)的成本很低。
简单值类型
但大多数结构体都是其他类型的简单聚合,可以很容易地被复制和移动,而且我们也不想为这些简单的值类型编写大量的构造函数。
为了解决这个问题,Mojo
为我们提供了 @value
装饰器,自动注入构造函数,我们前面也反复提到过。
例如,考虑这样一个简单的结构:
@value
struct MyPet:
var name: String
var age: Int
Mojo
看到 @value
装饰器后,发你没有定义任何构造函数,它会自动为你添加上。例如:
struct MyPet:
var name: String
var age: Int
fn __init__(inout self, owned name: String, age: Int):
self.name = name^
self.age = age
fn __copyinit__(inout self, existing: Self):
self.name = existing.name
self.age = existing.age
fn __moveinit__(inout self, owned existing: Self):
self.name = existing.name^
self.age = existing.age
Mojo
只会生成那些没有定义的生命周期方法,因此您可以在使用 @value
的同时,定义自己的版本来覆盖默认行为。
例如,使用 @value
创建一个成员构造函数,然后添加重载以获取不同的参数值
@value
struct MyPet:
var name: String
var age: Int
fn __init__(inout self, owned name: String):
self.name = name^
self.age = 0
请注意,重载的构造函数并不会阻止 @value
装饰器创建构造函数。要覆盖默认行为,您需要添加一个与默认成员构造函数具有相同签名的构造函数。
在这段代码中有一个小优化,可以使用只移动的类型。像 Int
这样的简单类型也会以 owned
的形式传递,但由于所有权对整数没有任何意义,为了简单起见,我们可以省略该声明和转移操作符 (^
)。
在这种情况下,转移操作符也只是一种形式,因为即使不使用 self.name = name^
,Mojo
编译器也会注意到 name
在这里是最后使用的,并将此赋值转换为移动,而不是复制+删除。
注意:如果您的类型包含任何只移动的字段,
Mojo
就不会生成复制构造函数,因为它无法复制这些字段。此外,如果您的任何成员既不可复制也不可移动,@value
装饰器根本就不起作用。
trivial 类型
到目前为止,我们已经讨论过内存中的值,这意味着它们有一个身份(地址),可以在函数之间传递(通过引用)。
这对大多数类型来说都很好,对于需要花费昂贵代价进行复制操作的来说,这也是一种安全的默认方式。
但是,对于像单个整数或浮点数这样的微小对象来说,这种方式效率很低。我们称这些类型为 “trivial
”,因为它们只是 “比特包”,只需复制、移动和销毁,无需调用任何自定义生命周期方法。
trivial
类型指的就是我们身边最常见的简单类型,通常这些值非常小,应该在 CPU
寄存器中传递,而不是通过内存间接传递。
Mojo
提供了一个结构体装饰器来声明这些类型的值:@register_passable("trivial")
。这个装饰器告诉 Mojo
,该类型应该是可复制和可移动的,但它没有用户定义的逻辑(没有生命周期方法)来实现这一点。
它还告诉 Mojo
尽可能通过 CPU
寄存器来传递值,这对性能有明显的好处。比如标准库中的 Int
类型就有这个装饰器:
@register_passable("trivial")
struct Int:
var value: __mlir_type.index
fn __init__(value: __mlir_type.index) -> Int:
return Self {value: value}
...
在 Mojo
标准库类型中会广泛使用该装饰器,但在一般应用级代码中可以忽略它。
注意
这个装饰器应该在后面的版本中被重新设置。因为缺少自定义的复制/移动/销毁逻辑和寄存器中可传递性是两个没有交集的问题,应该分开。
值的销毁
一旦某个值/对象不再使用,Mojo
会立即将其销毁,并不会等到代码块结束,甚至不会等到表达式结束才销毁未使用的值。
它的尽快销毁策略在每个子表达式之后运行。即使在 a+b+c+d
这样的表达式中,一旦不再需要中间的值,Mojo
就会立即将其销毁。
Mojo
使用静态编译器分析来查找值的最后使用时间,然后就会立即结束该值的生命,并调用 __del__
析构函数对该类型进行必要的清理。
例如
@value
struct MyPet:
var name: String
var age: Int
fn __del__(owned self):
print("Destruct", self.name)
fn main():
var a = MyPet("Loki", 4)
var b = MyPet("Sylvie", 2)
print(a.name)
# a.__del__() runs here for "Loki"
a = MyPet("Charlie", 8)
# a.__del__() runs immediately because "Charlie" is never used
print(b.name)
# b.__del__() runs here
输出
Loki
Destruct Loki
Destruct Charlie
Sylvie
Destruct Sylvie
大多数结构体并不需要自定义的析构函数,如果没有定义析构函数,Mojo
会自动添加一个无操作的析构函数。
默认销毁行为
你可能想知道 Mojo
如何在没有自定义析构函数的情况下销毁一个类型,或者为什么无操作析构函数是有用的。
如果一个类型只是一个简单字段的集合,比如 MyPet
,那么 Mojo
只需要销毁字段即可,结构体中没有动态分配内存或使用任何长期资源(如文件句柄)。销毁时,无需采取任何特殊操作。
因为 MyPet
只包含一个 Int
和 String
类型,其中:
Int
是所谓的微小(trivial
)类型,Mojo
清楚地知道它有多大,这些比特可以重复使用来存储其他东西。
Mojo
中的 String
类型是可变的。字符串对象有一个内部缓冲区(即 List
字段),用于保存组成字符串的字符。List
将其内容存储在堆上动态分配的内存中,它也没有什么特殊的析构逻辑,就是在析构的时候它会调用 List
字段的析构函数来释放内存。
由于 String
和 Int
不需要任何自定义的析构逻辑,Mojo
会自动帮我们管理。
尽快销毁的好处
与其他语言类似,Mojo
遵循的原则是,对象/值在构造函数 (__init__
)中获取资源,在析构函数 (__del__
)中释放资源。
不过,Mojo
的尽快销毁与基于作用域的销毁(如 C++
的 RAII
模式,即等到代码作用域结束时才销毁值)相比有一些优势:
- 在最后一次使用后立即销毁数值与 “移动” 优化功能完美结合,后者将 “复制+删除” 操作转换为 “移动” 操作。
- 在
C++
中,由于析构函数调用发生在尾部调用之后,这可能会造成严重的性能和内存问题
此外 Python
的垃圾回收器比基于作用域的销毁策略更频繁地清理资源。
Mojo
的销毁策略与 Rust
和 Swift
更为相似,它们都具有强大的值所有权跟踪功能,并提供内存安全。
不同之处在于,Rust
和 Swift
需要使用 “drop flag
”(它们维护隐藏的影子变量,以跟踪值的状态,从而提供安全性)。但 Mojo
方法完全消除了这一开销,使生成的代码更快,并避免了歧义。
析构函数
通常,我们不需要自定义析构函数,只有在涉及动态分配内存(例如,通过 Pointer
或 DTypePointer
),或任何长期存在的资源(如文件句柄)时,需要手动释放。
例如,前面定义的 HeapArray
需要释放指针分配的内存:
struct HeapArray:
var data: Pointer[Int]
var size: Int
fn __init__(inout self, size: Int, val: Int):
self.size = size
self.data = Pointer[Int].alloc(self.size)
for i in range(self.size):
self.data.store(i, val)
fn __del__(owned self):
self.data.free()
只要释放指针执行的内存就行,不用释放 Int
类型的值。
如果指针指向的值也有析构函数,仅仅在指针上调用 free
是不够的。还应该确保 Mojo
通过它们的析构函数完全销毁所有分配的类型。
我们更新前面的 __del__
方法,以确保在数组元素上调用析构函数。
struct HeapArray:
var data: Pointer[Int]
var size: Int
fn __del__(owned self):
for i in range(self.size):
_ = __get_address_as_owned_value((self.data + i).address)
self.data.free()
它通过将每个存储的值依次赋值给_
实现这一点(给 _
变量赋值会告诉编译器这是该值的最后一次使用,可以立即销毁它)
注意:
你不能只是显式地调用析构函数,因为
__del__
的self
是owned
声明的,默认情况下会复制参数,所以foo._del__
实际上会创建和销毁foo
的副本。然而,当Mojo
销毁一个值时,它会将原始值作为self
传递,而不是一个副本。不要手动调用析构函数。
如果前面的例子中,使用 UnsafePointer
类型的实例,就可以使用 destroy_pointee
函数来代替丢弃模式。例如:
fn __del__(owned self):
for i in range(self.size):
destroy_pointee(self.data + i)
self.data.free()
__del__
方法是一个额外的清理事件,并不会覆盖任何默认的销毁行为。例如,即使你实现__del__
不做任何事情,Mojo
仍然会销毁 MyPet
中的所有字段:
字段的生存期
除了跟踪程序中所有对象的生存期外,Mojo
还会跟踪结构体中的每个字段。
也就是说,Mojo
跟踪整个对象是完全还是部分初始化或销毁,并使用其尽快销毁策略独立地销毁每个字段。
例如,下面这段代码改变了一个字段的值:
@value
struct MyPet:
var name: String
var age: Int
fn main():
var pet = MyPet("Po", 8)
print(pet.name)
# pet.name 的值不再使用,运行 pet.name.__del__() 销毁 name
pet.name = String("Lola") # 重新创建 pet.name
print(pet.name)
# 运行 pet.__del__() 销毁对象
name
字段在第一次 print
之后就被销毁,因为 Mojo
知道它将在下面将被覆盖。你也可以在使用转移操作符时看到类似的行为:
fn consume(owned arg: String):
pass
fn use(arg: MyPet):
print(arg.name)
fn consume_and_use():
var pet = MyPet("Selma", 5)
consume(pet.name^)
# 运行 pet.name.__moveinit__() ,然后 pet.name 被销毁了
# 现在 pet 是部分初始化
# use(pet) # 因为 pet 是部分初始化,所以无法进行传递
pet.name = String("Jasper") # 完全初始化
use(pet) # 可以传递
# 运行 pet.__del__() 并销毁对象
注意,代码中 name
字段将所有权转移给了 consume
。所以在此之后的一段时间内,name
字段未初始化的。
然后 name
在传递给 use
函数之前被重新初始化。如果在 name
被重新初始化之前尝试调用 use
, Mojo
会以报未初始化字段的错误
Mojo
在这里的策略很强大,字段可以临时转移,但是整个对象必须用必须保持完整性,同时被销毁
这意味着不可能通过只初始化对象的字段来创建对象,同样也不可能通过只销毁对象的字段来销毁对象。
移动和销毁时的字段生存期
对于调用移动构造函数和析构函数,有一个比较有意思的问题。回顾一下移动构造函数和析构函数的方法签名是这样的:
struct TwoStrings:
fn __moveinit__(inout self, owned existing: Self):
# 消耗 existing 来初始化 self
fn __del__(owned self):
# 销毁 self 的所有数据
注意:
大写的
Self
是当前类型名称的别名(用作现有参数的类型说明符),而小写的self
是对当前实例的隐式传递引用的参数名称。
这两个生命周期方法都将自己类型的实例作为 owned
传递,也就是说 __moveinit__
会隐式地销毁现有对象的子元素,以便将所有权转移到一个新实例;而 __del__
为其自身实现删除逻辑。
所以,它们都需要拥有和转移实例,并且它们绝对不希望值的原始拥有者的析构函数也运行。这可能导致双重释放错误,并且在 __del__
方法的情况下,它将成为一个无限循环。
为了解决这个问题,Mojo
对这两个方法进行了特殊处理,假设所有值在方法 return
之前都被销毁了。
这意味着可以像往常一样使用整个对象,直到转移字段值或方法返回为止。例如
fn consume(owned str: String):
print('Consumed', str)
struct TwoStrings:
var str1: String
var str2: String
fn __init__(inout self, one: String):
self.str1 = one
self.str2 = String("bar")
fn __moveinit__(inout self, owned existing: Self):
self.str1 = existing.str1
self.str2 = existing.str2
fn __del__(owned self):
self.dump() # Self 是完整的
# str2 不再使用,运行 self.str2.__del__()
consume(self.str1^)
# self.str1 的所有权转移了,也被释放了;
# `self.__del__()` 不会运行,避免无限循环
fn dump(inout self):
print('str1:', self.str1)
print('str2:', self.str2)
fn main():
var two_strings = TwoStrings("foo")
显式的生存期
在几乎所有情况下 Mojo
都会在最后一次使用后销毁值。但是在极少数情况下,Mojo
无法正确预测这一点,并且会销毁仍然被其他方式引用的值。
由于 Mojo 编译器无法对指针进行预测,所以当构建的结构体中,一个字段(ptr
)携带的是指向另一个字段(obj
)的指针。当字段 obj
在不再使用时,它可能会销毁该字段(ptr
),即使 ptr
仍然持有指向该字段。
因此,在析构函数或移动构造函数中执行某些特殊逻辑之前,可能需要保持 obj
的活动状态。可以将值赋值给 _
来强制 Mojo
将值保留到某一点。例如:
fn __del__(owned self):
self.dump() # Self 还是完整的
consume(self.ptr^)
_ = self.obj
# Mojo 将 `obj` 的值保留到此处
在这种情况下,如果 consume
以某种方式引用了 obj
中的某个值,这将确保 Mojo
直到调用 consume
之后才销毁 obj
,因为对丢弃变量 _
的赋值实际上是最后一次使用。
对于其他情况,还可以使用 Python
风格的 with
语句限定值的生存期。也就是说,对于在 with
语句入口处定义的任何值,Mojo
将保持该值有效,直到 with
语句结束。例如:
with open("my_file.txt", "r") as file:
print(file.read())
foo()
# `file` 保留到此处
# `file` 被销毁
bar()