Skip to content

Day 16: 枚举与联合体

欢迎来到第十六天!继昨天学习了结构体(struct)之后,今天我们将探索Zig中另外两种强大的复合数据类型:枚举(enum)和联合体(union)。枚举用于创建一组命名的常量,非常适合表示状态或一组有限的选项,它提供了比整数常量更高的类型安全性。联合体则允许在同一块内存中存储不同类型的值,但一次只能存储其中一种,这是一种节省内存的有效方式。将两者结合,就形成了Zig中一个极其强大的模式:标签联合体(tagged union)。

枚举(enum)定义了一组命名的整数常量,这让代码更具可读性和类型安全性。

const LogLevel = enum {
Debug,
Info,
Warning,
Error,
};
var level: LogLevel = .Info;

默认情况下,枚举的值从0开始依次递增(Debug=0, Info=1, …)。你也可以为枚举指定一个整数基类型,并显式设置值。

const HttpStatus = enum(u16) {
Ok = 200,
NotFound = 404,
InternalServerError = 500,
};

枚举最大的优势在于类型安全。LogLevel.Info 和整数 1 是完全不同的类型,你不能将它们混用,这可以防止许多逻辑错误。

联合体(union)的所有字段共享同一块内存。这意味着在任何时候,只有一个字段是“活动的”。联合体的大小由其最大的字段决定。

const Value = union {
integer: i64,
float: f64,
boolean: bool,
};
var v: Value = .{ .integer = 123 };
// 此时,v中存储的是一个整数
v.float = 3.14;
// 现在,v中存储的是一个浮点数,整数值已经被覆盖

警告:普通的联合体很危险,因为编译器不知道当前哪个字段是活动的。访问了非活动的字段会导致未定义行为。为了解决这个问题,Zig引入了标签联合体。

标签联合体是联合体和枚举的结合。它使用一个枚举字段(“标签”)来跟踪当前哪个联合体字段是活动的。这是Zig中实现代数数据类型(Algebraic Data Types)的主要方式。

const Payload = union(enum) {
message: []const u8,
user_id: u64,
error_code: u16,
// 这是一个在tagged union中定义的方法
fn print(self: Payload) void {
// ...
}
};

在 union(enum) 中,Zig会自动创建一个匿名的枚举,其成员与联合体的字段名相同。这个枚举就是“标签”。

使用 switch 处理标签联合体:

switch 表达式是处理标签联合体的完美工具。编译器会强制你处理所有可能的情况,确保代码的完备性。

var p: Payload = .{ .message = "Hello" };
switch (p) {
.message => |msg| std.debug.print("Message: {s}\n", . প্রশস্ত{msg}),
.user_id => |id| std.debug.print("User ID: {d}\n", .{id}),
.error_code => |code| std.debug.print("Error Code: {d}\n", .{code}),
}

在 switch 的分支中,|msg| |id| |code| 会自动捕获并解开(unwrap)活动字段的值,非常方便。

有时你需要将枚举与其底层的整数值进行转换,可以使用内置函数 @enumToInt 和 @intToEnum。

const status = HttpStatus.NotFound; // 值为404的枚举
const status_code: u16 = @enumToInt(status); // 404
const new_status: HttpStatus = @intToEnum(200); // HttpStatus.Ok

警告:@intToEnum 是不安全的。如果你传入一个枚举中未定义的值,会导致未定义行为。

6. 示例:使用标签联合体实现状态机

Section titled “6. 示例:使用标签联合体实现状态机”

标签联合体非常适合用来建模状态机。

const std = @import("std");
const TrafficLight = union(enum) {
Red,
Yellow,
Green,
fn next(self: *TrafficLight) void {
switch (self.*) {
.Red => self.* = .Green,
.Green => self.* = .Yellow,
.Yellow => self.* = .Red,
}
}
};
pub fn main() !void {
var light: TrafficLight = .Red;
for (0..5) |_| {
switch (light) {
.Red => std.debug.print("Stop!\n", .{}),
.Green => std.debug.print("Go!\n", .{}),
.Yellow => std.debug.print("Caution!\n", .{}),
}
light.next();
}
}

7. 实践练习:带错误的标签联合体

Section titled “7. 实践练习:带错误的标签联合体”

创建一个名为 Result 的标签联合体,它可以存储一个 i32 类型的成功值,或者一个 error{ParseError, Overflow} 类型的错误值。

然后编写一个函数 parseInt(s: []const u8) Result,它尝试将一个字符串解析为 i32。如果成功,返回 .ok 成员;如果字符串格式不正确,返回 .err 成员并附带 error.ParseError。

问:联合体的内存布局是怎样的?

答:联合体的大小等于其最大字段的大小。所有字段都从相同的内存地址开始。对于标签联合体,标签(枚举)会存储在联合体旁边,其总大小是标签大小加上联合体大小(可能还有对齐填充)。

问:我可以在 enum 或 union 中定义方法吗?

答:可以。就像 struct 一样,你可以在 enum 和 union 的定义内部声明函数,这些函数就成为该类型的方法。这对于封装与该类型紧密相关的逻辑非常有用,如上面的状态机示例所示。

今天,我们学习了Zig中用于创建更富表达力、更安全代码的两种关键类型:枚举和联合体。枚举提供了类型安全的方式来表示一组有限的常量。联合体则实现了高效的内存复用。而将两者结合的标签联合体(tagged union),配合 switch 表达式,是Zig中一个极其强大和安全的模式,用于处理“一个东西可能是这几种类型之一”的情况,是模式匹配的核心。

明天,我们将探讨Zig如何处理 null 值:可选类型与空安全。