概述
第1章建立了运行Zig程序和处理数据的基础;现在我们将这些值转化为决策,通过遍历语言的流程控制原语,如#if中所述。Zig中的控制流是面向表达式的,因此选择分支或循环通常会产生一个值,而不仅仅是指导执行。
我们探索循环、标签流和switch背后的语义,强调break、continue和else子句如何在安全和发布构建中传达意图;参见#While、#for和#switch。
学习目标
- 使用
if表达式(带可选的有效载荷捕获)在显式处理缺失数据路径的同时派生值。 - 将
while/for循环与带标签的break/continue结合,以清晰管理嵌套迭代和退出条件。 - 应用
switch来枚举详尽的决策表,包括范围、多个值和枚举。 - 利用循环
else子句和带标签的break直接从迭代构造返回值。
控制流代码的处理过程
在深入控制流语法之前,了解编译器如何处理你的if、while和switch语句是有帮助的。Zig通过多个中间表示(IR)转换源代码,每个都有特定的目的:
| IR阶段 | 表示形式 | 关键特性 | 控制流目的 |
|---|---|---|---|
| Tokens | 扁平令牌流 | 原始词法分析 | 识别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
// 处理空值情况
}
pub fn main() !void {
// Array containing both present and absent (null) values
// 包含存在值和空值的数组
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子句配对来直接传递值,该子句在没有break的情况下执行完成时触发。带标签的循环(outer: while (…))协调嵌套迭代,因此你可以提前退出或跳过工作,而无需临时布尔值。
// 文件路径: chapters-data/code/02__control-flow-essentials/loop_labels.zig
// 演示Zig中的带标签循环和while-else结构
const std = @import("std");
/// 查找第一个两元素都为偶数的行
/// 使用while循环和continue语句跳过无效行
/// 返回匹配行的基于零的索引,如果未找到则返回null
fn findAllEvenPair(rows: []const [2]i32) ?usize {
// 在迭代期间跟踪当前行索引
var row: usize = 0;
// while-else结构:break提供值,else提供回退
const found = while (row < rows.len) : (row += 1) {
// 提取当前对进行检查
const pair = rows[row];
// 如果第一个元素是奇数则跳过该行
if (@mod(pair[0], 2) != 0) continue;
// 如果第二个元素是奇数则跳过该行
if (@mod(pair[1], 2) != 0) continue;
// 两个元素都是偶数:返回此行的索引
break row;
} else null; // 耗尽所有行后未找到匹配行
return found;
}
pub fn main() !void {
// 测试数据,包含混合奇偶值的整数对
const grid = [_][2]i32{
.{ 3, 7 }, // 两者都是奇数
.{ 2, 4 }, // 两者都是偶数(目标)
.{ 5, 6 }, // 混合
};
// 查找第一个全偶数对并报告结果
if (findAllEvenPair(&grid)) |row| {
std.debug.print("first all-even row: {d}\n", .{row});
} else {
std.debug.print("no all-even rows\n", .{});
}
// 演示用于多级break控制的带标签循环
var attempts: usize = 0;
// 为外部while循环添加标签以启用从嵌套for循环跳出
outer: while (attempts < grid.len) : (attempts += 1) {
// 遍历当前行的列并捕获索引
for (grid[attempts], 0..) |value, column| {
// 检查是否找到目标值
if (value == 4) {
// 报告目标值的位置
std.debug.print(
"found target value at row {d}, column {d}\n",
.{ attempts, column },
);
// 使用外部标签跳出两个循环
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,并且仅在循环完成而没有break时执行。
用于详尽决策
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
// 基于枚举值切换,根据模式分配不同的数值因子
// 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。
// 文件路径: chapters-data/code/02__control-flow-essentials/script_runner.zig
// 演示高级控制流:switch表达式、带标签循环
// 和基于阈值条件的早期终止
const std = @import("std");
/// 脚本处理器中所有可能操作类型的枚举
const Action = enum { add, skip, threshold, unknown };
/// 表示单个处理步骤,包含相关操作和值
const Step = struct {
tag: Action,
value: i32,
};
/// 包含脚本执行完成或提前终止后的最终状态
const Outcome = struct {
index: usize, // 处理停止的步骤索引
total: i32, // 终止时的累积总值
};
/// 将单字符代码映射到对应的Action枚举值
/// 对于无法识别的代码返回.unknown以保持穷举处理
fn mapCode(code: u8) Action {
return switch (code) {
'A' => .add,
'S' => .skip,
'T' => .threshold,
else => .unknown,
};
}
/// 执行步骤序列,累积值并检查阈值限制
/// 如果阈值步骤发现总值达到或超过限制,则提前停止处理
/// 返回包含停止索引和最终累积总值的Outcome
fn process(script: []const Step, limit: i32) Outcome {
// 加法操作的运行累积器
var total: i32 = 0;
// for-else结构:break提供早期终止值,else提供完成值
const stop = outer: for (script, 0..) |step, index| {
// 根据当前步骤的操作类型分派
switch (step.tag) {
// 加法操作:将步骤的值累积到运行总值
.add => total += step.value,
// 跳过操作:绕过此步骤而不修改状态
.skip => continue :outer,
// 阈值检查:如果达到或超过限制则提前终止
.threshold => {
if (total >= limit) break :outer Outcome{ .index = index, .total = total };
// 未达到阈值:继续下一步
continue :outer;
},
// 安全断言:未知操作不应出现在已验证的脚本中
.unknown => unreachable,
}
} else Outcome{ .index = script.len, .total = total }; // 所有步骤后正常完成
return stop;
}
pub fn main() !void {
// 定义演示所有操作类型的脚本序列
const script = [_]Step{
.{ .tag = mapCode('A'), .value = 2 }, // 加2 → 总计: 2
.{ .tag = mapCode('S'), .value = 0 }, // 跳过(无效果)
.{ .tag = mapCode('A'), .value = 5 }, // 加5 → 总计: 7
.{ .tag = mapCode('T'), .value = 6 }, // 阈值检查 (7 >= 6: 触发早期退出)
.{ .tag = mapCode('A'), .value = 10 }, // 由于早期终止而永不执行
};
// 使用阈值限制6执行脚本
const outcome = process(&script, 6);
// 报告执行停止的位置和最终累积值
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。
// 文件路径: chapters-data/code/02__control-flow-essentials/range_scan.zig
// 演示带标签break和continue语句的while循环
const std = @import("std");
pub fn main() !void {
// 示例数据数组,包含混合的正数、负数和零值
const data = [_]i16{ 12, 5, 9, -1, 4, 0 };
// 搜索数组中的第一个负值
var index: usize = 0;
// while-else结构:break提供值,else提供回退
const first_negative = while (index < data.len) : (index += 1) {
// 检查当前元素是否为负数
if (data[index] < 0) break index;
} else null; // 扫描整个数组后未找到负值
// 报告负值搜索结果
if (first_negative) |pos| {
std.debug.print("first negative at index {d}\n", .{pos});
} else {
std.debug.print("no negatives in sequence\n", .{});
}
// 累积偶数之和直到遇到零
var sum: i64 = 0;
var count: usize = 0;
// 为循环添加标签以启用显式break定位
accumulate: while (count < data.len) : (count += 1) {
const value = data[count];
// 遇到零时停止累积
if (value == 0) {
std.debug.print("encountered zero, breaking out\n", .{});
break :accumulate;
}
// 使用带标签的continue跳过奇数值
if (@mod(value, 2) != 0) continue :accumulate;
// 将偶数值加到运行总和
sum += value;
}
// 显示零之前的偶数前缀值的累积和
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保持详尽性。