结构体 — 数据的打包带,兼职的命名空间,和方法语法的糖¶
这一章讲什么: 2.3 节讲 Zig 的组织单位 struct(结构体)。 它先是个普通的东西:几个不同名字的字段打包成一个值。 然后作者掀开第二层:struct 还能装函数,于是它也是命名空间—— 源文件本身就是 struct,2.1 节那句伏笔在这里兑现。 最后是最 Zig 的部分:没有 class、没有继承、没有虚表, 面向对象的那点需求用「第一参数是自身的函数 + 点语法糖」接住。
1. 这一章讲什么¶
struct 是 Zig 的复合类型(composite type):把若干不同类型的变量 捆成一个单元,可以整体操作,也可以单独取用成员1。
核心就三条:
2. 顶层全景:struct 的三张脸¶
脸 ① 数据包: 字段打包,值语义(拷贝 = 整份拷)
脸 ② 命名空间:里面可放 fn / const;源文件本身就是 struct
脸 ③ 方法宿主:第一参数是自身类型的 fn,可用点语法调
图说:三张脸是同一条语法,没有三种机制。
3. 核心原理¶
3.1 数据包:初始化一个都不能少¶
const Point = struct {
x: i32,
y: i32,
};
const a = Point{ .x = 3, .y = -1 }; // 两个字段都要给
std.debug.print("x = {}, y = {}\n", .{a.x, a.y}); // 点语法取字段
Point{ .x = 3, .y = -1 } 里每个字段名前面的小点是 Zig 的「枚举式」字段初始化语法;
漏掉任何一个字段,编译器都会拦——和「歧义就是技术债」一个逻辑:
「想用部分初始化的 struct」是一种含糊,必须显式化。
3.2 命名空间:文件就是 struct 的兑现¶
还记得 04 篇那个伏笔吗——「源文件在 Zig 里定义的就是一个 struct 类型」。 这一节它兑现了。先看一个没有字段的 struct:
const animal_noises = struct {
fn moo() void { std.debug.print("moo!\n", .{}); }
fn woof() void { std.debug.print("woof!\n", .{}); }
};
pub fn main() void {
animal_noises.moo();
animal_noises.woof();
}
这个 struct 一个字段都没有,纯粹拿 struct 语法当命名空间用——完全合法5。
作者接着说:还记得我说过源文件其实也是 struct 吗?
把这个例子的全部代码想象成套在一个 struct {} 里面,
你看到的正是编译器看待源文件的方式;事实上,
编译器解析源文件和解析 struct 用的是同一份代码6。
这一句是理解 Zig 模块系统的钥匙:@import 一个文件得到的是 struct 类型,
点语法取它的声明,和点语法取 struct 字段,是同一个动作。
模块没有单独的机制——又是一次「一个机制,多种用法」。
3.3 伪方法:点语法是糖,指针才是真相¶
Zig 没有 class,但有「看起来是方法」的写法。规则只有一条: struct 里声明的函数,如果第一个参数的类型就是这个 struct 本身, 就可以用点语法调7。
工具是内建函数 @This():它指代「这段代码所在的容器类型」,
惯例是赋给一个名叫 Self 的常量——struct 改名时不用跟着改8。
本章主线例子:Rectangle 从读到写,一步一个具体数。
const Rectangle = struct {
const Self = @This();
width: i32,
height: i32,
fn size(self: Self) i32 {
return self.width * self.height;
}
};
pub fn main() void {
const rect = Rectangle{ .width = 20, .height = 30 };
const rect_size = rect.size(); // 点语法
const rect_size2 = Rectangle.size(rect); // 函数语法,与上面完全等价
// 都打印 600
}
主线例子的每一步:
rect Rectangle{ width = 20, height = 30 }
rect.size() Sugar(糖):等价于 Rectangle.size(rect)
size 内部 self.width * self.height = 20 * 30
输出 600(两种调用方式各打印一次 600)
图说:没有 this 指针魔术——self 就是个普通参数,点语法只是把它挪到前面。
数字是程序真实输出。
为什么叫「伪」方法:值语义在这里咬人。
注意 size 的 self 参数是 Rectangle 值——按 05 篇的规则,
传进去的是一份拷贝。想真的改实例,就得传指针:
fn quadruple(self: *Rectangle) void {
self.width = self.width * 2; // 点语法对 struct 指针也适用,
// 背后等价于 self.*.width
self.height *= 2;
}
接着主线例子走:
rect 起点 width = 10, height = 20
rect.quadruple()
self.width = 10 * 2 = 20 self.height = 20 * 2 = 40
rect.size() 20 * 40 = 800 ← 第一次输出
Rectangle.quadruple(&rect) ← 函数语法版,这次必须显式传 &rect
self.width = 20 * 2 = 40 self.height = 40 * 2 = 80
rect.size() 40 * 80 = 3200 ← 第二次输出
图说:想改就传 *Rectangle,这是值语义世界里的铁规矩。数字是程序真实输出。
原书对此的评语:「这里没有魔法,只是一点糖」9。
糖(syntactic sugar)指不改变语义、只让写法更顺的语法——
rect.size() 和 Rectangle.size(rect) 编译后是同一样东西。
对比有 class 的语言:方法调用背后藏着 this 指针和动态分派,
Zig 把这两件事都拆开了——要不要指针你自己定,要不要动态分派
(本章根本没有,以后用 comptime 自己造)。
3.4 默认字段值与 config struct 模式¶
struct 要求初始化给全字段,但有时「全给」太啰嗦——大部分字段其实都有 显然的默认值。Zig 允许在字段声明处写默认值:
pub const PopupOptions = struct {
title: []const u8 = "", // 默认空标题
buttons: []const Button = &.{}, // 默认没有按钮
};
pub fn addPopup(opts: PopupOptions) void { /* …… */ }
调用方就可以只给想配的部分:addPopup(.{})(全默认),
或者 addPopup(.{ .title = "Do you REALLY want to delete that?" })。
原书把这种「把一堆参数打包成 struct、带默认值」的用法叫 config struct 模式,说标准库到处都是,必须先讲掉10。 它值得单独命名,因为它解决的是「函数参数一长串且大多有默认」的通病: 参数一多,位置就乱;包成 struct,名字自解释,还能随版本加字段而不破坏旧调用。
3.5 极简的边界,和一句翻译咒语¶
2.3 节的收尾把立场说死了:Zig 的 struct 保持极简—— 没有继承、没有虚方法表;struct 就是「字段 + 声明 + 一点语法糖」, 不多不少11。习惯了高层语言那套复合类型特性的人,可能觉得它简陋; 但 comptime 很快会给基础数据结构添上一大批新能力(原书预告,未展开)。
然后作者留了一句值得抄下来的话:
「如果一件事在 Zig 里真的很难做,那说明 Zig 期望你用另一种方式做它。」12
这句话是全书价值观的翻译咒语。每当在 Zig 里撞墙—— 「怎么连继承都没有」「怎么没有虚函数」「怎么写个回调(把函数递给别的东西、 让它回头调你)这么绕」—— 先别骂,拿这句话问自己:我想要的到底是继承,还是代码复用? Zig 给的另一种做法通常更直白,只是和习惯拧着。
2.4 节的总结六条,一句话转述:项目由「源文件打包成模块」构成; 类型和运算符给得很全;指针/数组/slice 按语义区分; 字符串就是带编码的字节;struct 是打包多变量的主力机制,语法糖让它好用13。
4. 作者的判断与证据¶
| 判断 | 是书里给的证据,还是作者的推测 |
|---|---|
| struct 就是字段+声明+糖,不多不少 | 语言事实(确实没有继承/虚表) |
| 编译器用同一份代码解析文件和 struct | 可核对的实现陈述(start.zig 经验同逻辑) |
| config struct 模式在标准库到处都是 | 作者经验陈述,例子可验 |
| 「很难做 = 该换做法」 | 设计哲学断言,是信念不是论证 |
| comptime 会给 struct 加能力(预告) | 预告,MEAP V01 未兑现 |
5. 边界与局限¶
- 「伪方法」有个本书没明说的限制:点语法糖只对第一个参数生效, 且靠类型匹配识别——两个不同类型的「同名方法」没有重载(overload), Zig 没有同名函数重载这回事。
- 没有虚表意味着没有运行时多态的现成答案——C 时代用函数指针, Zig 时代要么用 vtable 模式手动搭(第 07 篇的 Writer 接口就是它), 要么等 comptime 泛型。本节完全没提这条路,读者要自己把坑填上。
- struct 的内存布局(字段顺序、对齐、padding)本节未提, 而它和 C 互操作关系很大——属于后续章节的账。
6. 可带走的¶
- struct = 字段打包;字段名唯一;初始化必须给全;
- struct 是值:传参会拷;要改就传
*Struct; - struct 即命名空间:能装 fn 和 const;文件就是 struct, import 文件 = 拿到一个 struct 类型;
@This()指代所在容器类型,惯例const Self = @This();;- 点语法 = 糖:
rect.size()≡Rectangle.size(rect),没有 this 魔法; - struct 指针用点语法访问字段,背后自动解一次引用(self.*.width);
- 默认字段值 → config struct 模式:多参数带默认值的标准解法;
- 没有继承、没有虚表;撞墙时翻译:「很难做 = 该换做法」;
- 2.4 总结:模块 = 源文件打包;指针/数组/slice 按语义分型;字符串 = 带编码的字节。
7. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| struct 是复合类型 | 2.3 Structs | text/14-ch02-03-2-3-structs.txt:2(搜「composite type」) |
| 字段唯一名、初始化给全 | 2.3 Structs | text/14-ch02-03-2-3-structs.txt:12(搜「unique name」) |
| struct 是值、可嵌套 | 2.3 Structs | text/14-ch02-03-2-3-structs.txt:38(搜「structs are just values」) |
| struct 即命名空间 | 2.3 Structs | text/14-ch02-03-2-3-structs.txt:42(搜「Structs are also namespaces」) |
| 文件即 struct;同一份解析代码 | 2.3 Structs | text/14-ch02-03-2-3-structs.txt:76(搜「same code」) |
| 点语法规则:第一参数是自身 | 2.3 Structs | text/14-ch02-03-2-3-structs.txt:78(搜「familiar dot syntax」) · text/14-ch02-03-2-3-structs.txt:114(搜「call it with the dot syntax」) |
| @This() 与 Self 惯例 | 2.3 Structs | text/14-ch02-03-2-3-structs.txt:83(搜「@This()」) |
| 「没有魔法,只是一点糖」 | 2.3 Structs | text/14-ch02-03-2-3-structs.txt:125(搜「No magic here」) |
| config struct 模式 | 2.3 Structs | text/14-ch02-03-2-3-structs.txt:176(搜「config struct pattern」) · text/14-ch02-03-2-3-structs.txt:183(搜「Popup config struct」) |
| 没有继承、没有虚表 | 2.3 Structs | text/15-ch02-04-2-4-summary.txt:2(搜「inheritance」) |
| 「很难做 = 该换做法」 | 2.3 Structs | text/15-ch02-04-2-4-summary.txt:9(搜「really hard to do」) |
| 2.4 总结 | 2.4 Summary | text/15-ch02-04-2-4-summary.txt:21(搜「primary mechanism」) |
-
出处:「2.3 Structs」第 1–9 段(COMPOSITE TYPE 栏,
text/14-ch02-03-2-3-structs.txt:2,搜「composite type」;:7,搜「grouped」)。原文:「A data structure that allows multiple variables of different types to be grouped together into a single unit.」 ↩ -
出处:「2.3 Structs」第 11–16 段(
text/14-ch02-03-2-3-structs.txt:12,搜「unique name」)。原文:「Each field of a struct must have a unique name. To initialize a struct, you must initialize all of its fields.」 ↩ -
出处:「2.3 Structs」第 36–40 段(
text/14-ch02-03-2-3-structs.txt:38,搜「structs are just values」;:39,搜「fields of other structs」)。原文:「At the end of the day, structs are just values. You can pass them around as function parameters, or use structs as fields of other structs!」 ↩ -
出处:「2.3 Structs」第 42 段(
text/14-ch02-03-2-3-structs.txt:42,搜「Structs are also namespaces」)。原文:「Structs are also namespaces.」 ↩ -
出处:「2.3 Structs」第 46–71 段(listing 2.18 及其讲解,
text/14-ch02-03-2-3-structs.txt:71,搜「to group any data together」)。原文:「Although animal_noises is a struct type, we aren't using it to group any data together. In this case, we're just using the struct syntax as a handy way to define a namespace.」「This struct has no fields, which is not an issue in Zig.」 ↩ -
出处:「2.3 Structs」第 72–76 段(
text/14-ch02-03-2-3-structs.txt:76,搜「same code」)。原文:「If you imagine that all of the code in this example is enclosed within the struct {} body, you would see an accurate picture of how the compiler treats source files. In fact, the compiler itself even parses source files and structs using the same code.」 ↩ -
出处:「2.3 Structs」第 77–81 段(
text/14-ch02-03-2-3-structs.txt:78,搜「familiar dot syntax」;:114,搜「call it with the dot syntax」)。原文:「If the first parameter of a function declared within a struct has the type of the struct itself, it will refer to the instance of the struct when called using the method syntax.」 ↩ -
出处:「2.3 Structs」第 83–87 段(
text/14-ch02-03-2-3-structs.txt:83,搜「@This()」)。原文:「A handy tool for writing methods is the @This() function, which refers to the container type that code lives in. Zig programmers often assign this to a variable named Self. This practice prevents code churn if the name of a struct needs to change.」 ↩ -
出处:「2.3 Structs」第 125–128 段(
text/14-ch02-03-2-3-structs.txt:125,搜「No magic here」)。原文:「No magic here, just a little 'sugar.'」「the self parameter in Rectangle.size is a value, not a reference. This means that, semantically speaking, the rectangle passed to Rectangle.size is a copy of our rectangle.」 ↩ -
出处:「2.3 Structs」第 173–181 段(2.3.2,
text/14-ch02-03-2-3-structs.txt:176,搜「config struct pattern」)。原文:「A common location where defaults show up is what I would call the config struct pattern, where values are passed to a function as a struct instead of directly as parameters. This pattern is used all over the standard library.」 ↩ -
出处:「2.3 Structs」第 1–7 段(2.3.3,
text/15-ch02-04-2-4-summary.txt:2,搜「inheritance」)。原文:「There are no object-oriented features like inheritance or virtual method tables. Structs are just a collection of fields and declarations with a sprinkle of syntax sugar — nothing more, nothing less.」 ↩ -
出处:「2.3 Structs」第 8–10 段(
text/15-ch02-04-2-4-summary.txt:9,搜「really hard to do」)。原文:「If something is really hard to do in Zig, that means Zig expects you do to it differently!」 ↩ -
出处:「2.4 Summary」第 14–24 段(
text/15-ch02-04-2-4-summary.txt:21,搜「primary mechanism」)。六条总结逐条对应原文:「Every Zig project is made up of source files packaged into modules.」「Zig gives you an exhaustive set of numeric and logical data types…」「We can semantically encode how to interact with multiple data using pointer, array, and slice types.」「Strings in Zig are just bytes in memory with an encoding.」「Structs are the primary mechanism in Zig to package multiple variables of potentially different types.」「Zig provides ergonomic syntax 'sugar' to make structs nice to work with.」 ↩