Mojo Structs 代码示例与测试全解:从 Struct 定义到 Bazel 构建验证
【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo
本篇技术指南以 Mojo 仓库中 Mojo/docs/site/code/manual/structs/index/README.md 为线索,围绕其配套的.mojo示例源码、BUILD.bazel测试配置,以及它所服务的 Mojo Manual 之 Structs 章节 展开。你将掌握:如何用struct定义自带行为的类型、如何通过@fieldwise_init与Copyable/ImplicitlyCopyabletrait 控制值的复制与移动、如何用mut self编写可变方法、如何用@staticmethod定义静态方法,以及如何使用 Bazel 一键构建并运行这些示例与测试。
一、该目录是什么:代码示例与测试的组织方式
Mojo/docs/site/code/manual/structs/index/是 Mojo Manual 中 Structs(index.mdx) 章节的配套代码目录。其 README.md 明确说明了该目录的定位:
- 目录内每个
.mojo文件都是一个可独立运行的 Mojo 应用程序(standalone Mojo application); - 目录内的 BUILD.bazel 负责定义构建与测试目标。
目录实际包含以下文件:
| 文件 | 作用 |
|---|---|
my_pair.mojo | 演示用@fieldwise_init定义 struct、调用实例方法、以及值的复制(copy)与移动(move) |
mutable_self_in_function.mojo | 演示用mut self编写可变方法并修改字段 |
static_method.mojo | 演示@staticmethod静态方法的定义与两种调用方式 |
object_initialization.mojo | 演示@fieldwise_init初始化与字段的移动、重新初始化 |
tests.mojo | 聚合型测试入口,覆盖构造、初始化器列表、可变方法与静态方法 |
BUILD.bazel | Bazel 构建定义,为每个.mojo生成二进制与运行测试目标 |
此外,同目录还存在operator-support/与reference/两个子目录,分别对应 Manual 中运算符支持(operator-support.mdx)与参考(reference.mdx)两个章节的示例代码,形成一套完整的"文档—示例—测试"三位一体结构。
二、BUILD.bazel 解读:如何把示例变成可运行测试
BUILD.bazel 是整个目录的构建核心,逻辑非常简洁:
load("//bazel:api.bzl", "modular_run_binary_test", "mojo_binary") package(default_visibility = ["//oss/modular/docs:__subpackages__"]) MOJO_SRCS = glob(["*.mojo"]) [ mojo_binary( name = src.split(".")[0], srcs = [src], deps = [ "@mojo//:std", ], ) for src in MOJO_SRCS ] [ modular_run_binary_test( name = src.split(".")[0] + "_test", size = "small", binary = src.split(".")[0], ) for src in MOJO_SRCS ]这段配置揭示了两个关键设计:
- 自动展开:
glob(["*.mojo"])会捕获目录下所有.mojo源码,随后通过列表推导式为每个文件生成两个目标——以文件名(去扩展名)命名的mojo_binary目标,以及带_test后缀的modular_run_binary_test测试目标。也就是说,新增一个.mojo示例文件后无需手工修改 BUILD 文件,构建系统会自动为其生成对应的二进制与测试。 - 标准库依赖:所有示例统一依赖
@mojo//:std,即 Mojo 标准库,这保证了示例可以放心使用print、String、Int等标准能力。
在实际使用中,可以在仓库根目录(使用仓库自带的./bazelw封装脚本)按如下方式构建与运行:
# 构建某个示例的二进制 ./bazelw build //Mojo/docs/site/code/manual/structs/index:my_pair # 直接运行该示例 ./bazelw run //Mojo/docs/site/code/manual/structs/index:my_pair # 运行某个示例对应的运行测试 ./bazelw test //Mojo/docs/site/code/manual/structs/index:my_pair_test由于modular_run_binary_test的语义是"运行该二进制",因此tests.mojo这类自带断言并raises的测试文件,也会通过"运行即验证"的方式被纳入 Bazel 测试体系。
三、Struct 是什么:数据与行为的静态封装
在深入示例之前,先建立对 Mojo struct 的完整认知。依据 Mojo Manual 的 Structs 章节,struct 是 Mojo 中定义自定义类型的首要方式:它把数据(字段)与操作这些数据的逻辑(方法)捆绑在一起。一个 struct 可以定义以下四类成员:
- 字段(Fields):存储与 struct 相关的数据,必须用
var声明并带类型注解; - 方法(Methods):定义在 struct 内部、通常作用于字段数据的函数;
- 静态方法(Static methods):由类型提供的函数,用于执行行为、提供常量或创建特化实例,无需实例即可调用;
- 双下划线方法(Dunder methods):形如
__init__的预定义特殊方法,用于定义初始化、运算符重载、生命周期等行为,是 struct 遵循 trait 的桥梁; comptime成员:编译期引用,用于优化。
struct 是静态的:它在编译期被完全确定,运行时不可增删字段或方法。正是这种静态性,让 Mojo 能计算出 struct 的精确内存布局、确保字段使用前已初始化、生成直接的字段访问代码,从而实现高性能与内存安全。
Manual 还特别指出,Mojo 的标准类型(如Int、String)本身就是用 struct 实现的,而非语言内建特例——这意味着你定义的自定义类型与标准库类型拥有完全同等的表达能力。
四、从零定义 Struct:init与 @fieldwise_init
Manual 给出的最基本 struct 定义如下:
struct MyPair: var first: Int var second: Int但这个 struct 暂时无法实例化,因为它没有初始化器。加上__init__后:
struct MyPair: var first: Int var second: Int def __init__(out self, first: Int, second: Int): self.first = first self.second = second这里有两个值得注意的语言机制:
self是第一个参数:所有 struct 方法都以self(名称只是约定,可换用其他名字)作为第一个参数引用当前实例。调用初始化器时你从不传self,Mojo 会自动传入。out self是参数约定:out声明self是一个"开始时未初始化、函数返回前必须完成初始化"的可变引用。这是 Mojo 值语义(value semantics)的重要组成部分,详见 Mojo/docs/site/manual/values/ 下的所有权(ownership)相关章节。
为了省去逐字段赋值样板代码,Mojo 提供了@fieldwise_init装饰器,它可以按字段自动生成 field-wise 初始化器:
@fieldwise_init struct MyPair: var first: Int var second: Int这正是示例文件 my_pair.mojo 所采用的形式。Manual 同时强调一个约束:不能在声明字段时赋初值,必须在初始化器中初始化全部字段,否则代码无法编译。
有了初始化器后,即可构造实例并访问字段:
var mine = MyPair(2, 4) print(mine.first) # 输出 2Mojo 还支持初始化器列表(initializer lists):当类型可从上下文推断时,可以直接用花括号传参,例如{first = 3, second = 4}等价于调用对应类型的__init__(first=3, second=4)。这一点在 tests.mojo 中有直接验证:
var pair2: MyPair = {first = 3, second = 4} assert_true(pair2.first == 3) assert_true(pair2.second == 4) test_initializer_list({first = 5, second = 6}) # 类型由函数参数推断五、示例源码逐文件精讲
5.1 my_pair.mojo:复制、移动与实例方法
my_pair.mojo 是一个完整的、可独立运行的演示程序。它在@fieldwise_init之外还声明了ImplicitlyCopyabletrait,并定义了一个普通的实例方法:
@fieldwise_init struct MyPair(ImplicitlyCopyable): var first: Int var second: Int def get_sum(self) -> Int: return self.first + self.second def main(): var a = MyPair(1, 2) # copyable, movable var original_pair = MyPair(2, 6) var copied_pair = original_pair # copy var moved_pair = original_pair^ # move # methods var mine = MyPair(6, 8) print(mine.get_sum()) # Suppress compiler warnings _ = a^ _ = copied_pair^ _ = moved_pair^这段代码演示了三件事:
- 复制(copy):
var copied_pair = original_pair通过隐式复制初始化器创建副本; - 移动(move):
var moved_pair = original_pair^使用^后缀运算符把值移动出去; - 实例方法:
mine.get_sum()通过self读取字段并计算求和,输出14。
注意末尾用_ = a^之类的写法"消费"未使用的变量,以抑制编译器对未使用值的警告——这也是 Mojo 所有权模型的一个体现:值可以被显式 move 并释放。
5.2 mutable_self_in_function.mojo:用 mut self 修改字段
默认情况下,struct 方法的self是不可变的,无法对字段赋值。示例 mutable_self_in_function.mojo 展示了正确姿势:
struct MyStruct: var value: Int def increment(mut self): self.value += 1 # Works. Mutable `self` allows assignment # But without `mut`: # ERROR: expression must be mutable in assignment def __init__(out self, value: Int): self.value = value def main(): var my_struct = MyStruct(1) my_struct.increment() print(my_struct.value) # 输出 2把接收者声明为mut self后,方法内对字段的修改会持久化到实例上。注释中还保留了一条"反例":若去掉mut,self.value += 1会触发编译错误expression must be mutable in assignment。关于mut参数约定的更多细节,可参见 Mojo/docs/site/manual/values/ 下的"Mutable arguments (mut)"小节。
5.3 static_method.mojo:不依赖实例的静态方法
静态方法不需要创建实例即可调用,且不接收隐式的self参数,因此无法访问实例字段。定义方式是用@staticmethod装饰器并省略self:
struct Logger: def __init__(out self): pass @staticmethod def log_info(message: String): print("Info: ", message) def main(): Logger.log_info("Static method called.") var l = Logger() l.log_info("Static method called from instance.")输出为:
Info: Static method called. Info: Static method called from instance.可以看到,静态方法既可以通过类型名直接调用(Logger.log_info(...)),也可以通过某个实例调用(l.log_info(...)),两种形式等价。
5.4 object_initialization.mojo:字段的移动与再初始化
object_initialization.mojo 展示了在一个mut self方法内移动字段的值并重新初始化:
@fieldwise_init struct MyStruct: var name: String def move_field(mut self, var new_name: String): var name = self.name^ # Moves field; self.name is now uninitialized print("Name:", name) # Prints: "Name: Ken" self.name = new_name^ # reinitialize the field def main(): var instance = MyStruct("Ken") instance.move_field("Scott") print("Name:", instance.name) # Prints: "Name: Scott"要点在于:self.name^把字段值移动出来,此时self.name处于未初始化状态;随后必须用新值重新初始化该字段,实例才能保持有效。这体现了 Mojo 严格的生命周期管理——任何时刻都不允许出现悬空或半初始化状态。
5.5 tests.mojo:把示例固化为断言测试
tests.mojo 是该目录的"验证中枢",它从std.testing导入assert_equal与assert_true,将前述各语言特性固化为可自动验证的断言:
from std.testing import assert_equal, assert_true @fieldwise_init struct MyPair: var first: Int var second: Int def aux_test_local(self, value: Int) raises: assert_true(self.first == 1) assert_true(value == 2) var first = value assert_true(first == value) # 局部变量遮蔽字段 assert_true(self.first != value) def test_initializer_list(pair: MyPair) raises: assert_true(pair.first == 5) assert_true(pair.second == 6) def test_mypair_construction() raises: var pair = MyPair(1, 2) assert_true(pair.first == 1) assert_true(pair.second == 2) pair.aux_test_local(2) var pair2: MyPair = {first = 3, second = 4} assert_true(pair2.first == 3) assert_true(pair2.second == 4) test_initializer_list({first = 5, second = 6}) @fieldwise_init struct MyStruct: var value: Int def increment(mut self): self.value += 1 # Works: Mutable `self` allows assignment def test_mutable_self() raises: var s = MyStruct(10) s.increment() assert_true(s.value == 11) struct Logger: def __init__(out self): pass @staticmethod def log_info(message: String) -> String: return "Info: " + message def test_static_method() raises: assert_equal(Logger.log_info("Hello"), "Info: Hello") def main() raises: test_mypair_construction() test_mutable_self() test_static_method()这份测试代码额外揭示了一个字段约定:可以复用字段名作为方法参数或局部变量名(如aux_test_local内的var first = value),局部变量会遮蔽字段,需要通过self.first显式访问字段。所有测试函数均标注raises,断言失败即抛出异常并导致main以非零状态退出,从而被modular_run_binary_test判定为失败。
六、让 Struct 可复制:Copyable 与 ImplicitlyCopyable
默认情况下,Mojo struct可以移动(movable)但不可复制(not copyable)。Manual 给出了直观的错误演示:
var a = MyPair(1, 2) var b = a # 错误:MyPair 未遵循 ImplicitlyCopyable,无法隐式复制 var c = a.copy() # 错误:MyPair 没有 copy 方法,未遵循 Copyable var d = a^ # 正确:移动是允许的要获得复制能力,有两条路径:
6.1 Copyable:显式复制
struct MyPair(Copyable): ...大多数情况下只需添加该 trait,Mojo 会自动生成复制初始化器(形如__init__(out self, *, copy: Self)),无需手写。只有当你的 struct 需要自定义复制逻辑(例如内部动态分配了内存、需要深拷贝)时,才需要自行实现。Copyable提供两种复制方式:copy()实例方法与复制初始化器,Manual 建议优先使用copy()方法。
6.2 ImplicitlyCopyable:隐式复制
struct MyPair(ImplicitlyCopyable): ...ImplicitlyCopyable自动蕴含Copyable与Movable,允许在赋值等场景下自动发生复制。Manual 给出了重要提醒:只有复制开销低且无副作用时才应使用隐式复制,因为不必要的复制会显著消耗内存与性能。
示例文件 my_pair.mojo 正是选择了ImplicitlyCopyable,因此var copied_pair = original_pair才能编译通过。而 tests.mojo 中的MyPair同样遵循ImplicitlyCopyable,保证了测试中大量按值传参的合法性。
七、字段的硬性要求与命名约定
Manual 对字段提出了四条必须遵守的规则,违反即编译失败:
- 必须用
var声明:value: Int会报错(缺少var),var count: Int才是合法写法; - 符号必须唯一:字段、方法与
comptime成员同处一个命名空间,重复声明(如两个count)属于非法重声明; - 修改字段的方法必须用
mut self; - 字段必须在初始化器中初始化,而不是在声明处赋初值:
var foo: Int = 10会报 "Unknown tokens" 错误;但comptime成员是编译期常量、不占实例存储,因此可以在声明处初始化。
此外,Manual 还给出了一套与标准库代码风格一致的命名约定:
- 字段名使用小写 snake_case(如
user_count、max_capacity); - 使用描述用途而非类型的名字(如
error_msg,而非msg_string); - 仅供内部使用或维持不变量的成员加下划线前缀(如
_private_field); - 布尔字段用
is_或has_前缀(如is_valid、has_data); - 除常见的数学约定(如坐标
x、y、z)外,避免单字母命名。
八、方法与特殊方法(Dunder Methods)
除了用户自定义的实例方法与静态方法,Mojo 还预定义了一批特殊方法(special methods,又称 dunder methods),如__init__、__deinit__等。它们的命名带双下划线,Mojo 会在合适的时机自动调用——例如创建实例时调用__init__,销毁实例时调用__deinit__。看起来像内建的运算符(+、<、==、|等)本质上也是特殊方法实现的运算符重载。
特殊方法大致分为两类:
- 运算符重载:如
<、+、|,使自定义类型能参与运算符表达式,详见 operator-support.mdx; - 生命周期事件处理:如
__init__、__deinit__,以及复制、移动等值语义行为,详见 Mojo/docs/site/manual/lifecycle/。
对于绝大多数只是"其他类型的简单聚合"的 struct,无需手工编写全部生命周期方法——用@fieldwise_init装饰器配合Copyable/Movabletrait 即可自动合成所需的关键生命周期方法。
九、Struct 与 Class 的对比:静态 vs 动态
如果你熟悉面向对象语言,自然会拿 struct 与 class 对比。Manual 明确指出,Mojo 未来会支持与 Python 类行为一致的 class,但目前 struct 是自定义类型的首选。两者关键差异如下:
| 维度 | Python 类(动态) | Mojo struct(静态) |
|---|---|---|
| 绑定时机 | 运行时动态分发,可运行时绑定实例字段 | 编译期绑定,运行时不能新增方法或字段 |
| 修改能力 | 支持 monkey-patching / swizzling | 不支持运行时修改 |
| 继承 | 支持继承 | 不支持子类化,但可实现 trait |
| 类属性 | 支持被所有实例共享的类属性 | 不支持静态数据成员 |
| 字段声明 | 无需显式声明 | 必须用var显式声明全部字段 |
尽管牺牲了动态灵活性,静态 struct 换来了可预测的内存布局与编译期优化——程序在编译时就知道 struct 的信息在哪里、如何使用,运行时无需额外步骤。同时,struct 与 Python 生态中熟悉的特性(如运算符重载)配合良好,且所有标准库类型都由 struct 构建,意味着"没有特殊待遇",你写的类型与标准类型平起平坐。
十、实践建议与小结
综合 README、示例源码与 Manual 章节,使用 struct 的推荐路径是:
- 定义:用
struct声明字段(必须var+ 类型注解),用@fieldwise_init快速生成初始化器; - 行为:需要读字段的写普通实例方法(
self),需要改字段的写mut self方法,无需实例的写@staticmethod; - 值语义:默认可移动;需要复制时按"复制开销低且无副作用"的原则选择
ImplicitlyCopyable或Copyable; - 验证:把关键行为写入带
assert_*断言的tests.mojo,并通过BUILD.bazel自动生成的mojo_binary与modular_run_binary_test目标纳入 Bazel 测试流水线。
通过 Mojo/docs/site/code/manual/structs/index/ 下的这套示例,你可以在本地一键构建、运行并测试每一个 struct 语言特性;再结合 index.mdx、operator-support.mdx 与 reference.mdx 三份文档,即可完整掌握 Mojo 自定义类型的核心语法与最佳实践。
【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考