概述
Zig的用户定义类型是特意设计得小而精的工具。结构体将数据和行为组合在一个清晰的命名空间下,枚举用显式的整数表示来编码封闭的状态集,而联合体则模拟变体数据——为安全而标记或为底层控制而未标记。它们共同构成了符合人体工程学的API和内存感知系统代码的骨干;详情请参阅#结构体、#枚举和#联合体。
本章培养了实用的流畅性:结构体上的方法和默认值,用@intFromEnum/@enumFromInt进行的枚举往返转换,以及带标签和不带标签的联合体。我们还将窥探布局修饰符(packed、extern)和匿名结构体/元组,它们对于轻量级返回值和FFI非常方便。相关助手请参见fmt.zig和math.zig。
学习目标
- 定义和使用带有方法、默认值和清晰命名空间的结构体。
- 安全地将枚举与整数相互转换,并对其进行详尽的匹配。
- 在带标签和不带标签的联合体之间进行选择;理解何时
packed/extern布局很重要(参见#packed-struct和#extern-struct)。
结构体:数据 + 命名空间
结构体将字段和相关的辅助函数聚集在一起。方法只是带有显式接收者参数的函数——没有魔法,这使得调用点显而易见且易于单元测试。默认值减少了常见情况的样板代码。
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) });
}
$ 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");
// 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) });
}
$ zig run enum_roundtrip.zigm=busy int=1
m2=paused int=2@enumFromInt要求整数映射到一个已声明的标签。如果你期望未知的值(例如,文件格式),请考虑使用哨兵标签、验证路径或带有显式错误处理的独立整数解析。
联合体:变体数据
带标签的联合体同时携带一个标签和一个有效载荷;模式匹配既直接又类型安全。不带标签的联合体需要你手动管理活动字段,适用于底层位重新解释或FFI填充。
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 }),
}
}
$ 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 - matches tag
.text => |t| print("{s}", .{t}), // Compiler ensures both cases covered
}不带标签的联合体需要你手动维护安全不变量——编译器帮不了你。
布局和匿名结构体/元组
当你必须精确地安排位(线路格式)或匹配C ABI布局时,Zig提供了packed和extern。匿名结构体(通常称为“元组”)对于快速返回多个值很方便。
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 });
}
$ 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提供了三种结构体布局策略,每种策略在内存效率、性能和兼容性方面都有不同的权衡:
布局模式比较:
| 布局 | 大小/对齐 | 字段顺序 | 用例 |
|---|---|---|---|
| 默认 | 由编译器优化 | 可以重新排序 | 普通Zig代码 |
| 紧凑 | 位精确,无填充 | 固定,位级 | 线路格式,位标志 |
| 外部 | C ABI规则 | 固定(声明顺序) | FFI,C互操作 |
详细行为:
默认布局:
const Point = struct {
x: u8, // 编译器可能会重新排序这个
y: u32, // 以最小化填充
z: u8,
};
// 编译器选择最佳顺序,通常是:
// y (4字节,对齐) + x (1字节) + z (1字节) + 填充紧凑布局:
const Flags = packed struct {
enabled: bool, // 位 0
mode: u3, // 位 1-3
priority: u4, // 位 4-7
};
// 总共:8位 = 1字节,无填充
// 非常适合硬件寄存器和线路协议外部布局:
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标量(
null、bool、number、string)的带标签联合体,并编写一个print函数来格式化每种情况。
替代方案和边缘情况
- ABI布局:
extern尊重平台ABI。在发布库时,用@sizeOf/@alignOf验证大小并进行交叉编译。 - 位打包:
packed struct压缩字段但会增加指令数;在热路径中提交前进行测量。 - 元组与命名结构体:对于稳定的API,优先使用命名结构体;元组在局部、短生命周期的粘合代码中表现出色。