概述
第一章为运行Zig程序和处理数据奠定了基础;现在我们通过遍历语言的控制流原语,将这些值转化为决策,具体如#if中所述。Zig中的控制流是面向表达式的,因此选择一个分支或循环通常会产生一个值,而不仅仅是引导执行。
我们探讨了循环、带标签的流和switch背后的语义,强调了break、continue和else子句如何在安全和发布构建中传达意图;参见#while、#for和#switch。
学习目标
- 使用
if表达式(带有可选的有效载荷捕获)来派生值,同时明确处理缺失的数据路径。 - 将
while/for循环与带标签的break/continue结合使用,以清晰地管理嵌套迭代和退出条件。 - 应用
switch来枚举详尽的决策表,包括范围、多个值和枚举。 - 利用循环的
else子句和带标签的break直接从迭代结构中返回值。
控制流代码会发生什么
在深入学习控制流语法之前,了解编译器如何处理你的if、while和switch语句是很有帮助的。Zig通过多个中间表示(IRs)转换源代码,每个IR都有其特定的目的:
| IR阶段 | 表示 | 关键属性 | 控制流的目的 |
|---|---|---|---|
| 令牌 | 扁平令牌流 | 原始词法分析 | 识别if、while、switch关键字 |
| AST | 树形结构 | 语法正确,未类型化 | 保留嵌套控制流的结构 |
| ZIR | 基于指令的IR | 未类型化,每个声明一个SSA形式 | 将控制流降低为块和分支 |
| AIR | 基于指令的IR | 完全类型化,每个函数一个SSA形式 | 带有已知结果的类型检查分支 |
| MIR | 后端特定的IR | 接近机器码,已寄存器分配 | 转换为跳转和条件指令 |
你编写的控制流结构——if表达式、switch语句、带标签的循环——都会系统地通过这些阶段进行降低。当你的代码到达机器码时,一个switch已经变成了一个跳转表,而一个while循环则是一个条件分支指令。本章中的图表展示了这种降低在ZIR阶段是如何发生的,此时控制流变成了显式的块和分支。
核心控制结构
Zig中的控制流将块和循环视为表达式,这意味着每个结构都可以产生一个值,并直接参与赋值或返回语句。本节将逐步介绍条件、循环和switch,展示它们如何融入表达式模型,同时保持高度的可读性,具体如#代码块中所述。
作为表达式的条件
if的求值结果是其运行分支的值,而可选的捕获形式(if (opt) |value|)是一种简洁的方式来解包可选值,而不会遮蔽早期的名称。嵌套的带标签块(blk: { … })让你可以在多个结果中进行选择,同时仍然返回单个值。
// 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 });
}
}
$ zig run branching.zigsample 0: positive
sample 1: zero
sample 2: missing
sample 3: negative该函数返回一个[]const u8,因为if表达式本身产生了字符串,这强调了面向表达式的分支如何使调用点保持紧凑。samples循环显示,for可以与索引元组(item, index)一起迭代,但仍然依赖于上游表达式来格式化输出。
if-else表达式如何降低到ZIR
当编译器遇到一个if表达式时,它会将其转换为ZIR(Zig中间表示)中的块和条件分支。确切的降低方式取决于是否需要结果位置;参见结果位置:
当你写const result = if (x > 0) "positive" else "negative"时,编译器会创建两个块(每个分支一个),并使用break语句返回所选的值。这就是为什么if表达式可以参与赋值——它们被编译成通过其break语句产生值的块。
带标签的while和for循环
Zig中的循环可以通过将break结果与循环的else子句配对来直接传递值,当执行完成而没有中断时,该子句就会触发。带标签的循环(outer: while (…))协调嵌套迭代,这样你就可以提前退出或跳过工作,而无需使用临时布尔值。
// 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;
}
}
}
}
$ zig run loop_labels.zigfirst all-even row: 1
found target value at row 1, column 1while循环的else null捕获了“无匹配”的情况,而无需额外的状态,并且带标签的break :outer一旦找到目标就会立即退出两个循环。这种模式使状态处理保持紧凑,同时对控制转移保持明确。
循环如何降低到ZIR
循环被转换为带有显式break和continue目标的带标签块。这就是为什么带标签的break和循环else子句成为可能:
当你写outer: while (x < 10)时,编译器会创建:
- break_block:
break :outer语句的目标——退出循环 - continue_block:
continue :outer语句的目标——跳到下一次迭代 - 循环体:包含你的代码,可以访问这两个目标
这就是为什么你可以嵌套循环并使用带标签的break来退出到特定级别的原因——每个循环标签在ZIR中都会创建自己的break_block。循环的else子句附加到break_block,并且只有在循环完成而没有中断时才执行。
用于穷尽决策的
switch详尽地检查值——涵盖字面量、范围和枚举——并且编译器强制执行完整性,除非你提供一个else分支。将switch与辅助函数结合使用是一种集中分类逻辑的干净方法。
// 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 });
}
$ zig run switch_examples.zig0: 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表示中进行编码:
穷尽性检查发生在语义分析期间(ZIR生成之后),此时类型是已知的。编译器验证:
- 所有枚举标签都已覆盖(或存在
else分支) - 整数范围不重叠
- 不存在不可达的分支
这就是为什么你在对枚举进行switch时不能意外地忘记一个情况——类型系统在编译时确保了完整性。像0…5这样的范围语法在ZIR中被编码为范围情况,而不是单个值。
工作流模式
结合这些结构可以解锁更具表现力的管道:循环收集或过滤数据,switch路由操作,循环标签使嵌套流保持精确,而无需引入可变的哨兵。本节将这些原语链接成可重用的模式,你可以将其应用于解析、模拟或状态机。
使用值的脚本处理
这个例子解释了一个迷你指令流,使用一个带标签的for循环来维持一个运行总数,并在达到阈值时停止。switch处理命令分派,包括在开发过程中出现未知标签时故意使用unreachable。
// 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 },
);
}
$ zig run script_runner.zigstopped at step 3 with total 7break :outer返回一个完整的Outcome结构体,使得循环像一个搜索,要么找到它的目标,要么回退到循环的else。显式的unreachable为未来的贡献者记录了假设,并在调试构建中激活安全检查。
循环守卫和提前终止
有时数据本身会发出停止信号。这个演练识别第一个负数,然后累加偶数值,直到出现一个0哨兵,演示了循环的else子句、带标签的continue和常规的break。
// 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});
}
$ zig run range_scan.zigfirst negative at index 3
encountered zero, breaking out
sum of even prefix values = 16这两个循环展示了互补的退出风格:一个带有else默认值的循环表达式,以及一个带标签的循环,其中continue和break明确了哪些迭代对运行总数有贡献。
注意与警告
- 在任何有嵌套迭代的情况下,为了清晰起见,请优先使用带标签的循环;它使
break/continue保持明确,并避免了哨兵变量。 switch必须保持详尽——如果你依赖else,请用注释或unreachable来记录不变量,这样未来的情况就不会被静默忽略。- 循环的
else子句仅在循环自然退出时求值;请确保你的break路径返回值,以避免回退到非预期的默认值。
练习
- 扩展
branching.zig,增加第三个分支,以不同方式格式化大于100的值,确认if表达式仍然返回单个字符串。 - 修改
loop_labels.zig,通过break :outer返回确切的坐标作为结构体,然后在main中打印它们。 - 修改
script_runner.zig以在运行时解析字符(例如,从字节切片中),并添加一个重置总数的新命令,确保switch保持详尽。