Chapter 05

结构体、枚举与联合类型

掌握 Zig 的复合数据类型,以及底层内存布局控制

结构体(struct)

基本定义与初始化

const Point = struct {
    x: f32,
    y: f32,
};

// 初始化:所有字段都必须指定(或有默认值)
const p1 = Point{ .x = 1.0, .y = 2.0 };
const p2: Point = .{ .x = 3.0, .y = 4.0 };  // 类型推断

// 带默认值的字段
const Config = struct {
    host: []const u8 = "localhost",
    port: u16 = 8080,
    debug: bool = false,
};

const cfg = Config{};  // 使用所有默认值
const custom = Config{ .port = 3000, .debug = true };
_ = p1; _ = p2; _ = cfg; _ = custom;

方法与 Self

const Vec2 = struct {
    x: f32,
    y: f32,

    // 方法:第一个参数通常是 self 或 *Self
    pub fn length(self: Vec2) f32 {
        return std.math.sqrt(self.x * self.x + self.y * self.y);
    }

    pub fn scale(self: *Vec2, factor: f32) void {
        self.x *= factor;
        self.y *= factor;
    }

    pub fn add(a: Vec2, b: Vec2) Vec2 {
        return .{ .x = a.x + b.x, .y = a.y + b.y };
    }

    // 静态方法(无 self 参数)
    pub fn zero() Vec2 {
        return .{ .x = 0, .y = 0 };
    }
};

var v = Vec2{ .x = 3.0, .y = 4.0 };
std.debug.print("length: {d}\n", .{v.length()});  // 5.0
v.scale(2.0);
std.debug.print("scaled: ({d}, {d})\n", .{ v.x, v.y });

元组结构体

// 没有字段名的结构体(元组)
const Pair = struct { i32, f64 };
const pair = Pair{ 42, 3.14 };
// 通过索引访问字段
std.debug.print("first: {d}, second: {d}\n", .{ pair[0], pair[1] });

枚举(enum)

基本枚举

const Direction = enum { north, south, east, west };

const dir = Direction.north;
const same = .north;  // 类型推断

// 枚举的整数值
const Status = enum(u8) {
    ok = 200,
    not_found = 404,
    error_ = 500,
};

// 枚举 → 整数
const code: u8 = @intFromEnum(Status.ok);  // 200
// 整数 → 枚举(可能失败)
const status = try std.meta.intToEnum(Status, 404);  // Status.not_found
_ = dir; _ = same; _ = code; _ = status;

// 枚举方法
const Color = enum {
    red, green, blue,

    pub fn hex(self: Color) u24 {
        return switch (self) {
            .red   => 0xFF0000,
            .green => 0x00FF00,
            .blue  => 0x0000FF,
        };
    }
};
std.debug.print("red hex: #{x:0>6}\n", .{Color.red.hex()});

联合类型(union)

普通 union(C 风格)

// 普通 union:字段共享内存,不追踪当前活跃字段(不安全)
const Num = union {
    int: i64,
    float: f64,
    uint: u64,
};

var n = Num{ .int = 42 };
// 访问错误字段会导致未定义行为(debug 模式会 panic)
std.debug.print("int: {d}\n", .{n.int});

tagged union:安全的联合类型

// union(enum):带标签的联合,运行时追踪当前活跃字段
const Value = union(enum) {
    int: i64,
    float: f64,
    string: []const u8,
    boolean: bool,
    nil,  // void 变体,不携带数据
};

fn printValue(v: Value) void {
    switch (v) {
        .int    => |i| std.debug.print("int: {d}\n", .{i}),
        .float  => |f| std.debug.print("float: {d}\n", .{f}),
        .string => |s| std.debug.print("string: {s}\n", .{s}),
        .boolean=> |b| std.debug.print("bool: {}\n", .{b}),
        .nil    =>     std.debug.print("nil\n", .{}),
    }
}

const v1 = Value{ .int = 42 };
const v2 = Value{ .string = "hello" };
const v3 = Value.nil;
printValue(v1);
printValue(v2);
printValue(v3);

// 修改 union(必须整体替换活跃字段)
var val = Value{ .int = 10 };
val = .{ .float = 3.14 };  // 切换到 float 字段

extern struct 与 packed struct

extern struct:C ABI 兼容布局

// extern struct:字段按 C ABI 规则对齐,用于与 C 代码交互
const CPoint = extern struct {
    x: c_float,
    y: c_float,
};

// 可以直接传递给 C 函数
extern fn c_draw_point(p: CPoint) void;
// c_draw_point(.{ .x = 1.0, .y = 2.0 });

// 确认内存布局(@sizeOf 和 @offsetOf)
std.debug.print("size: {d}, x offset: {d}\n",
    .{ @sizeOf(CPoint), @offsetOf(CPoint, "x") });

packed struct:位字段控制

// packed struct:字段紧密排列,无填充字节,支持位字段
const Flags = packed struct(u8) {  // 整体是 1 字节
    read:    u1,   // 1 位
    write:   u1,   // 1 位
    execute: u1,   // 1 位
    _pad:    u5,   // 5 位填充(对齐到字节)
};

const rwx = Flags{ .read = 1, .write = 1, .execute = 1, ._pad = 0 };
std.debug.print("flags size: {d} bytes\n", .{@sizeOf(Flags)});  // 1
std.debug.print("read={d} write={d} execute={d}\n",
    .{ rwx.read, rwx.write, rwx.execute });

// packed struct 转整数(按位解释)
const byte: u8 = @bitCast(rwx);
std.debug.print("as byte: 0b{b:0>8}\n", .{byte});
packed struct 的限制

packed struct 中的字段必须是整数、布尔值、其他 packed struct/union 或枚举。不能包含指针(指针有对齐要求)或浮点数(某些平台限制)。packed struct 主要用于协议头、硬件寄存器映射等需要精确位控制的场景。

结构体与枚举的反射

// 在 comptime 检查类型信息
const TypeInfo = @typeInfo(Vec2);  // 返回 std.builtin.Type
switch (TypeInfo) {
    .Struct => |s| {
        std.debug.print("字段数: {d}\n", .{s.fields.len});
        for (s.fields) |field| {
            std.debug.print("字段: {s}\n", .{field.name});
        }
    },
    else => {},
}

泛型结构体:参数化类型

Zig 通过 comptime 参数实现泛型,而不是专门的泛型语法。函数可以返回类型,这是 Zig 泛型的核心机制:

// 泛型栈:返回不同元素类型的 Stack 结构体
pub fn Stack(comptime T: type) type {
    return struct {
        items: std.ArrayList(T),

        const Self = @This();  // 引用当前类型

        pub fn init(allocator: std.mem.Allocator) Self {
            return .{ .items = std.ArrayList(T).init(allocator) };
        }

        pub fn deinit(self: *Self) void {
            self.items.deinit();
        }

        pub fn push(self: *Self, value: T) !void {
            try self.items.append(value);
        }

        pub fn pop(self: *Self) ?T {
            if (self.items.items.len == 0) return null;
            return self.items.pop();
        }

        pub fn len(self: Self) usize {
            return self.items.items.len;
        }
    };
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    // 使用泛型 Stack:实例化时传入类型参数
    var int_stack = Stack(i32).init(gpa.allocator());
    defer int_stack.deinit();

    try int_stack.push(1);
    try int_stack.push(2);
    try int_stack.push(3);

    while (int_stack.pop()) |val| {
        std.debug.print("{d}\n", .{val});  // 3, 2, 1(后进先出)
    }

    // 字符串栈
    var str_stack = Stack([]const u8).init(gpa.allocator());
    defer str_stack.deinit();
    try str_stack.push("hello");
    try str_stack.push("world");
}

接口模拟:鸭子类型与 anytype

Zig 没有传统 OOP 的接口,但通过 anytype 参数和 comptime 可以实现类似效果:

// anytype:接受任何实现了 write 方法的类型
fn writeLine(writer: anytype, line: []const u8) !void {
    try writer.writeAll(line);
    try writer.writeByte('\n');
}

// 同一函数可用于 stdout、文件、内存缓冲区等任何 Writer
pub fn main() !void {
    // 写入标准输出
    const stdout = std.io.getStdOut().writer();
    try writeLine(stdout, "Hello, stdout!");

    // 写入内存缓冲区
    var buf: [256]u8 = undefined;
    var fbs = std.io.fixedBufferStream(&buf);
    try writeLine(fbs.writer(), "Hello, buffer!");
    std.debug.print("Buffer content: {s}\n", .{fbs.getWritten()});
}

// 编译期检查:如果类型没有 writeAll 方法,编译报错(而非运行时错误)
// 这比动态类型的"运行时找不到方法"更安全

完整实战:JSON-like 值类型

用 tagged union 实现一个能表示任意嵌套值的动态类型:

const std = @import("std");

const Value = union(enum) {
    null_val,
    bool_val: bool,
    int_val: i64,
    float_val: f64,
    string_val: []const u8,
    array_val: []const Value,  // 嵌套数组

    pub fn isNull(self: Value) bool {
        return self == .null_val;
    }

    pub fn asInt(self: Value) ?i64 {
        return switch (self) {
            .int_val => |i| i,
            .float_val => |f| @intFromFloat(f),
            else => null,
        };
    }

    pub fn print(self: Value, depth: usize) void {
        switch (self) {
            .null_val    => std.debug.print("null", .{}),
            .bool_val    => |b| std.debug.print("{}", .{b}),
            .int_val     => |i| std.debug.print("{d}", .{i}),
            .float_val   => |f| std.debug.print("{d}", .{f}),
            .string_val  => |s| std.debug.print("\"{s}\"", .{s}),
            .array_val   => |arr| {
                std.debug.print("[", .{});
                for (arr, 0..) |v, i| {
                    if (i > 0) std.debug.print(", ", .{});
                    v.print(depth + 1);
                }
                std.debug.print("]", .{});
            },
        }
    }
};

pub fn main() void {
    const values = [_]Value{
        .{ .int_val = 42 },
        .{ .string_val = "hello" },
        .null_val,
        .{ .bool_val = true },
    };
    const array = Value{ .array_val = &values };
    array.print(0);  // 输出: [42, "hello", null, true]
}

本章小结

本章核心要点