第2章 控制流、结构体与类型
第2章 控制流、结构体与类型
上一章我们建立了 Zig 的基础认知。本章将深入探讨 Zig 开发中最高频使用的核心元素:控制流、结构体以及模块系统。
我们将首先学习如何通过循环和条件判断来控制程序流程,接着探讨 Zig 独特的结构体设计——以及如何利用它实现面向对象(OOP)编程模式。最后,我们将剖析 Zig 的类型推断、类型转换以及模块化机制。
控制流 (Control Flow)
控制流决定了程序代码的执行顺序。在 Zig 中,我们主要通过条件分支(if/switch)和循环(for/while)来操控这一流程。
If / Else 语句
这是最基础的条件分支结构。Zig 的 if 语法简洁明了:
const x = 5;
if (x > 10) {
try stdout.print("x > 10!\n", .{});
} else {
try stdout.print("x <= 10!\n", .{});
}
Zig 的 if 也是一个表达式,这意味着你可以直接将 if 的结果赋值给变量(前提是各分支返回类型一致),这类似于三元运算符:
const score = 85;
const grade = if (score >= 60) "Pass" else "Fail";
Switch 语句
Zig 的 switch 语句非常强大且严格,类似于 Rust 的 match。
const Role = enum {
SE, DPE, DE, DA, PM, PO, KS
};
const role = Role.SE;
var area: []const u8 = undefined;
switch (role) {
// 多个匹配项可以用逗号分隔
.PM, .SE, .DPE, .PO => {
area = "Platform";
},
.DE, .DA => {
area = "Data & Analytics";
},
// 每个可能的值都必须被处理
.KS => {
area = "Sales";
},
}
关键特性:
- 穷尽性检查 (Exhaustiveness):Zig 强制要求处理所有可能的情况。如果
role是一个枚举,你必须列出所有枚举值。不能遗漏任何一种可能性,否则编译报错。 - 类型推断:注意代码中的
.PM,Zig 能够根据role的类型自动推断出枚举成员,无需写成Role.PM。 - Else 分支:如果你不想一一列举所有情况,可以使用
else处理剩余所有情况。
switch (level) {
1, 2 => "Beginner",
3 => "Pro",
else => @panic("Unsupported level!"), // 处理所有未列出的情况
}
- 范围匹配:可以使用
...操作符匹配数值范围(闭区间,包含两端)。
const level: u8 = 45;
const category = switch (level) {
0...25 => "Beginner",
26...75 => "Intermediate",
76...100 => "Professional",
else => "Invalid",
};
- 带标签的 Switch:从 Zig 0.14.0 开始,switch 支持标签,配合
continue可实现类似状态机的跳转逻辑。
Defer 关键字
defer 是 Zig 中管理资源生命周期的神器。它允许你注册一段代码,这段代码将在当前作用域(Scope)退出时无条件执行。
它的核心作用是防止资源泄漏。无论函数是因为正常执行完毕、遇到 return 语句,还是因为错误而返回,defer 语句块都会被执行。
fn foo() !void {
// 注册 defer:在离开 foo 函数作用域时打印
defer std.debug.print("Exiting function...\n", .{});
try stdout.print("Working...\n", .{});
// ... 更多逻辑
}
对比 Go:Go 语言的
defer是在函数退出时执行。而 Zig 的defer是在作用域(即当前花括号{})退出时执行。这意味着你可以在if块或循环块中使用defer来精确控制资源释放时机。
Errdefer 关键字
errdefer 是 defer 的兄弟,它是一个条件执行的 defer。
规则:只有当当前作用域返回错误时,errdefer 注册的代码才会被执行。
这在涉及复杂初始化的场景中非常有用:你分配了资源 A,正准备分配资源 B。如果资源 B 分配失败,你需要释放资源 A。这时 errdefer 就派上用场了。
fn createResource() !void {
var item = allocResourceA();
// 如果后续发生错误,记得清理 item
errdefer freeResourceA(item);
try doSomethingRisky(); // 如果这里报错,freeResourceA 会被执行
// 如果执行到这里,说明成功,errdefer 不会触发
return item;
}
循环 (Loops)
For 循环
Zig 的 for 循环主要用于遍历数组或切片。语法结构为 for (items) |capture|。
const items = [_]u8{1, 2, 3};
// 1. 仅获取值
for (items) |value| {
try stdout.print("{d}\n", .{value});
}
// 2. 同时获取值和索引
// 使用 0.. 语法生成索引流,与 items 并行迭代
for (items, 0..) |value, index| {
try stdout.print("Index: {d}, Value: {d}\n", .{index, value});
}
// 3. 遍历切片 (范围)
for (items[0..2]) |value| {
// ...
}
While 循环
while 循环用于基于条件的重复执行。
var i: u8 = 0;
while (i < 5) {
try stdout.print("{d} ", .{i});
i += 1;
}
while 支持继续表达式 (Continue Expression),即在冒号后定义的表达式,它会在每次循环结束前执行。这非常适合处理循环计数器:
var i: u8 = 0;
// i += 1 会在每次循环体执行完后自动执行
while (i < 5) : (i += 1) {
if (i == 2) continue; // 跳过 2,但 i 依然会自增
try stdout.print("{d} ", .{i});
}
// 输出: 0 1 3 4
函数与参数
核心规则:Zig 中的函数参数是不可变的 (Immutable)。
在函数体内,你不能对参数重新赋值。这与 C 语言中参数是局部变量副本的行为不同。
fn addTwo(x: u32) u32 {
// x += 2; // 编译错误!不能修改常量参数
return x + 2;
}
为什么这样设计? 对于基本类型(如整数),Zig 默认按值传递。对于复杂类型(如结构体),Zig 编译器会自动优化,决定是按值传递还是按引用传递。为了保证这种优化的安全性,Zig 强制要求参数不可变。
如果我必须修改参数怎么办? 传递指针。
// 接受一个指向 u32 的指针
fn addTwoInPlace(x: *u32) void {
x.* += 2; // 解引用指针并修改内存中的值
}
pub fn main() !void {
var val: u32 = 10;
addTwoInPlace(&val); // 传递 val 的地址
// val 现在是 12
}
结构体与 OOP (Structs & OOP)
Zig 没有 class(类),没有继承,也没有传统的接口。但它使用 struct(结构体)来实现面向对象编程的核心模式。
在 Zig 中,结构体不仅可以包含数据字段,还可以包含函数(方法)。
const std = @import("std");
const User = struct {
// 1. 数据字段 (以逗号结尾)
id: u64,
name: []const u8,
email: []const u8,
// 2. 静态方法 (构造函数)
// 习惯上命名为 init,返回结构体实例
pub fn init(id: u64, name: []const u8, email: []const u8) User {
return User{
.id = id,
.name = name,
.email = email,
};
}
// 3. 成员方法
// 第一个参数是 self (通常是 User 或 *User)
pub fn printName(self: User) void {
std.debug.print("Name: {s}\n", .{self.name});
}
// 修改状态的方法,self 必须是指针 (*User)
pub fn updateId(self: *User, new_id: u64) void {
self.id = new_id;
}
};
pub fn main() !void {
// 调用静态方法 init
var user = User.init(1, "Pedro", "p@example.com");
// 调用成员方法 (语法糖:user.printName() 等价于 User.printName(user))
user.printName();
// 修改状态
user.updateId(99);
}
关键点解析
-
pub关键字:- 结构体本身需要标记为
pub才能被其他模块访问。 - 结构体的字段和方法也需要分别标记为
pub,否则它们默认是私有的。仅仅公开结构体并不会自动公开其成员。
- 结构体本身需要标记为
-
self参数:- 如果方法只需要读取数据,使用
self: User(或self: *const User)。 - 如果方法需要修改数据,必须使用
self: *User(指针)。
- 如果方法只需要读取数据,使用
-
匿名结构体:
- 在 Zig 中,你可以使用
.{}语法创建结构体,而无需显式写出类型名。前提是编译器能从上下文中推断出类型。 - 例如:
User.init(1, "Name", "Email")返回的是User类型,但在函数内部return User{...}可以简写为return .{...}。
- 在 Zig 中,你可以使用
类型推断 (Type Inference)
Zig 是强类型语言,但它拥有强大的类型推断能力,尤其体现在点语法 (.) 上。
当编译器能从上下文中推断出预期类型时,你可以省略类型名,直接以 . 开头引用枚举值或结构体构造器。
// 1. 枚举推断
const role: Role = .PM; // 等价于 Role.PM
// 2. 结构体推断 (常用于配置对象)
// 假设 print 函数期望第二个参数是结构体
stdout.print("Hello", .{}); // .{} 被推断为一个空元组/结构体
这在 Zig 标准库中随处可见,极大地减少了代码的冗余。
类型转换 (Type Coercion)
Zig 对类型转换非常谨慎,拒绝隐式的危险转换。
1. 安全转换:@as()
当你需要进行显式的、安全的类型转换时,使用内置函数 @as()。
const x: u8 = 10;
const y = @as(u32, x); // 将 u8 提升为 u32,这是绝对安全的
2. 数值转换
- 整数转浮点数:使用
@floatFromInt(int_val)。 - 浮点数转整数:使用
@intFromFloat(float_val)。
const i: i32 = 100;
const f: f32 = @floatFromInt(i);
3. 指针转换:@ptrCast()
这是比较底层的操作。当你需要强制转换指针类型时(例如将 *u8 转为 *u32),使用 @ptrCast()。这通常用于底层系统编程或与 C 代码交互。
const bytes = [_]u8{0x12, 0x34, 0x56, 0x78};
// 强行将字节数组的指针解释为 u32 指针
const u32_ptr: *const u32 = @ptrCast(&bytes);
模块系统 (Modules)
在 Zig 中,文件即模块。
- 导入:使用
@import("file_path.zig")。导入的结果是一个结构体对象,包含了该文件所有pub的成员。 - 标准库:
@import("std")导入的是内置的标准库模块。
// 导入同目录下的 user.zig 文件
const UserModule = @import("user.zig");
pub fn main() void {
// 访问 user.zig 中公开的 User 结构体
const u = UserModule.User.init(...);
}
通过这种方式,Zig 实现了简洁而强大的模块化编程,无需复杂的头文件或命名空间声明。
下章预告:我们将深入 Zig 的核心——内存管理。你将学习 Zig 如何通过分配器(Allocators)来手动管理内存,这是掌握 Zig 的关键一步。