Skip to content

Day 8: 控制流 - If与Switch

欢迎来到第八天!任何有意义的程序都需要根据不同的条件执行不同的代码路径。今天,我们将学习Zig中用于控制程序流程的两个核心结构:if 和 switch。与C语言等命令式语言不同,Zig中的 if 和 switch 都是表达式,这意味着它们可以求值并返回一个结果。这种设计使得代码可以写得更简洁、更具表现力。

if 表达式用于根据一个布尔条件来决定执行哪个代码块。

  • 基本形式:

    const condition = true;
    if (condition) {
    // do something if true
    } else {
    // do something if false
    }

    else 分支是可选的。

  • 作为表达式使用: 因为 if 是一个表达式,所以它的结果可以被赋值给一个变量。

    const a = 10;
    const b = 20;
    const max = if (a > b) a else b; // max 的值是 20

    当 if 作为表达式使用时,else 分支是必需的(除非 if 分支返回 noreturn,例如 panic),因为编译器必须保证 max 变量在任何情况下都能被赋值。

switch 表达式提供了一种更强大、更安全的模式匹配方式,用于替代冗长的 if-else if 链。

  • 核心特性:穷尽性 (Exhaustiveness) Zig的 switch 必须是穷尽的,这意味着你必须处理所有可能的情况。这可以防止因遗漏某个case而导致的bug。

    const value: u8 = 1;
    switch (value) {
    1 => { /* ... */ },
    2 => { /* ... */ },
    3...10 => { /* ... */ }, // 可以使用范围
    else => { /* ... */ }, // else 捕获所有其他情况
    }

    如果你省略了 else 并且没有覆盖所有可能的值,编译器会报错。对于枚举类型,你必须处理每一个成员。

  • _ 通配符: 在 switch 中,_ 也可以用作 else 的同义词,表示“捕获所有”。

    switch (value) {
    1 => { /* ... */ },
    _ => { /* ... */ }, // 等同于 else
    }

当有多个互斥的条件需要判断时,你可以使用 if-else if 链,但 switch 通常是更清晰、更安全的选择。

  • if-else if 链:

    const code = 200;
    if (code == 200) {
    // OK
    } else if (code == 404) {
    // Not Found
    } else if (code == 500) {
    // Server Error
    } else {
    // Other
    }
  • 使用 switch:

    const code = 200;
    switch (code) {
    200 => { /* OK */ },
    404 => { /* Not Found */ },
    500 => { /* Server Error */ },
    else => { /* Other */ },
    }

    switch 的意图更明确,并且编译器的穷尽性检查能保证你不会意外漏掉某个case。

if 表达式有一种特殊的语法,用于处理Zig的错误联合类型,这是Zig错误处理的核心模式之一。

  • if (err) |e|: 如果一个函数返回 !T (一个错误或一个值 T),你可以用 if 来“解包”它。

    const result = someFunctionThatCanFail();
    if (result) |value| {
    // 成功:result中包含一个值,现在它被捕获到 `value` 变量中
    // `value` 的类型是 T
    } else |err| {
    // 失败:result中包含一个错误,现在它被捕获到 `err` 变量中
    // `err` 的类型是 error
    }

    |value| 和 |err| 分别被称为“payload capture”和“error capture”。else 分支是可选的。

switch 与枚举(enum)是天作之合。编译器会强制你处理枚举的每一个成员,确保代码的健壮性。

const std = @import("std");
const State = enum { On, Off, Standby };
fn printState(state: State) void {
switch (state) {
.On => std.debug.print("Device is On\n", .{}),
.Off => std.debug.print("Device is Off\n", .{}),
.Standby => std.debug.print("Device is in Standby\n", .{}),
} // 这里不需要 else,因为我们已经处理了所有 State 的成员
}
pub fn main() !void {
printState(State.On);
}

编写一个简单的函数,模拟一个基于数字输入的菜单系统。

  1. 函数签名 fn selectMenu(choice: u8) void。
  2. 使用 switch 表达式来处理不同的 choice:
    • 1: 打印 “Start New Game”
    • 2: 打印 “Load Game”
    • 3: 打印 “Settings”
    • 4: 打印 “Exit”
    • 对于任何其他输入,打印 “Invalid choice”。
  3. 在 main 函数中调用这个函数,并传入不同的值进行测试。

问:如果我确定我的 switch 已经覆盖了所有情况,但编译器仍然要求 else 分支,该怎么办?

答:这通常发生在你对一个整数类型进行 switch 时。例如,一个 u8 有256个可能的值。即使你只关心其中的几个,编译器也无法知道其他值不会出现。在这种情况下,你必须提供一个 else 分支。如果你确信其他情况在逻辑上不可能发生,可以在 else 分支中使用 unreachable,这会告诉编译器:如果程序走到了这里,那就是一个bug。

switch (value) {
0...9 => { /* ... */ },
else => unreachable, // 我断言 value 不会大于9
}

问:if 表达式的两个分支必须返回相同的类型吗?

答:是的。因为 if 表达式本身需要有一个确定的类型,所以所有可能的分支都必须能够返回相同或可以强制转换为相同类型的值。

今天,我们学习了 if 和 switch 这两个控制流的基本工具。我们看到了它们作为表达式的强大能力,以及 switch 的穷尽性检查如何帮助我们编写更安全的代码。我们还首次接触了 if 在错误处理中的特殊用法,这是理解Zig健壮性设计的关键一步。

明天,我们将学习循环结构:while 和 for,它们是处理重复任务的基础。