第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";
    },
}

关键特性

  1. 穷尽性检查 (Exhaustiveness):Zig 强制要求处理所有可能的情况。如果 role 是一个枚举,你必须列出所有枚举值。不能遗漏任何一种可能性,否则编译报错。
  2. 类型推断:注意代码中的 .PM,Zig 能够根据 role 的类型自动推断出枚举成员,无需写成 Role.PM
  3. Else 分支:如果你不想一一列举所有情况,可以使用 else 处理剩余所有情况。
switch (level) {
    1, 2 => "Beginner",
    3 => "Pro",
    else => @panic("Unsupported level!"), // 处理所有未列出的情况
}
  1. 范围匹配:可以使用 ... 操作符匹配数值范围(闭区间,包含两端)。
const level: u8 = 45;
const category = switch (level) {
    0...25 => "Beginner",
    26...75 => "Intermediate",
    76...100 => "Professional",
    else => "Invalid",
};
  1. 带标签的 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 关键字

errdeferdefer 的兄弟,它是一个条件执行的 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);
}

关键点解析

  1. pub 关键字

    • 结构体本身需要标记为 pub 才能被其他模块访问。
    • 结构体的字段和方法也需要分别标记为 pub,否则它们默认是私有的。仅仅公开结构体并不会自动公开其成员。
  2. self 参数

    • 如果方法只需要读取数据,使用 self: User(或 self: *const User)。
    • 如果方法需要修改数据,必须使用 self: *User(指针)。
  3. 匿名结构体

    • 在 Zig 中,你可以使用 .{} 语法创建结构体,而无需显式写出类型名。前提是编译器能从上下文中推断出类型。
    • 例如:User.init(1, "Name", "Email") 返回的是 User 类型,但在函数内部 return User{...} 可以简写为 return .{...}

类型推断 (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 中,文件即模块

  1. 导入:使用 @import("file_path.zig")。导入的结果是一个结构体对象,包含了该文件所有 pub 的成员。
  2. 标准库@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 的关键一步。


原文出处 : https://pedropark99.github.io/zig-book/