理解 Zig 结构体
2025 年 5 月 3 日
和许多编程语言一样,Zig 也有结构体。结构体是一个非常简单直接的概念:它是一种用户定义的数据模型,可以包含多个字段或成员。换句话说,结构体是一种把相关数据以逻辑化、结构化方式组织在一起的方法。
传统面向对象语言,比如 C++、C# 和 Java,通常使用类来实现同样目标。我们都很熟悉那些老套类比:类表示某个通用概念,然后子类继承它并添加各自特定的行为和数据,或者通过接口实现多态。
Zig 和 C 一样没有类,但它有结构体、枚举和联合体。在 Zig 中,结构体是组织相关数据最常见的方式。
如何在 Zig 中定义结构体?
在 Zig 中,你可以使用 struct 关键字定义结构体,后面跟结构体名称,再用花括号 {} 包住结构体主体。在主体内部定义结构体的字段或成员。
下面是一个表示游戏角色的结构体示例:
const Character = struct {
name: []const u8,
health: u32,
stamina: u32,
say_hello: fn ([]const u8) void,
};
定义好 Character 结构体后,可以像下面这样创建它的实例:
const player = Character{
.name = "Ziggy Stardust",
.health = 100,
.stamina = 100,
.say_hello = fn(name: []const u8) void {
std.log.info("Hello, {s}!\n", .{name}),
},
};
// 调用 say_hello 函数
player.say_hello(player.name);
在这个例子中,我们创建了一个名为 player 的 Character 实例,并用 "Ziggy Stardust"、100、100 以及一个打印日志的函数来初始化结构体字段。
很简单,对吧?这和在 C 中定义结构体很相似。不过还有更多内容。
结构体字段可以有默认值
Zig 结构体中的默认值会在编译期执行,并允许你在创建结构体实例时省略对应字段。这相当于让结构体拥有类型安全的可选字段:如果没有提供值,就使用默认值。
例如,我们可以修改 Character 结构体,让 health 和 stamina 字段拥有默认值:
const Character = struct {
name: []const u8,
health: u32 = 100,
stamina: u32 = 100,
say_hello: fn ([]const u8) void,
};
这样,当我们创建新的 Character 实例时,就可以省略 health 和 stamina 字段。它们会被初始化为默认值,也就是这里的 100。
const player = Character{
.name = "Ziggy Stardust",
.say_hello = fn(name: []const u8) void {
std.log.info("Hello, {s}!\n", .{name}),
},
};
结构体可以被打包
默认情况下,Zig 中的结构体不会保证字段按你定义时的顺序排列。大多数时候这没问题,但有些情况下你可能希望字段拥有特定顺序,比如为了优化内存使用,或者与 OpenGL 这类要求结构体字段顺序固定的库交互。
这种情况下,可以在定义结构体时使用 packed 关键字,确保字段按照定义顺序排列。
此外,打包结构体的字段之间不会有填充字节。
打包结构体也可以参与位转换或指针转换,包括在编译期执行这些操作。
const Character = packed struct {
name: []const u8,
health: u32,
stamina: u32,
say_hello: fn ([]const u8) void,
};
这样结构体的字节顺序就会与字段定义顺序保持一致。就是这么简单。
结构体字段可以是 undefined
如果你还没准备好给 Zig 结构体中的某个字段赋值,可以使用 undefined 关键字把该字段设置为未定义状态。这在你想先创建结构体、再稍后设置部分字段时很有用。
const Goblin = Character{
.name = undefined,
.health = 100,
.stamina = 100,
.say_hello = fn(name: []const u8) void {
std.log.info("Hello, {s}!\n", .{name}),
},
}
在这个例子中,我们创建了一个 Character 实例,并把 name 字段设置为 undefined,把 health 和 stamina 设置为 100。这样就可以稍后在代码中再设置 name 字段,例如在角色进入玩家视野时为它生成一个随机名称。
结构体可以拥有方法或函数
你可能已经注意到,我们在 Character 结构体中把函数作为字段。和许多语言一样,Zig 允许在结构体字段中定义函数,并通过结构体实例调用。
Character 结构体中的 say_hello 字段是一个函数,它接收 []const u8 参数,并返回 void,也就是不返回任何值。
举例来说,我们还可以添加一个函数,通过传入另一个角色的引用来攻击它:
const Character = struct {
name: []const u8,
health: u32,
stamina: u32,
say_hello: fn ([]const u8) void,
attack: fn (target: *Character) void,
};
const player = Character{
.name = "Ziggy Stardust",
.health = 100,
.stamina = 100,
.say_hello = fn(name: []const u8) void {
std.log.info("Hello, {s}!\n", .{name}),
},
.attack = fn(target: *Character) void {
std.log.info("{s} attacks {s}!\n", .{player.name, target.name}),
},
};
const enemy = Character{
.name = "Goblin",
.health = 50,
.stamina = 50,
.say_hello = fn(name: []const u8) void {
std.log.info("Hello, {s}!\n", .{name}),
},
.attack = fn(target: *Character) void {
std.log.info("{s} attacks {s}!\n", .{enemy.name, target.name}),
},
};
player.attack(&enemy);
enemy.attack(&player);
在这个例子中,我们给 Character 结构体添加了一个 attack 函数。它接收另一个 Character 的指针作为参数,并向控制台输出一条日志。随后我们创建了两个 Character 实例:player 和 enemy,并让它们互相调用 attack 函数。
更理想的做法是,再给 Character 结构体添加一个 take_damage 函数,在角色受到攻击时减少它的生命值。不过这篇文章只用于教学。
函数可以返回结构体,从而形成泛型
和许多语言一样,在 Zig 中函数可以返回结构体。真正有趣的是,你可以利用这一点创建泛型。
fn GoblinHorde(comptime T: type) type {
return struct {
pub const Goblin = struct {
prev: ?*Goblin,
next: ?*Goblin,
data: T,
}
first: ?*Goblin,
last: ?*Goblin,
len: usize,
}
}
总结
结构体是 Zig 的基础构建块,用于把相关数据以逻辑化、结构化方式组织在一起。结构体可以拥有默认值,可以被打包,可以包含未定义字段,可以包含方法,也可以由函数返回并用于构建泛型。结构体是一种强大且灵活的数据建模方式,在 Zig 标准库和许多第三方库中都被大量使用。