Switch 语句
switch
语句用于进行多分支匹配,并且要求覆盖所有可能的匹配情况。
基本使用
const num: u8 = 5;
switch (num) {
5 => {
print("this is 5\n", .{});
},
else => {
print("this is not 5\n", .{});
},
}
const std = @import("std");
const print = std.debug.print;
pub fn main() void {
const num: u8 = 5;
switch (num) {
5 => {
print("this is 5\n", .{});
},
else => {
print("this is not 5\n", .{});
},
}
}
🅿️ 提示
switch
语句的匹配必须穷尽所有可能的分支,或者包含一个 else
分支来处理未匹配的情况!
进阶使用
switch
还支持使用 ,
分割的多匹配、...
范围选择符、类似循环中的 tag
语法以及编译期表达式。以下是演示:
const a: u64 = 10;
const zz: u64 = 103;
// 作为表达式使用
const b = switch (a) {
// 多匹配项
1, 2, 3 => 0,
// 范围匹配
5...100 => 1,
// tag形式的分配匹配,可以任意复杂
101 => blk: {
const c: u64 = 5;
// 下一行代表返回到blk这个tag处
break :blk c * 2 + 1;
},
zz => zz,
// 支持编译期运算
blk: {
const d: u32 = 5;
const e: u32 = 100;
break :blk d + e;
} => 107,
// else 匹配剩余的分支
else => 9,
};
try expect(b == 1);
作为表达式使用
在 Zig 中,switch
语句还可以作为表达式使用:
const os_msg = switch (builtin.target.os.tag) {
.linux => "we found a linux user",
else => "not a linux user",
};
const builtin = @import("builtin");
pub fn main() void {
const os_msg = switch (builtin.target.os.tag) {
.linux => "we found a linux user",
else => "not a linux user",
};
_ = os_msg;
}
捕获 Tag Union
我们还可以使用 switch
对标记联合类型进行捕获操作。要修改字段值,可以在捕获变量名称之前放置 *
并将其转换为指针:
// 定义两个结构体
const Point = struct {
x: u8,
y: u8,
};
const Item = union(enum) {
a: u32,
c: Point,
d,
e: u32,
};
var a = Item{ .c = Point{ .x = 1, .y = 2 } };
const b = switch (a) {
// 多个匹配
Item.a, Item.e => |item| item,
// 可以使用 * 语法来捕获对应的指针进行修改操作
Item.c => |*item| blk: {
item.*.x += 1;
break :blk 6;
},
// 这里最后一个联合类型,匹配已经穷尽了,我们就不需要使用else了
Item.d => 8,
};
std.debug.print("{any}\n", .{b});
匹配和推断枚举
在使用 switch
匹配时,也可以继续对枚举类型进行自动推断:
const Color = enum {
auto,
off,
on,
};
const color = Color.off;
// 编译器会帮我们完成其余的工作
const result = switch (color) {
.auto => false,
.on => false,
.off => true,
};
内联 switch
switch
的分支可以标记为 inline
,以要求编译器生成该分支对应的所有可能情况:
// 这段函数用来判断一个结构体的字段是否是 optional,同时它也是 comptime 的
// 故我们可以在下面使用inline 来要求编译器帮我们展开这个switch
fn isFieldOptional(comptime T: type, field_index: usize) !bool {
const fields = @typeInfo(T).Struct.fields;
return switch (field_index) {
// 这里每次都是不同的值
inline 0...fields.len - 1 => |idx| {
return @typeInfo(fields[idx].type) == .Optional;
},
else => return error.IndexOutOfBounds,
};
}
inline else
可以展开所有的 else
分支。这样做的好处是,允许编译器在编译时显式生成所有分支,从而在编译时检查分支是否都能被正确处理:
const AnySlice = union(enum) {
a: u8,
b: i8,
c: bool,
d: []u8,
};
fn withSwitch(any: AnySlice) usize {
return switch (any) {
// 这里的 slice 可以匹配所有的 Anyslice 类型
inline else => |slice| _ = slice,
};
}
当使用 inline else
捕获 tag union
时,可以额外捕获标签(tag)和对应的值(value):
const U = union(enum) {
a: u32,
b: f32,
};
fn getNum(u: U) u32 {
switch (u) {
// 这里 num 是一个运行时可知的值
// 而 tag 则是对应的标签名,这是编译期可知的
inline else => |num, tag| {
if (tag == .b) {
return @intFromFloat(num);
}
return num;
},
}
}
标签化 switch
(Labeled Switch)
这是 0.14.0
版本引入的新特性。当 switch
语句带有标签时,它可以被 break
或 continue
语句引用。break
将从 switch
语句返回一个值。
针对 switch
的 continue
语句必须带有一个操作数。当执行时,它会跳转到匹配的分支,就像用 continue
的操作数替换了初始 switch
值后重新执行 switch
一样。
例如,以下两段代码的写法是等价的:
sw: switch (@as(i32, 5)) {
5 => continue :sw 4,
// `continue` can occur multiple times within a single switch prong.
2...4 => |v| {
if (v > 3) {
continue :sw 2;
} else if (v == 3) {
// `break` can target labeled loops.
break :sw;
}
continue :sw 1;
},
1 => return,
else => unreachable,
}
var sw: i32 = 5;
while (true) {
switch (sw) {
5 => {
sw = 4;
continue;
},
2...4 => |v| {
if (v > 3) {
sw = 2;
continue;
} else if (v == 3) {
break;
}
sw = 1;
continue;
},
1 => return,
else => unreachable,
}
}
这可以提高(例如)状态机的清晰度,其中 continue :sw .next_state
这样的语法是明确、清晰且易于理解的。
这个设计的目的是处理对数组中每个元素进行 switch
判断的情况。在这种情况下,使用单个 switch
语句可以提高代码的清晰度和性能:
const Instruction = enum {
add,
mul,
end,
};
fn evaluate(initial_stack: []const i32, code: []const Instruction) !i32 {
const std = @import("std");
var stack = try std.BoundedArray(i32, 8).fromSlice(initial_stack);
var ip: usize = 0;
return vm: switch (code[ip]) {
// Because all code after `continue` is unreachable, this branch does
// not provide a result.
.add => {
try stack.append(stack.pop().? + stack.pop().?);
ip += 1;
continue :vm code[ip];
},
.mul => {
try stack.append(stack.pop().? * stack.pop().?);
ip += 1;
continue :vm code[ip];
},
.end => stack.pop().?,
};
}
如果 continue
的操作数在编译时就已知,那么它可以被简化为一个无条件分支指令,直接跳转到相关的 case
。这种分支是完全可预测的,因此通常执行速度很快。
如果操作数在运行时才能确定,每个 continue
可以内联嵌入一个条件分支(理想情况下通过跳转表实现),这使得 CPU 可以独立于其他分支来预测其目标。相比之下,基于循环的简化实现会迫使所有分支都通过同一个分发点,这会妨碍分支预测。