Chapter 08User Types Structs Enums Unions

用户定义类型

概述

Zig的用户定义类型是经过精心设计的小巧而锐利的工具。结构体在清晰的命名空间下组合数据和行为,枚举使用显式整数表示编码封闭的状态集合,而联合体则建模变体数据——带标签的用于安全性,不带标签的用于低级控制。这些共同构成了符合人体工程学的API和内存感知系统代码的骨干;参见#结构体#枚举#联合体作为参考。

本章构建实用的熟练度:结构体的方法和默认值,枚举与@intFromEnum/@enumFromInt的往返转换,以及带标签和不带标签的联合体。我们还将了解布局修饰符(packedextern)和匿名结构体/元组,这些在轻量级返回值和FFI中变得很方便。参见fmt.zigmath.zig获取相关助手。

学习目标

  • 定义和使用带有方法、默认值和清晰命名空间的结构体。
  • 安全地在枚举和整数之间进行转换,并对它们进行穷尽匹配。
  • 在带标签和不带标签的联合体之间进行选择;理解何时packed/extern布局很重要(参见#packed struct#extern struct)。

结构体:数据+命名空间

结构体收集字段和相关的辅助函数。方法只是带有显式接收器参数的函数——没有魔法,这使得调用点显而易见且可进行单元测试。默认值减少了常见情况的样板代码。

Zig
const std = @import("std");

// 第8章 — 结构体基础:字段、方法、默认值、命名空间
//
// 演示如何使用字段和方法定义结构体,包括
// 默认字段值。同时展示方法的命名空间与自由函数的区别。
//
// Usage: 
//    zig run struct_basics.zig

const Point = struct {
    x: i32,
    y: i32 = 0, // default value

    pub fn len(self: Point) f64 {
        const dx = @as(f64, @floatFromInt(self.x));
        const dy = @as(f64, @floatFromInt(self.y));
        return std.math.sqrt(dx * dx + dy * dy);
    }

    pub fn translate(self: *Point, dx: i32, dy: i32) void {
        self.x += dx;
        self.y += dy;
    }
};

// 命名空间:文件作用域的自由函数与方法
fn distanceFromOrigin(p: Point) f64 {
    return p.len();
}

pub fn main() !void {
    var p = Point{ .x = 3 }; // y uses default 0
    std.debug.print("p=({d},{d}) len={d:.3}\n", .{ p.x, p.y, p.len() });

    p.translate(-3, 4);
    std.debug.print("p=({d},{d}) len={d:.3}\n", .{ p.x, p.y, distanceFromOrigin(p) });
}
运行
Shell
$ zig run struct_basics.zig
输出
Shell
p=(3,0) len=3.000
p=(0,4) len=4.000

方法是命名空间函数;你可以根据可测试性和API清晰度自由混合自由函数和方法。

枚举:具有精确位表示的状态

枚举可以设置其整数表示(例如,enum(u8))并使用内置函数在整数之间进行转换。对枚举的switch必须是穷尽的,除非你包含else,这非常适合在编译时捕获新状态。

Zig
const std = @import("std");

// 第8章 — 枚举:整数表示、转换、穷举性检查
//
// 演示定义具有显式整数表示的枚举,
// 使用@intFromEnum和@enumFromInt在枚举和整数之间转换,
// 以及使用穷举性检查的模式匹配。
//
// 用法:
//    zig run enum_roundtrip.zig

const Mode = enum(u8) {
    Idle = 0,
    Busy = 1,
    Paused = 2,
};

fn describe(m: Mode) []const u8 {
    return switch (m) {
        .Idle => "idle",
        .Busy => "busy",
        .Paused => "paused",
    };
}

pub fn main() !void {
    const m: Mode = .Busy;
    const int_val: u8 = @intFromEnum(m);
    std.debug.print("m={s} int={d}\n", .{ describe(m), int_val });

    // 使用@enumFromInt往返;整数必须映射到声明的标签
    const m2: Mode = @enumFromInt(2);
    std.debug.print("m2={s} int={d}\n", .{ describe(m2), @intFromEnum(m2) });
}
运行
Shell
$ zig run enum_roundtrip.zig
输出
Shell
m=busy int=1
m2=paused int=2

@enumFromInt要求整数映射到已声明的标签。如果你期望未知值(例如,文件格式),请考虑哨兵标签、验证路径或具有显式错误处理的单独整数解析。

联合体:变体数据

带标签的联合体同时携带标签和有效载荷;模式匹配简单且类型安全。不带标签的联合体需要你手动管理活动字段,适用于低级位重新解释或FFI垫片。

Zig
const std = @import("std");

// 第8章 — 联合体:带标签与不带标签
//
// 演示带标签的联合体(使用枚举判别符)和不带标签的联合体
// (无判别符)。带标签的联合体是安全且符合语言习惯的;不带标签的
// 联合体是高级用法,若使用不当则不安全。
//
// 用法:
//    zig run union_demo.zig

const Kind = enum { number, text };

const Value = union(Kind) {
    number: i64,
    text: []const u8,
};

// 不带标签的联合体(高级):需要外部跟踪,若使用不当则不安全。
const Raw = union { u: u32, i: i32 };

pub fn main() !void {
    var v: Value = .{ .number = 42 };
    printValue("start: ", v);

    v = .{ .text = "hi" };
    printValue("update: ", v);

    // 不带标签的示例:以 u32 写入,以 i32 读取(位重新解释)。
    const r = Raw{ .u = 0xFFFF_FFFE }; // -2 as signed 32-bit
    const as_i: i32 = @bitCast(r.u);
    std.debug.print("raw u=0x{X:0>8} i={d}\n", .{ r.u, as_i });
}

fn printValue(prefix: []const u8, v: Value) void {
    switch (v) {
        .number => |n| std.debug.print("{s}number={d}\n", .{ prefix, n }),
        .text => |s| std.debug.print("{s}{s}\n", .{ prefix, s }),
    }
}
运行
Shell
$ zig run union_demo.zig
输出
Shell
start: number=42
update: hi
raw u=0xFFFFFFFE i=-2

在不重新解释位的情况下从不带标签的联合体中读取不同的字段(例如,通过@bitCast)是非法的;Zig在编译时阻止这种情况。除非你真正需要控制,否则优先使用带标签的联合体以确保安全。

带标签联合体的内存表示

理解带标签联合体在内存中的布局方式阐明了安全性与空间之间的权衡,并解释了何时选择带标签与不带标签的联合体:

graph TB subgraph "Tagged Union Definition" TAGGED["const Value = union(enum) {<br/> number: i32, // 4 bytes<br/> text: []const u8, // 16 bytes (ptr+len)<br/>}"] end subgraph "Tagged Union Memory (24 bytes on 64-bit)" TAG_MEM["Memory Layout:<br/><br/>| tag (u8) | padding | payload (16 bytes) |<br/><br/>Tag identifies active field<br/>Payload holds largest variant"] end subgraph "Untagged Union Definition" UNTAGGED["const Raw = union {<br/> number: i32,<br/> text: []const u8,<br/>}"] end subgraph "Untagged Union Memory (16 bytes)" UNTAG_MEM["Memory Layout:<br/><br/>| payload (16 bytes) |<br/><br/>No tag - you track active field<br/>Size = largest variant only"] end TAGGED --> TAG_MEM UNTAGGED --> UNTAG_MEM subgraph "Access Patterns" SAFE["Tagged: Safe Pattern Matching<br/>switch (value) {<br/> .number => |n| use(n),<br/> .text => |t| use(t),<br/>}"] UNSAFE["Untagged: Manual Tracking<br/>// You must know which field is active<br/>const n = raw.number; // Unsafe!"] end TAG_MEM --> SAFE UNTAG_MEM --> UNSAFE

内存布局细节:

带标签联合体: - 大小 = 标签大小 + 填充 + 最大变体大小 - 标签字段(通常是u8或适合标签数量的最小整数) - 用于有效载荷对齐的填充 - 有效载荷空间大小调整为容纳最大变体 - 示例:union(enum) { i32, []const u8 } = 1字节标签 + 7字节填充 + 16字节有效载荷 = 24字节

不带标签联合体: - 大小 = 最大变体大小(无标签开销) - 无运行时标签需要检查 - 你需要负责跟踪哪个字段是活动的 - 示例:union { i32, []const u8 } = 16字节(仅有效载荷)

何时使用每种:

  • 使用带标签联合体(默认选择):
  • 使用不带标签联合体(罕见,专家使用):

安全保证:

带标签联合体提供编译时穷尽性检查和运行时标签验证:

Zig
const val = Value{ .number = 42 };
switch (val) {
    .number => |n| print("{}", .{n}),  // OK - 匹配标签
    .text => |t| print("{s}", .{t}),  // 编译器确保两种情况都覆盖
}

不带标签联合体需要你手动维护安全不变量——编译器无法帮助你。

布局和匿名结构体/元组

当你必须精确地适应位(线格式)或匹配C ABI布局时,Zig提供了packedextern。匿名结构体(通常称为"元组")对于快速的多值返回很方便。

Zig
const std = @import("std");

// Chapter 8 — Layout (packed/extern) and anonymous structs/tuples
// 章节 8 — Layout (packed/extern) 和 anonymous structs/tuples

const Packed = packed struct {
    a: u3,
    b: u5,
};

const Extern = extern struct {
    a: u32,
    b: u8,
};

pub fn main() !void {
    // Packed bit-fields combine into a single byte.
    // Packed bit-fields combine into 一个 single byte.
    std.debug.print("packed.size={d}\n", .{@sizeOf(Packed)});

    // Extern layout matches the C ABI (padding may be inserted).
    // Extern layout matches C ABI (padding may be inserted).
    std.debug.print("extern.size={d} align={d}\n", .{ @sizeOf(Extern), @alignOf(Extern) });

    // Anonymous struct (tuple) literals and destructuring.
    // Anonymous struct (tuple) literals 和 destructuring.
    const pair = .{ "x", 42 };
    const name = @field(pair, "0");
    const value = @field(pair, "1");
    std.debug.print("pair[0]={s} pair[1]={d} via names: {s}/{d}\n", .{ @field(pair, "0"), @field(pair, "1"), name, value });
}
运行
Shell
$ zig run layout_and_anonymous.zig
输出
Shell
packed.size=1
extern.size=8 align=4
pair[0]=x pair[1]=42 via names: x/42

元组字段访问使用@field(val, "0")@field(val, "1")。它们是具有数字字段名的匿名结构体,这使它们保持简单且无需分配。

内存布局:默认 vs 打包 vs 外部

Zig提供了三种结构体布局策略,每种策略在内存效率、性能和兼容性方面都有不同的权衡:

graph TB subgraph "Default Layout (Optimized)" DEF_CODE["const Point = struct {<br/> x: u8, // 1 byte<br/> y: u32, // 4 bytes<br/> z: u8, // 1 byte<br/>};"] DEF_MEM["Memory: 12 bytes<br/><br/>| x | pad(3) | y(4) | z | pad(3) |<br/><br/>Compiler reorders & pads for efficiency"] end subgraph "Packed Layout (No Padding)" PACK_CODE["const Flags = packed struct {<br/> a: bool, // 1 bit<br/> b: u3, // 3 bits<br/> c: bool, // 1 bit<br/> d: u3, // 3 bits<br/>};"] PACK_MEM["Memory: 1 byte<br/><br/>| abcd(8 bits) |<br/><br/>No padding, bit-exact packing"] end subgraph "Extern Layout (C ABI)" EXT_CODE["const Data = extern struct {<br/> x: u8,<br/> y: u32,<br/> z: u8,<br/>};"] EXT_MEM["Memory: 12 bytes<br/><br/>| x | pad(3) | y(4) | z | pad(3) |<br/><br/>C ABI rules, field order preserved"] end DEF_CODE --> DEF_MEM PACK_CODE --> PACK_MEM EXT_CODE --> EXT_MEM subgraph "Key Differences" DIFF1["Default: Compiler can reorder fields<br/>Extern: Field order fixed<br/>Packed: Bit-level packing"] DIFF2["Default: Optimized alignment<br/>Extern: Platform ABI alignment<br/>Packed: No alignment (bitfields)"] end

布局模式比较:

布局大小/对齐字段顺序使用场景
Default由编译器优化可以重新排序普通Zig代码
Packed位精确,无填充固定,位级别线格式,位标志
ExternC ABI rulesFixed (declaration order)FFI, C interop

Detailed behavior:

Default Layout:

Zig
const Point = struct {
    x: u8,   // Compiler might reorder this
    y: u32,  // to minimize padding
    z: u8,
};
// Compiler chooses optimal order, typically:
// y (4 bytes, aligned) + x (1 byte) + z (1 byte) + padding

Packed Layout:

Zig
const Flags = packed struct {
    enabled: bool,    // bit 0
    mode: u3,         // bits 1-3
    priority: u4,     // bits 4-7
};
// Total: 8 bits = 1 byte, no padding
// Perfect for hardware registers and wire protocols

Extern Layout:

Zig
const CHeader = extern struct {
    version: u32,  // Matches C struct layout exactly
    flags: u16,    // Field order preserved
    padding: u16,  // Explicit padding if needed
};
// For calling C functions or reading C-written binary data

When to use each layout:

  • Default (no modifier):
  • Packed:
  • Extern:

Important notes:

  • Use @sizeOf(T) and @alignOf(T) to verify layout
  • Packed structs can be slower—measure before optimizing
  • Extern structs must match the C definition exactly (including padding)
  • Default layout may change between compiler versions (always safe, but field order not guaranteed)

注意事项

  • 方法是无糖的;考虑将辅助函数设为pub放在结构体内部以提高可发现性和测试作用域。
  • 枚举表示(enum(uN))定义大小并影响ABI/FFI——选择适合你协议的最小值。
  • 不带标签的联合体是锋利的工具。在大多数应用代码中,优先使用带标签的联合体和模式匹配。

练习

  • Point添加一个scale方法,该方法将两个坐标乘以f64,然后重写len以避免大整数的精度损失。
  • 使用新的Error状态扩展Mode,并观察编译器如何强制执行更新的switch
  • 创建一个表示JSON标量(nullboolnumberstring)的带标签联合体,并编写一个格式化每种情况的print函数。

替代方案与边缘情况

  • ABI布局:extern遵循平台ABI。使用@sizeOf/@alignOf验证大小,并在发布库时进行交叉编译。
  • 位打包:packed struct压缩字段但可能增加指令数量;在关键路径上提交之前进行测量。
  • 元组与命名结构体:对于稳定的API优先使用命名结构体;元组在本地、短期的胶水代码中表现出色。

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.