概述
Zig的用户定义类型是经过精心设计的小巧而锐利的工具。结构体在清晰的命名空间下组合数据和行为,枚举使用显式整数表示编码封闭的状态集合,而联合体则建模变体数据——带标签的用于安全性,不带标签的用于低级控制。这些共同构成了符合人体工程学的API和内存感知系统代码的骨干;参见#结构体、#枚举和#联合体作为参考。
本章构建实用的熟练度:结构体的方法和默认值,枚举与@intFromEnum/@enumFromInt的往返转换,以及带标签和不带标签的联合体。我们还将了解布局修饰符(packed、extern)和匿名结构体/元组,这些在轻量级返回值和FFI中变得很方便。参见fmt.zig和math.zig获取相关助手。
学习目标
- 定义和使用带有方法、默认值和清晰命名空间的结构体。
- 安全地在枚举和整数之间进行转换,并对它们进行穷尽匹配。
- 在带标签和不带标签的联合体之间进行选择;理解何时
packed/extern布局很重要(参见#packed struct和#extern struct)。
结构体:数据+命名空间
结构体收集字段和相关的辅助函数。方法只是带有显式接收器参数的函数——没有魔法,这使得调用点显而易见且可进行单元测试。默认值减少了常见情况的样板代码。
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) });
}
$ zig run struct_basics.zigp=(3,0) len=3.000
p=(0,4) len=4.000方法是命名空间函数;你可以根据可测试性和API清晰度自由混合自由函数和方法。
枚举:具有精确位表示的状态
枚举可以设置其整数表示(例如,enum(u8))并使用内置函数在整数之间进行转换。对枚举的switch必须是穷尽的,除非你包含else,这非常适合在编译时捕获新状态。
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) });
}
$ zig run enum_roundtrip.zigm=busy int=1
m2=paused int=2@enumFromInt要求整数映射到已声明的标签。如果你期望未知值(例如,文件格式),请考虑哨兵标签、验证路径或具有显式错误处理的单独整数解析。
联合体:变体数据
带标签的联合体同时携带标签和有效载荷;模式匹配简单且类型安全。不带标签的联合体需要你手动管理活动字段,适用于低级位重新解释或FFI垫片。
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 }),
}
}
$ zig run union_demo.zigstart: number=42
update: hi
raw u=0xFFFFFFFE i=-2在不重新解释位的情况下从不带标签的联合体中读取不同的字段(例如,通过@bitCast)是非法的;Zig在编译时阻止这种情况。除非你真正需要控制,否则优先使用带标签的联合体以确保安全。
带标签联合体的内存表示
理解带标签联合体在内存中的布局方式阐明了安全性与空间之间的权衡,并解释了何时选择带标签与不带标签的联合体:
内存布局细节:
带标签联合体:
- 大小 = 标签大小 + 填充 + 最大变体大小
- 标签字段(通常是u8或适合标签数量的最小整数)
- 用于有效载荷对齐的填充
- 有效载荷空间大小调整为容纳最大变体
- 示例:union(enum) { i32, []const u8 } = 1字节标签 + 7字节填充 + 16字节有效载荷 = 24字节
不带标签联合体:
- 大小 = 最大变体大小(无标签开销)
- 无运行时标签需要检查
- 你需要负责跟踪哪个字段是活动的
- 示例:union { i32, []const u8 } = 16字节(仅有效载荷)
何时使用每种:
- 使用带标签联合体(默认选择):
- 使用不带标签联合体(罕见,专家使用):
安全保证:
带标签联合体提供编译时穷尽性检查和运行时标签验证:
const val = Value{ .number = 42 };
switch (val) {
.number => |n| print("{}", .{n}), // OK - 匹配标签
.text => |t| print("{s}", .{t}), // 编译器确保两种情况都覆盖
}不带标签联合体需要你手动维护安全不变量——编译器无法帮助你。
布局和匿名结构体/元组
当你必须精确地适应位(线格式)或匹配C ABI布局时,Zig提供了packed和extern。匿名结构体(通常称为"元组")对于快速的多值返回很方便。
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 });
}
$ zig run layout_and_anonymous.zigpacked.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提供了三种结构体布局策略,每种策略在内存效率、性能和兼容性方面都有不同的权衡:
布局模式比较:
| 布局 | 大小/对齐 | 字段顺序 | 使用场景 |
|---|---|---|---|
| Default | 由编译器优化 | 可以重新排序 | 普通Zig代码 |
| Packed | 位精确,无填充 | 固定,位级别 | 线格式,位标志 |
| Extern | C ABI rules | Fixed (declaration order) | FFI, C interop |
Detailed behavior:
Default Layout:
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) + paddingPacked Layout:
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 protocolsExtern Layout:
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 dataWhen 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标量(
null、bool、number、string)的带标签联合体,并编写一个格式化每种情况的print函数。
替代方案与边缘情况
- ABI布局:
extern遵循平台ABI。使用@sizeOf/@alignOf验证大小,并在发布库时进行交叉编译。 - 位打包:
packed struct压缩字段但可能增加指令数量;在关键路径上提交之前进行测量。 - 元组与命名结构体:对于稳定的API优先使用命名结构体;元组在本地、短期的胶水代码中表现出色。