Hey!
Billgo

EN

理解 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);

在这个例子中,我们创建了一个名为 playerCharacter 实例,并用 "Ziggy Stardust"100100 以及一个打印日志的函数来初始化结构体字段。

很简单,对吧?这和在 C 中定义结构体很相似。不过还有更多内容。

结构体字段可以有默认值

Zig 结构体中的默认值会在编译期执行,并允许你在创建结构体实例时省略对应字段。这相当于让结构体拥有类型安全的可选字段:如果没有提供值,就使用默认值。

例如,我们可以修改 Character 结构体,让 healthstamina 字段拥有默认值:

const Character = struct {
    name: []const u8,
    health: u32 = 100,
    stamina: u32 = 100,
    say_hello: fn ([]const u8) void,
};

这样,当我们创建新的 Character 实例时,就可以省略 healthstamina 字段。它们会被初始化为默认值,也就是这里的 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,把 healthstamina 设置为 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 实例:playerenemy,并让它们互相调用 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 标准库和许多第三方库中都被大量使用。