承接前文

在前两篇文章中,我们学习了 Zig 的基础知识:变量与数据类型、数组与字符串,以及使用 defer 进行内存管理。掌握了这些基础概念后,今天我们就来探索 Zig 的**控制流(Control Flow)**机制。

控制流是编程语言的核心,它决定了程序的执行路径。Zig 提供了多种控制流结构:

  • if - 条件判断
  • switch - 多分支选择
  • while - 条件循环
  • for - 迭代循环

与许多现代语言不同,Zig 的控制流设计遵循"显式优于隐式"的哲学,没有隐式的类型转换或意外的行为。让我们逐一深入了解。

if 表达式

基本用法

Zig 的 if 是一个表达式,这意味着它可以返回值。基本条件判断如下:

1
2
3
4
5
6
7
8
9
10
11
const std = @import("std");

pub fn main() void {
const age: u8 = 18;

if (age >= 18) {
std.debug.print("成年人\n", .{});
} else {
std.debug.print("未成年人\n", .{});
}
}

if 作为表达式

既然 if 是表达式,我们就可以用它来为变量赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const std = @import("std");

pub fn main() void {
const score: u8 = 85;

// if 表达式返回值
const grade = if (score >= 90) {
'A'
} else if (score >= 80) {
'B'
} else if (score >= 60) {
'C'
} else {
'D'
};

std.debug.print("等级: {c}\n", .{grade});
}

注意:作为表达式使用时,if 的所有分支必须返回相同类型的值。

可选类型与 if

Zig 没有 null,而是使用可选类型(Optional Types)?T 来表示可能不存在的值。结合 if 可以安全地处理可选值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const std = @import("std");

pub fn main() void {
const maybe_value: ?i32 = 42;

// 使用 if 解构可选类型
if (maybe_value) |value| {
std.debug.print("值存在: {d}\n", .{value});
} else {
std.debug.print("值不存在\n", .{});
}

// 空值的情况
const empty: ?i32 = null;
if (empty) |value| {
std.debug.print("值: {d}\n", .{value});
} else {
std.debug.print("空值\n", .{});
}
}

使用 if (optional) |value| { ... } 的语法,当可选值存在时,value 会被赋值为解包后的值。

错误联合类型与 if

类似地,if 也可以处理错误联合类型 !T

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const std = @import("std");

fn mayFail() !i32 {
return 42;
// 或者返回错误: return error.SomeError;
}

pub fn main() !void {
const result = mayFail();

// 使用 if 解构错误联合类型
if (result) |value| {
std.debug.print("成功: {d}\n", .{value});
} else |err| {
std.debug.print("错误: {s}\n", .{@errorName(err)});
}
}

switch 表达式

基本用法

switch 是 Zig 中强大的多分支选择结构,它也是一个表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const std = @import("std");

pub fn main() void {
const number: u8 = 3;

const result = switch (number) {
1 => "一",
2 => "二",
3 => "三",
4 => "四",
5 => "五",
else => "其他",
};

std.debug.print("{s}\n", .{result});
}

重要switch 必须覆盖所有可能的情况,因此通常需要 else 分支。

范围匹配

Zig 的 switch 支持范围表达式,使用 ... 运算符创建包含两端点的范围:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const std = @import("std");

pub fn main() void {
const level: u8 = 45;

const category = switch (level) {
0...25 => "初学者",
26...75 => "中级",
76...100 => "专家",
else => {
@panic("不支持的等级!");
},
};

std.debug.print("等级 {d} 属于: {s}\n", .{ level, category });
}

范围匹配也适用于字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const std = @import("std");

fn getCharType(c: u8) []const u8 {
return switch (c) {
'a'...'z' => "小写字母",
'A'...'Z' => "大写字母",
'0'...'9' => "数字",
else => "其他字符",
};
}

pub fn main() void {
std.debug.print("'a' 是 {s}\n", .{getCharType('a')});
std.debug.print("'5' 是 {s}\n", .{getCharType('5')});
std.debug.print("'@' 是 {s}\n", .{getCharType('@')});
}

多值匹配

可以在一个分支中匹配多个值:

1
2
3
4
5
6
7
8
9
10
11
12
13
const std = @import("std");

pub fn main() void {
const day: u8 = 6;

const dayType = switch (day) {
1, 2, 3, 4, 5 => "工作日",
6, 7 => "周末",
else => "无效的日期",
};

std.debug.print("星期{d} 是 {s}\n", .{ day, dayType });
}

switch 与枚举

switch 与枚举类型配合使用时,如果覆盖了所有枚举值,可以省略 else

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const std = @import("std");

const Color = enum {
red,
green,
blue,
};

pub fn main() void {
const color = Color.green;

const name = switch (color) {
.red => "红色",
.green => "绿色",
.blue => "蓝色",
// 不需要 else,因为所有枚举值都已覆盖
};

std.debug.print("颜色: {s}\n", .{name});
}

while 循环

基本 while 循环

while 是 Zig 中最灵活的循环结构:

1
2
3
4
5
6
7
8
9
10
11
const std = @import("std");

pub fn main() void {
var i: u8 = 0;

while (i < 5) {
std.debug.print("{d} ", .{i});
i += 1;
}
// 输出: 0 1 2 3 4
}

带 continue 表达式的 while

Zig 的 while 支持** continue 表达式**,在每次迭代结束时执行:

1
2
3
4
5
6
7
8
9
10
11
const std = @import("std");

pub fn main() void {
var i: u8 = 0;

// 传统 for 循环的风格:while (条件) : (更新)
while (i < 5) : (i += 1) {
std.debug.print("{d} ", .{i});
}
// 输出: 0 1 2 3 4
}

这种语法 while (条件) : (继续表达式)while 可以模拟传统 C 风格的 for 循环。

while 与可选类型

while 也可以用于解构可选类型,实现"当值存在时继续循环"的模式:

1
2
3
4
5
6
7
8
9
10
11
12
const std = @import("std");

pub fn main() void {
var maybe_num: ?i32 = 5;

// 当 maybe_num 不为 null 时继续循环
while (maybe_num) |num| {
std.debug.print("{d} ", .{num});
maybe_num = if (num > 0) num - 1 else null;
}
// 输出: 5 4 3 2 1 0
}

while 与错误联合类型

类似地,while 可以处理可能返回错误的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const std = @import("std");

var counter: i32 = 0;

fn mayReturnError() !i32 {
counter += 1;
if (counter > 3) return error.TooMany;
return counter;
}

pub fn main() !void {
// 当操作成功时继续
while (mayReturnError()) |value| {
std.debug.print("成功: {d}\n", .{value});
} else |err| {
std.debug.print("错误: {s}\n", .{@errorName(err)});
}
}

for 循环

基本用法

Zig 的 for 循环专门用于迭代,与 while 的职责清晰分离:

1
2
3
4
5
6
7
8
9
10
11
const std = @import("std");

pub fn main() void {
const items = [_]i32{ 10, 20, 30, 40, 50 };

// 迭代数组
for (items) |item| {
std.debug.print("{d} ", .{item});
}
// 输出: 10 20 30 40 50
}

获取索引

使用 for (items) |item, index| 可以同时获取元素和索引:

1
2
3
4
5
6
7
8
9
const std = @import("std");

pub fn main() void {
const fruits = [_][]const u8{ "苹果", "香蕉", "橙子" };

for (fruits) |fruit, i| {
std.debug.print("[{d}] {s}\n", .{ i, fruit });
}
}

迭代切片

for 循环也可以迭代切片:

1
2
3
4
5
6
7
8
9
10
11
const std = @import("std");

pub fn main() void {
const arr = [_]u8{ 1, 2, 3, 4, 5 };
const slice = arr[1..4]; // {2, 3, 4}

for (slice) |item| {
std.debug.print("{d} ", .{item});
}
// 输出: 2 3 4
}

迭代字符串

迭代字符串时,我们得到的是字节:

1
2
3
4
5
6
7
8
9
10
const std = @import("std");

pub fn main() void {
const text = "Hello";

for (text) |byte| {
std.debug.print("{c} ", .{byte});
}
// 输出: H e l l o
}

使用范围迭代

Zig 提供了 0..n 语法来创建范围,用于迭代指定次数:

1
2
3
4
5
6
7
8
9
const std = @import("std");

pub fn main() void {
// 迭代 0 到 4(不包含 5)
for (0..5) |i| {
std.debug.print("{d} ", .{i});
}
// 输出: 0 1 2 3 4
}

注意0..5 创建的区间是左闭右开的,包含 0 但不包含 5。

循环控制:break 与 continue

break

break 用于立即退出循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
const std = @import("std");

pub fn main() void {
var i: u8 = 0;

while (true) : (i += 1) {
if (i >= 5) {
break; // 退出循环
}
std.debug.print("{d} ", .{i});
}
// 输出: 0 1 2 3 4
}

break 也可以带返回值(类似带标签的代码块):

1
2
3
4
5
6
7
8
9
10
11
const std = @import("std");

pub fn main() void {
const result = for (0..10) |i| {
if (i * i > 30) {
break i; // 返回第一个使 i*i > 30 的 i
}
} else 10; // 如果没有 break,返回 10

std.debug.print("结果: {d}\n", .{result}); // 输出: 6
}

continue

continue 用于跳过当前迭代,进入下一次循环:

1
2
3
4
5
6
7
8
9
10
11
const std = @import("std");

pub fn main() void {
for (0..10) |i| {
if (i % 2 == 0) {
continue; // 跳过偶数
}
std.debug.print("{d} ", .{i});
}
// 输出: 1 3 5 7 9
}

带标签的循环

与代码块一样,循环也可以添加标签,用于在嵌套循环中精确控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
const std = @import("std");

pub fn main() void {
outer: for (0..3) |i| {
for (0..3) |j| {
if (i * j > 2) {
break :outer; // 跳出外层循环
}
std.debug.print("({d},{d}) ", .{ i, j });
}
}
// 输出: (0,0) (0,1) (0,2) (1,0) (1,1) (1,2) (2,0) (2,1)
}

Ziglings 练习

通过以下练习巩固控制流的知识:

练习 009-011:if 表达式

1
2
3
4
5
6
7
8
9
10
// 练习 009:基本的 if 表达式
const std = @import("std");

pub fn main() void {
const discount = true;

const price: u8 = if (discount) 75 else 100;

std.debug.print("价格: {d}\n", .{price});
}
1
2
3
4
5
6
7
8
9
10
11
12
// 练习 010:if 与可选类型
const std = @import("std");

pub fn main() void {
const maybe_num: ?u32 = 1729;

if (maybe_num) |num| {
std.debug.print("数字是: {d}\n", .{num});
} else {
std.debug.print("没有数字\n", .{});
}
}

练习 012-014:while 循环

1
2
3
4
5
6
7
8
9
10
// 练习 012:带 continue 表达式的 while
const std = @import("std");

pub fn main() void {
var i: u8 = 0;

while (i < 5) : (i += 1) {
std.debug.print("{d}", .{i});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 练习 013:while 与可选类型
const std = @import("std");

pub fn main() void {
var n: ?u32 = 3;

while (n) |value| {
std.debug.print("{d}... ", .{value});
n = if (value > 0) value - 1 else null;
}
std.debug.print("发射!\n", .{});
}
// 输出: 3... 2... 1... 0... 发射!

练习 015-017:for 循环

1
2
3
4
5
6
7
8
9
10
// 练习 015:基本的 for 循环
const std = @import("std");

pub fn main() void {
const animals = [_][]const u8{ "狗", "猫", "鸟" };

for (animals) |animal| {
std.debug.print("{s} ", .{animal});
}
}
1
2
3
4
5
6
7
8
9
10
// 练习 016:带索引的 for 循环
const std = @import("std");

pub fn main() void {
const nums = [_]u8{ 2, 4, 6, 8 };

for (nums) |n, i| {
std.debug.print("[{d}] {d}\n", .{ i, n });
}
}
1
2
3
4
5
6
7
8
9
// 练习 017:范围迭代
const std = @import("std");

pub fn main() void {
// 打印 1 到 5
for (1..6) |i| {
std.debug.print("{d} ", .{i});
}
}

总结

本文我们深入学习了 Zig 的控制流机制:

  1. if 表达式

    • 可以作为表达式返回值
    • 支持可选类型解构 if (optional) |value|
    • 支持错误联合类型解构 if (result) |value| else |err|
  2. switch 表达式

    • 必须覆盖所有情况
    • 支持范围匹配 0...100
    • 支持多值匹配 1, 2, 3
    • 与枚举配合时可省略 else
  3. while 循环

    • 基本形式:while (条件) { ... }
    • 带 continue 表达式:while (条件) : (更新) { ... }
    • 支持可选类型和错误联合类型解构
  4. for 循环

    • 专用于迭代:for (items) |item|
    • 可获取索引:for (items) |item, index|
    • 范围迭代:for (0..n) |i|
  5. 循环控制

    • break:退出循环,可带返回值
    • continue:跳过当前迭代
    • 带标签的循环:用于嵌套循环控制

Zig 的控制流设计体现了语言的核心理念:清晰、显式、无意外。每个结构都有明确的用途,没有隐式的类型转换或魔术行为。这种设计虽然代码量可能稍多,但大大降低了出错的可能性,让代码更易读、易维护。

在下一篇文章中,我们将探索 Zig 的函数定义、参数传递、错误处理等高级特性,继续我们的 Zig 学习之旅!