Chapter 02Control Flow Essentials

控制流基础

概述

第一章为运行Zig程序和处理数据奠定了基础;现在我们通过遍历语言的控制流原语,将这些值转化为决策,具体如#if中所述。Zig中的控制流是面向表达式的,因此选择一个分支或循环通常会产生一个值,而不仅仅是引导执行。

我们探讨了循环、带标签的流和switch背后的语义,强调了breakcontinueelse子句如何在安全和发布构建中传达意图;参见#while#for#switch

学习目标

  • 使用if表达式(带有可选的有效载荷捕获)来派生值,同时明确处理缺失的数据路径。
  • while/for循环与带标签的break/continue结合使用,以清晰地管理嵌套迭代和退出条件。
  • 应用switch来枚举详尽的决策表,包括范围、多个值和枚举。
  • 利用循环的else子句和带标签的break直接从迭代结构中返回值。

控制流代码会发生什么

在深入学习控制流语法之前,了解编译器如何处理你的ifwhileswitch语句是很有帮助的。Zig通过多个中间表示(IRs)转换源代码,每个IR都有其特定的目的:

graph LR SOURCE["源代码<br/>.zig 文件"] TOKENS["令牌流"] AST["抽象语法树<br/>(Ast.zig)"] ZIR["ZIR<br/>(Zir)"] AIR["AIR<br/>(Air.zig)"] MIR["MIR<br/>(codegen.AnyMir)"] MACHINE["机器码"] SOURCE -->|"tokenizer.zig"| TOKENS TOKENS -->|"Parse.zig"| AST AST -->|"AstGen.zig"| ZIR ZIR -->|"Sema.zig"| AIR AIR -->|"codegen.generateFunction()"| MIR MIR -->|"codegen.emitFunction()"| MACHINE
IR阶段表示关键属性控制流的目的
令牌扁平令牌流原始词法分析识别ifwhileswitch关键字
AST树形结构语法正确,未类型化保留嵌套控制流的结构
ZIR基于指令的IR未类型化,每个声明一个SSA形式将控制流降低为块和分支
AIR基于指令的IR完全类型化,每个函数一个SSA形式带有已知结果的类型检查分支
MIR后端特定的IR接近机器码,已寄存器分配转换为跳转和条件指令

你编写的控制流结构——if表达式、switch语句、带标签的循环——都会系统地通过这些阶段进行降低。当你的代码到达机器码时,一个switch已经变成了一个跳转表,而一个while循环则是一个条件分支指令。本章中的图表展示了这种降低在ZIR阶段是如何发生的,此时控制流变成了显式的块和分支。

核心控制结构

Zig中的控制流将块和循环视为表达式,这意味着每个结构都可以产生一个值,并直接参与赋值或返回语句。本节将逐步介绍条件、循环和switch,展示它们如何融入表达式模型,同时保持高度的可读性,具体如#代码块中所述。

作为表达式的条件

if的求值结果是其运行分支的值,而可选的捕获形式(if (opt) |value|)是一种简洁的方式来解包可选值,而不会遮蔽早期的名称。嵌套的带标签块(blk: { …​ })让你可以在多个结果中进行选择,同时仍然返回单个值。

Zig
// File: chapters-data/code/02__control-flow-essentials/branching.zig

// Demonstrates Zig's control flow and optional handling capabilities
// 演示Zig的控制流和可选值处理能力
const std = @import("std");

/// Determines a descriptive label for an optional integer value.
/// 为可选整数值确定描述性标签。
/// Uses labeled blocks to handle different numeric cases cleanly.
/// 使用带标签的块来清晰处理不同的数值情况。
/// Returns a string classification based on the value's properties.
/// 根据值的属性返回字符串分类。
fn chooseLabel(value: ?i32) []const u8 {
    // Unwrap the optional value using payload capture syntax
    // 使用有效载荷捕获语法解包可选值
    return if (value) |v| blk: {
        // Check for zero first
        // 首先检查是否为零
        if (v == 0) break :blk "zero";
        // Positive numbers
        // 正数
        if (v > 0) break :blk "positive";
        // All remaining cases are negative
        // 所有剩余情况都是负数
        break :blk "negative";
    } else "missing"; // Handle null case
    // 处理null情况

pub fn main() !void {
    // Array containing both present and absent (null) values
    // 包含存在和不存在(null)值的数组
    const samples = [_]?i32{ 5, 0, null, -3 };

    // Iterate through samples with index capture
    // 使用索引捕获遍历样本
    for (samples, 0..) |item, index| {
        // Classify each sample value
        // 分类每个样本值
        const label = chooseLabel(item);
        // Display the index and corresponding label
        // 显示索引和对应的标签
        std.debug.print("sample {d}: {s}\n", .{ index, label });
    }
}
运行
Shell
$ zig run branching.zig
输出
Shell
sample 0: positive
sample 1: zero
sample 2: missing
sample 3: negative

该函数返回一个[]const u8,因为if表达式本身产生了字符串,这强调了面向表达式的分支如何使调用点保持紧凑。samples循环显示,for可以与索引元组(item, index)一起迭代,但仍然依赖于上游表达式来格式化输出。

if-else表达式如何降低到ZIR

当编译器遇到一个if表达式时,它会将其转换为ZIR(Zig中间表示)中的块和条件分支。确切的降低方式取决于是否需要结果位置;参见结果位置

graph TB IfNode["if (cond) then_expr else else_expr"] --> EvalCond["评估条件"] EvalCond --> CheckRL["需要结果位置吗?"] CheckRL -->|无RL| SimpleIf["生成condbr<br/>两个带break的块"] CheckRL -->|有RL| BlockIf["生成block_inline<br/>共享结果指针"] SimpleIf --> ThenBlock["then_block:<br/>评估then_expr<br/>break value"] SimpleIf --> ElseBlock["else_block:<br/>评估else_expr<br/>break value"] BlockIf --> AllocResult["alloc_inferred"] BlockIf --> ThenBlockRL["then_block:<br/>写入结果指针"] BlockIf --> ElseBlockRL["else_block:<br/>写入结果指针"]

当你写const result = if (x > 0) "positive" else "negative"时,编译器会创建两个块(每个分支一个),并使用break语句返回所选的值。这就是为什么if表达式可以参与赋值——它们被编译成通过其break语句产生值的块。

带标签的while和for循环

Zig中的循环可以通过将break结果与循环的else子句配对来直接传递值,当执行完成而没有中断时,该子句就会触发。带标签的循环(outer: while (…​))协调嵌套迭代,这样你就可以提前退出或跳过工作,而无需使用临时布尔值。

Zig
// File: chapters-data/code/02__control-flow-essentials/loop_labels.zig

// Demonstrates labeled loops and while-else constructs in Zig
// 演示Zig中的带标签循环和while-else结构
const std = @import("std");

/// Searches for the first row where both elements are even numbers.
/// 搜索第一个两个元素都为偶数的行。
/// Uses a while loop with continue statements to skip invalid rows.
/// 使用带有continue语句的while循环跳过无效行。
/// Returns the zero-based index of the matching row, or null if none found.
/// 返回匹配行的基于零的索引,如果没有找到则返回null。
fn findFirstEvenPair(rows: []const [2]i32) ?usize {
    // Track current row index during iteration
    // 在迭代期间跟踪当前行索引
    var row: usize = 0;
    // while-else construct: break provides value, else provides fallback
    // while-else结构:break提供值,else提供后备
    const found = while (row < rows.len) : (row += 1) {
        // Extract current pair for examination
        // 提取当前对进行检查
        const pair = rows[row];
        // Skip row if first element is odd
        // 如果第一个元素是奇数则跳过该行
        if (@mod(pair[0], 2) != 0) continue;
        // Skip row if second element is odd
        // 如果第二个元素是奇数则跳过该行
        if (@mod(pair[1], 2) != 0) continue;
        // Both elements are even: return this row's index
        // 两个元素都是偶数:返回此行的索引
        break row;
    } else null; // No matching row found after exhausting all rows
    // 耗尽所有行后未找到匹配行

    return found;
}

pub fn main() !void {
    // Test data containing pairs of integers with mixed even/odd values
    // 包含具有混合奇偶值的整数对测试数据
    const grid = [_][2]i32{
        .{ 3, 7 }, // Both odd
        // 都是奇数
        .{ 2, 4 }, // Both even (target)
        // 都是偶数(目标)
        .{ 5, 6 }, // Mixed
        // 混合
    };

    // Search for first all-even pair and report result
    // 搜索第一个全偶数对并报告结果
    if (findFirstEvenPair(&grid)) |row| {
        std.debug.print("first all-even row: {d}\n", .{row});
    } else {
        std.debug.print("no all-even rows\n", .{});
    }

    // Demonstrate labeled loop for multi-level break control
    // 演示用于多级中断控制的带标签循环
    var attempts: usize = 0;
    // Label the outer while loop to enable breaking from nested for loop
    // 标记外层while循环以允许从嵌套for循环中断
    outer: while (attempts < grid.len) : (attempts += 1) {
        // Iterate through columns of current row with index capture
        // 使用索引捕获遍历当前行的列
        for (grid[attempts], 0..) |value, column| {
            // Check if target value is found
            // 检查是否找到目标值
            if (value == 4) {
                // Report location of target value
                // 报告目标值的位置
                std.debug.print(
                    "found target value at row {d}, column {d}\n",
                    .{ attempts, column },
                );
                // Break out of both loops using the outer label
                // 使用外部标签跳出两个循环
                break :outer;
            }
        }
    }
}
运行
Shell
$ zig run loop_labels.zig
输出
Shell
first all-even row: 1
found target value at row 1, column 1

while循环的else null捕获了“无匹配”的情况,而无需额外的状态,并且带标签的break :outer一旦找到目标就会立即退出两个循环。这种模式使状态处理保持紧凑,同时对控制转移保持明确。

循环如何降低到ZIR

循环被转换为带有显式break和continue目标的带标签块。这就是为什么带标签的break和循环else子句成为可能:

graph TB Loop["while/for"] --> LoopLabel["创建带标签的块"] LoopLabel --> Condition["生成循环条件"] Condition --> Body["生成循环体"] Body --> Continue["生成continue表达式"] LoopLabel --> BreakTarget["break_block 目标"] Body --> ContinueTarget["continue_block 目标"] Continue --> CondCheck["跳回条件"]

当你写outer: while (x < 10)时,编译器会创建:

  • break_blockbreak :outer语句的目标——退出循环
  • continue_blockcontinue :outer语句的目标——跳到下一次迭代
  • 循环体:包含你的代码,可以访问这两个目标

这就是为什么你可以嵌套循环并使用带标签的break来退出到特定级别的原因——每个循环标签在ZIR中都会创建自己的break_block。循环的else子句附加到break_block,并且只有在循环完成而没有中断时才执行。

用于穷尽决策的

switch详尽地检查值——涵盖字面量、范围和枚举——并且编译器强制执行完整性,除非你提供一个else分支。将switch与辅助函数结合使用是一种集中分类逻辑的干净方法。

Zig
// File: chapters-data/code/02__control-flow-essentials/switch_examples.zig

// Import the standard library for I/O operations
// 导入标准库用于I/O操作
const std = @import("std");

// Define an enum representing different compilation modes
// 定义表示不同编译模式的枚举
const Mode = enum { fast, safe, tiny };

/// Converts a numeric score into a descriptive text message.
/// 将数值分数转换为描述性文本消息。
/// Demonstrates switch expressions with ranges, multiple values, and catch-all cases.
/// 演示带范围、多个值和通配符案例的switch表达式。
/// Returns a string literal describing the score's progress level.
/// 返回描述分数进度级别的字符串字面量。
fn describeScore(score: u8) []const u8 {
    return switch (score) {
        0 => "no progress",           // Exact match for zero
        // 精确匹配零
        1...3 => "warming up",         // Range syntax: matches 1, 2, or 3
        // 范围语法:匹配1、2或3
        4, 5 => "halfway there",       // Multiple discrete values
        // 多个离散值
        6...9 => "almost done",        // Range: matches 6 through 9
        // 范围:匹配6到9
        10 => "perfect run",           // Maximum valid score
        // 最大有效分数
        else => "out of range",        // Catch-all for any other value
        // 通配符用于任何其他值
    };
}

pub fn main() !void {
    // Array of test scores to demonstrate switch behavior
    // 测试分数数组以演示switch行为
    const samples = [_]u8{ 0, 2, 5, 8, 10, 12 };

    // Iterate through each score and print its description
    // 遍历每个分数并打印其描述
    for (samples) |score| {
        std.debug.print("{d}: {s}\n", .{ score, describeScore(score) });
    }

    // Demonstrate switch with enum values
    // 演示带枚举值的switch
    const mode: Mode = .safe;

    // Switch on enum to assign different numeric factors based on mode
    // 根据模式对枚举进行switch以分配不同的数值因子
    // All enum cases must be handled (exhaustive matching)
    // 必须处理所有枚举情况(穷举匹配)
    const factor = switch (mode) {
        .fast => 32,  // Optimization for speed
        // 速度优化
        .safe => 16,  // Balanced mode
        // 平衡模式
        .tiny => 4,   // Optimization for size
        // 大小优化
    };

    // Print the selected mode and its corresponding factor
    // 打印选定的模式及其对应的因子
    std.debug.print("mode {s} -> factor {d}\n", .{ @tagName(mode), factor });
}
运行
Shell
$ zig run switch_examples.zig
输出
Shell
0: no progress
2: warming up
5: halfway there
8: almost done
10: perfect run
12: out of range
mode safe -> factor 16

每个switch都必须考虑所有可能性——一旦每个标签都被覆盖,编译器就会验证没有遗漏的情况。枚举消除了魔术数字,同时仍然允许你对编译时已知的变体进行分支。

表达式如何降低到ZIR

编译器将switch语句转换为一个结构化的块,该块详尽地处理所有情况。范围情况、每个分支的多个值和有效载荷捕获都在ZIR表示中进行编码:

graph TB Switch["switch (target) { ... }"] --> EvalTarget["评估目标操作数"] EvalTarget --> Prongs["处理switch分支"] Prongs --> Multi["每个分支多个情况"] Prongs --> Range["范围情况 (a...b)"] Prongs --> Capture["捕获有效载荷"] Multi --> SwitchBlock["生成switch_block"] Range --> SwitchBlock Capture --> SwitchBlock SwitchBlock --> ExtraData["存储在extra中:<br/>- 分支数量<br/>- case项<br/>- 分支体"]

穷尽性检查发生在语义分析期间(ZIR生成之后),此时类型是已知的。编译器验证:

  • 所有枚举标签都已覆盖(或存在else分支)
  • 整数范围不重叠
  • 不存在不可达的分支

这就是为什么你在对枚举进行switch时不能意外地忘记一个情况——类型系统在编译时确保了完整性。像0…​5这样的范围语法在ZIR中被编码为范围情况,而不是单个值。

工作流模式

结合这些结构可以解锁更具表现力的管道:循环收集或过滤数据,switch路由操作,循环标签使嵌套流保持精确,而无需引入可变的哨兵。本节将这些原语链接成可重用的模式,你可以将其应用于解析、模拟或状态机。

使用值的脚本处理

这个例子解释了一个迷你指令流,使用一个带标签的for循环来维持一个运行总数,并在达到阈值时停止。switch处理命令分派,包括在开发过程中出现未知标签时故意使用unreachable

Zig
// File: chapters-data/code/02__control-flow-essentials/script_runner.zig

// Demonstrates advanced control flow: switch expressions, labeled loops,
// and early termination based on threshold conditions
// 演示高级控制流:switch表达式、带标签循环和基于阈值条件的早期终止
const std = @import("std");

/// Enumeration of all possible action types in the script processor
// 脚本处理器中所有可能操作类型的枚举
const Action = enum { add, skip, threshold, unknown };

/// Represents a single processing step with an associated action and value
// 表示具有关联操作和值的单个处理步骤
const Step = struct {
    tag: Action,
    value: i32,
};

/// Contains the final state after script execution completes or terminates early
// 包含脚本执行完成或早期终止后的最终状态
const Outcome = struct {
    index: usize, // Step index where processing stopped
    // 处理停止的步骤索引
    total: i32,   // Accumulated total at termination
    // 终止时的累积总和
};

/// Maps single-character codes to their corresponding Action enum values.
/// 将单字符代码映射到其对应的Action枚举值。
/// Returns .unknown for unrecognized codes to maintain exhaustive handling.
/// 对无法识别的代码返回.unknown以保持穷举处理。
fn mapCode(code: u8) Action {
    return switch (code) {
        'A' => .add,
        'S' => .skip,
        'T' => .threshold,
        else => .unknown,
    };
}

/// Executes a sequence of steps, accumulating values and checking threshold limits.
/// 执行一系列步骤,累积值并检查阈值限制。
/// Processing stops early if a threshold step finds the total meets or exceeds the limit.
/// 如果阈值步骤发现总和达到或超过限制,则提前停止处理。
/// Returns an Outcome containing the stop index and final accumulated total.
/// 返回包含停止索引和最终累积总和的Outcome。
fn process(script: []const Step, limit: i32) Outcome {
    // Running accumulator for add operations
    // 用于add操作的运行累加器
    var total: i32 = 0;

    // for-else construct: break provides early termination value, else provides completion value
    // for-else结构:break提供早期终止值,else提供完成值
    const stop = outer: for (script, 0..) |step, index| {
        // Dispatch based on the current step's action type
        // 根据当前步骤的操作类型进行分派
        switch (step.tag) {
            // Add operation: accumulate the step's value to the running total
            // Add操作:将步骤的值累积到运行总和
            .add => total += step.value,
            // Skip operation: bypass this step without modifying state
            // Skip操作:跳过此步骤而不修改状态
            .skip => continue :outer,
            // Threshold check: terminate early if limit is reached or exceeded
            // 阈值检查:如果达到或超过限制则提前终止
            .threshold => {
                if (total >= limit) break :outer Outcome{ .index = index, .total = total };
                // Threshold not met: continue to next step
                // 未达到阈值:继续下一步
                continue :outer;
            },
            // Safety assertion: unknown actions should never appear in validated scripts
            // 安全断言:未知操作不应出现在已验证的脚本中
            .unknown => unreachable,
        }
    } else Outcome{ .index = script.len, .total = total }; // Normal completion after all steps
    // 所有步骤后的正常完成

    return stop;
}

pub fn main() !void {
    // Define a script sequence demonstrating all action types
    // 定义演示所有操作类型的脚本序列
    const script = [_]Step{
        .{ .tag = mapCode('A'), .value = 2 },  // Add 2 → total: 2
        // 加2 → 总计:2
        .{ .tag = mapCode('S'), .value = 0 },  // Skip (no effect)
        // 跳过(无效果)
        .{ .tag = mapCode('A'), .value = 5 },  // Add 5 → total: 7
        // 加5 → 总计:7
        .{ .tag = mapCode('T'), .value = 6 },  // Threshold check (7 >= 6: triggers early exit)
        // 阈值检查(7 >= 6:触发提前退出)
        .{ .tag = mapCode('A'), .value = 10 }, // Never executed due to early termination
        // 由于提前终止而从未执行
    };

    // Execute the script with a threshold limit of 6
    // 使用阈值限制6执行脚本
    const outcome = process(&script, 6);

    // Report where execution stopped and the final accumulated value
    // 报告执行停止的位置和最终累积值
    std.debug.print(
        "stopped at step {d} with total {d}\n",
        .{ outcome.index, outcome.total },
    );
}
运行
Shell
$ zig run script_runner.zig
输出
Shell
stopped at step 3 with total 7

break :outer返回一个完整的Outcome结构体,使得循环像一个搜索,要么找到它的目标,要么回退到循环的else。显式的unreachable为未来的贡献者记录了假设,并在调试构建中激活安全检查。

循环守卫和提前终止

有时数据本身会发出停止信号。这个演练识别第一个负数,然后累加偶数值,直到出现一个0哨兵,演示了循环的else子句、带标签的continue和常规的break

Zig
// File: chapters-data/code/02__control-flow-essentials/range_scan.zig

// Demonstrates while loops with labeled breaks and continue statements
// 演示带有标签中断和continue语句的while循环
const std = @import("std");

pub fn main() !void {
    // Sample data array containing mixed positive, negative, and zero values
    // 包含混合正数、负数和零值的样本数据数组
    const data = [_]i16{ 12, 5, 9, -1, 4, 0 };

    // Search for the first negative value in the array
    // 搜索数组中的第一个负值
    var index: usize = 0;
    // while-else construct: break provides value, else provides fallback
    // while-else结构:break提供值,else提供后备
    const first_negative = while (index < data.len) : (index += 1) {
        // Check if current element is negative
        // 检查当前元素是否为负
        if (data[index] < 0) break index;
    } else null; // No negative value found after scanning entire array
    // 扫描整个数组后未找到负值

    // Report the result of the negative value search
    // 报告负值搜索结果
    if (first_negative) |pos| {
        std.debug.print("first negative at index {d}\n", .{pos});
    } else {
        std.debug.print("no negatives in sequence\n", .{});
    }

    // Accumulate sum of even numbers until encountering zero
    // 累积偶数和直到遇到零
    var sum: i64 = 0;
    var count: usize = 0;

    // Label the loop to enable explicit break targeting
    // 标记循环以启用显式中断定位
    accumulate: while (count < data.len) : (count += 1) {
        const value = data[count];
        // Stop accumulation if zero is encountered
        // 如果遇到零则停止累积
        if (value == 0) {
            std.debug.print("encountered zero, breaking out\n", .{});
            break :accumulate;
        }
        // Skip odd values using labeled continue
        // 使用带标签的continue跳过奇数值
        if (@mod(value, 2) != 0) continue :accumulate;
        // Add even values to the running sum
        // 将偶数值加到运行总和
        sum += value;
    }

    // Display the accumulated sum of even values before zero
    // 显示零之前偶数值的累积和
    std.debug.print("sum of even prefix values = {d}\n", .{sum});
}
运行
Shell
$ zig run range_scan.zig
输出
Shell
first negative at index 3
encountered zero, breaking out
sum of even prefix values = 16

这两个循环展示了互补的退出风格:一个带有else默认值的循环表达式,以及一个带标签的循环,其中continuebreak明确了哪些迭代对运行总数有贡献。

注意与警告

  • 在任何有嵌套迭代的情况下,为了清晰起见,请优先使用带标签的循环;它使break/continue保持明确,并避免了哨兵变量。
  • switch必须保持详尽——如果你依赖else,请用注释或unreachable来记录不变量,这样未来的情况就不会被静默忽略。
  • 循环的else子句仅在循环自然退出时求值;请确保你的break路径返回值,以避免回退到非预期的默认值。

练习

  • 扩展branching.zig,增加第三个分支,以不同方式格式化大于100的值,确认if表达式仍然返回单个字符串。
  • 修改loop_labels.zig,通过break :outer返回确切的坐标作为结构体,然后在main中打印它们。
  • 修改script_runner.zig以在运行时解析字符(例如,从字节切片中),并添加一个重置总数的新命令,确保switch保持详尽。

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.