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");

// Chapter 8 — Struct basics: fields, methods, defaults, namespacing
//
// Demonstrates defining a struct with fields and methods, including
// default field values. Also shows namespacing of methods vs free functions.
//
// Usage:
//    zig run struct_basics.zig
// 第8章 - 结构体基础:字段、方法、默认值、命名空间
//
// 演示定义具有字段和方法的结构体,包括
// 默认字段值。还将展示方法与自由函数的命名空间区别。
//
// 用法:
//    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;
    }
};

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

pub fn main() !void {
    var p = Point{ .x = 3 }; // y uses default 0
    // y使用默认值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");

// Chapter 8 — Enums: integer repr, conversions, exhaustiveness
//
// Demonstrates defining an enum with explicit integer representation,
// converting between enum and integer using @intFromEnum and @enumFromInt,
// and pattern matching with exhaustiveness checking.
//
// Usage:
//    zig run enum_roundtrip.zig
// 第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 });

    // Round-trip using @enumFromInt; the integer must map to a declared tag.
    // 使用@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");

// Chapter 8 — Unions: tagged and untagged
//
// Demonstrates a tagged union (with enum discriminant) and an untagged union
// (without discriminant). Tagged unions are safe and idiomatic; untagged
// unions are advanced and unsafe if used incorrectly.
//
// Usage:
//    zig run union_demo.zig
// 第8章 - 联合体:带标签和无标签
//
// 演示带标签的联合体(带枚举判别式)和无标签联合体
// (不带判别式)。带标签的联合体是安全且惯用的;无标签
// 联合体是高级特性,如果使用不当则不安全。
//
// 用法:
//    zig run union_demo.zig

const Kind = enum { number, text };

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

// Untagged union (advanced): requires external tracking and is unsafe if used wrong.
    // 无标签联合体(高级):需要外部跟踪,如果使用不当则不安全。
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);

    // Untagged example: write as u32, read as i32 (bit reinterpret).
    // 无标签示例:作为u32写入,作为i32读取(位重新解释)。
    const r = Raw{ .u = 0xFFFF_FFFE }; // -2 as signed 32-bit
    // 作为带符号32位整数的-2
    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["const Value = union(enum) {<br/> number: i32, // 4 字节<br/> text: []const u8, // 16 字节 (ptr+len)<br/>}"] end subgraph "带标签联合体内存 (64位系统上为24字节)" TAG_MEM["内存布局:<br/><br/>| tag (u8) | padding | payload (16 字节) |<br/><br/>标签标识活动字段<br/>有效载荷容纳最大的变体"] end subgraph "不带标签联合体定义" UNTAGGED["const Raw = union {<br/> number: i32,<br/> text: []const u8,<br/>}"] end subgraph "不带标签联合体内存 (16字节)" UNTAG_MEM["内存布局:<br/><br/>| payload (16 字节) |<br/><br/>无标签 - 你来跟踪活动字段<br/>大小 = 仅最大变体的大小"] end TAGGED --> TAG_MEM UNTAGGED --> UNTAG_MEM subgraph "访问模式" SAFE["带标签:安全模式匹配<br/>switch (value) {<br/> .number => |n| use(n),<br/> .text => |t| use(t),<br/>}"] UNSAFE["不带标签:手动跟踪<br/>// 你必须知道哪个字段是活动的<br/>const n = raw.number; // 不安全!"] 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 - matches tag
    .text => |t| print("{s}", .{t}),  // Compiler ensures both cases covered
}

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

布局和匿名结构体/元组

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

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

// Chapter 8 — Layout (packed/extern) and anonymous structs/tuples
// 第8章 - 布局(packed/extern)和匿名结构体/元组

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.
    // 压缩位域合并为单个字节。
    std.debug.print("packed.size={d}\n", .{@sizeOf(Packed)});

    // Extern layout matches the C ABI (padding may be inserted).
    // Extern布局匹配C ABI(可能会插入填充)。
    std.debug.print("extern.size={d} align={d}\n", .{ @sizeOf(Extern), @alignOf(Extern) });

    // Anonymous struct (tuple) literals and 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 "默认布局(优化)" DEF_CODE["const Point = struct {<br/> x: u8, // 1 字节<br/> y: u32, // 4 字节<br/> z: u8, // 1 字节<br/>};"] DEF_MEM["内存:12 字节<br/><br/>| x | pad(3) | y(4) | z | pad(3) |<br/><br/>编译器为效率重新排序和填充"] end subgraph "紧凑布局(无填充)" PACK_CODE["const Flags = packed struct {<br/> a: bool, // 1 位<br/> b: u3, // 3 位<br/> c: bool, // 1 位<br/> d: u3, // 3 位<br/>};"] PACK_MEM["内存:1 字节<br/><br/>| abcd(8 位) |<br/><br/>无填充,位精确打包"] end subgraph "外部布局(C ABI)" EXT_CODE["const Data = extern struct {<br/> x: u8,<br/> y: u32,<br/> z: u8,<br/>};"] EXT_MEM["内存:12 字节<br/><br/>| x | pad(3) | y(4) | z | pad(3) |<br/><br/>C ABI规则,字段顺序保留"] end DEF_CODE --> DEF_MEM PACK_CODE --> PACK_MEM EXT_CODE --> EXT_MEM subgraph "关键差异" DIFF1["默认:编译器可以重新排序字段<br/>外部:字段顺序固定<br/>紧凑:位级打包"] DIFF2["默认:优化对齐<br/>外部:平台ABI对齐<br/>紧凑:无对齐(位域)"] end

布局模式比较:

布局大小/对齐字段顺序用例
默认由编译器优化可以重新排序普通Zig代码
紧凑位精确,无填充固定,位级线路格式,位标志
外部C ABI规则固定(声明顺序)FFI,C互操作

详细行为:

默认布局:

Zig
const Point = struct {
    x: u8,   // 编译器可能会重新排序这个
    y: u32,  // 以最小化填充
    z: u8,
};
// 编译器选择最佳顺序,通常是:
// y (4字节,对齐) + x (1字节) + z (1字节) + 填充

紧凑布局:

Zig
const Flags = packed struct {
    enabled: bool,    // 位 0
    mode: u3,         // 位 1-3
    priority: u4,     // 位 4-7
};
// 总共:8位 = 1字节,无填充
// 非常适合硬件寄存器和线路协议

外部布局:

Zig
const CHeader = extern struct {
    version: u32,  // 精确匹配C结构体布局
    flags: u16,    // 字段顺序保留
    padding: u16,  // 如果需要,显式填充
};
// 用于调用C函数或读取C编写的二进制数据

何时使用每种布局:

  • 默认(无修饰符):
  • 紧凑
  • 外部

重要说明:

  • 使用@sizeOf(T)@alignOf(T)来验证布局
  • 紧凑结构体可能会变慢——在优化前进行测量
  • 外部结构体必须精确匹配C定义(包括填充)
  • 默认布局可能会在不同编译器版本之间更改(总是安全的,但字段顺序不保证)

注意与警告

  • 方法是无糖的;考虑在结构体内部将助手设为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.