简介 §
Zig 是一种通用编程语言和工具链,用于维护健壮、优化和可重用的软件。
- 健壮
- 即使在边缘情况(如内存不足)下,行为也是正确的。
- 优化
- 以最佳方式编写程序,使其行为和性能达到最优。
- 可重用
- 相同的代码可以在具有不同约束的多种环境中工作。
- 可维护
- 向编译器和其他程序员精确传达意图。该语言在阅读代码时施加的开销很低,并且对不断变化的需求和环境具有弹性。
通常,学习新事物最有效的方法是查看示例,因此本文档展示了如何使用 Zig 的每个特性。它全部在一个页面上,因此您可以使用浏览器的搜索工具进行搜索。
本文档中的代码示例作为 Zig 主测试套件的一部分进行编译和测试。
此 HTML 文档不依赖于任何外部文件,因此您可以离线使用它。
翻译:WaterRun,使用 Claude Sonnet 4.5
翻译日期:2025年12月11日
更新日期:2025年12月17日
Zig 标准库 §
Zig 标准库有自己的文档。
Zig 标准库包含常用的算法、数据结构和定义,可帮助您构建程序或库。您将在本文档中看到许多使用 Zig 标准库的示例。要了解有关 Zig 标准库的更多信息,请访问上面的链接。
或者,每个 Zig 发行版都提供 Zig 标准库文档。可以通过本地 Web 服务器渲染它:
zig std
Hello World §
const std = @import("std");
pub fn main() !void {
try std.fs.File.stdout().writeAll("Hello, World!\n");
}
$ zig build-exe hello.zig $ ./hello Hello, World!
大多数时候,写入 stderr 而不是 stdout 更合适,并且消息是否成功写入流并不相关。此外,格式化打印通常也很方便。对于这种常见情况,有一个更简单的 API:
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, {s}!\n", .{"World"});
}
$ zig build-exe hello_again.zig $ ./hello_again Hello, World!
在这种情况下,可以从 main 的返回类型中省略
!,因为该函数不会返回错误。
另见:
注释 §
Zig 支持 3 种注释类型。普通注释会被忽略,但文档注释和顶级文档注释被编译器用于生成包文档。
生成的文档仍处于实验阶段,可以使用以下命令生成:
zig test -femit-docs main.zig
const print = @import("std").debug.print;
pub fn main() void {
// Zig 中的注释以 "//" 开头,在下一个 LF 字节(行尾)结束。
// 下面的行是注释,不会被执行。
//print("Hello?", .{});
print("Hello, world!\n", .{}); // 另一个注释
}
$ zig build-exe comments.zig $ ./comments Hello, world!
Zig 中没有多行注释(例如 C 中的
/* */ 注释)。这使得 Zig
具有这样的特性:每行代码都可以在没有上下文的情况下进行标记化。
文档注释 §
文档注释是以恰好三个斜杠开头的注释(即
/// 但不是
////);连续的多个文档注释会合并在一起形成多行文档注释。文档注释记录紧随其后的内容。
/// 用于存储时间戳的结构,具有纳秒精度(这是一个
/// 多行文档注释)。
const Timestamp = struct {
/// 自纪元以来的秒数(这也是一个文档注释)。
seconds: i64, // 有符号,所以我们可以表示 1970 年之前(不是文档注释)
/// 超过秒数的纳秒数(又是文档注释)。
nanos: u32,
/// 返回表示 Unix 纪元的 `Timestamp` 结构;即
/// 1970 年 1 月 1 日 00:00:00 UTC 的时刻(这也是文档注释)。
pub fn unixEpoch() Timestamp {
return Timestamp{
.seconds = 0,
.nanos = 0,
};
}
};
文档注释只允许在某些位置;在意外位置出现文档注释是编译错误,例如在表达式的中间,或在非文档注释之前。
/// 文档注释
//! 顶级文档注释
const std = @import("std");
$ zig build-obj invalid_doc-comment.zig /home/andy/dev/zig/doc/langref/invalid_doc-comment.zig:1:16: error: 期望类型表达式,发现 '文档注释' /// 文档注释 ^
pub fn main() void {}
/// 文件结束
$ zig build-obj unattached_doc-comment.zig /home/andy/dev/zig/doc/langref/unattached_doc-comment.zig:3:1: error: 未附加的文档注释 /// 文件结束 ^~~~~~~~~~~~~~~
文档注释可以与普通注释交错。目前,在生成包文档时,普通注释会与文档注释合并。
顶级文档注释 §
顶级文档注释是以两个斜杠和一个感叹号开头的注释://!;它记录当前模块。
如果顶级文档注释未放置在容器的开头、任何表达式之前,则是编译错误。
//! 此模块提供用于检索当前日期和
//! 时间的函数,具有不同程度的精度和准确性。它不
//! 依赖于 libc,但如果可用,将使用其中的函数。
const S = struct {
//! 顶级注释允许出现在模块以外的容器中,
//! 但这并不是很有用。目前,在生成包
//! 文档时,这些注释会被忽略。
};
值 §
// 顶级声明与顺序无关:
const print = std.debug.print;
const std = @import("std");
const os = std.os;
const assert = std.debug.assert;
pub fn main() void {
// 整数
const one_plus_one: i32 = 1 + 1;
print("1 + 1 = {}\n", .{one_plus_one});
// 浮点数
const seven_div_three: f32 = 7.0 / 3.0;
print("7.0 / 3.0 = {}\n", .{seven_div_three});
// 布尔值
print("{}\n{}\n{}\n", .{
true and false,
true or false,
!true,
});
// 可选值
var optional_value: ?[]const u8 = null;
assert(optional_value == null);
print("\n可选值 1\n类型: {}\n值: {?s}\n", .{
@TypeOf(optional_value), optional_value,
});
optional_value = "hi";
assert(optional_value != null);
print("\n可选值 2\n类型: {}\n值: {?s}\n", .{
@TypeOf(optional_value), optional_value,
});
// 错误联合
var number_or_error: anyerror!i32 = error.ArgNotFound;
print("\n错误联合 1\n类型: {}\n值: {!}\n", .{
@TypeOf(number_or_error),
number_or_error,
});
number_or_error = 1234;
print("\n错误联合 2\n类型: {}\n值: {!}\n", .{
@TypeOf(number_or_error), number_or_error,
});
}
$ zig build-exe values.zig $ ./values 1 + 1 = 2 7.0 / 3.0 = 2.3333333 false true false 可选值 1 类型: ?[]const u8 值: null 可选值 2 类型: ?[]const u8 值: hi 错误联合 1 类型: anyerror!i32 值: error.ArgNotFound 错误联合 2 类型: anyerror!i32 值: 1234
基本类型 §
| 类型 | C 等价类型 | 描述 |
|---|---|---|
i8
|
int8_t |
有符号 8 位整数 |
u8
|
uint8_t |
无符号 8 位整数 |
i16
|
int16_t |
有符号 16 位整数 |
u16
|
uint16_t |
无符号 16 位整数 |
i32
|
int32_t |
有符号 32 位整数 |
u32
|
uint32_t |
无符号 32 位整数 |
i64
|
int64_t |
有符号 64 位整数 |
u64
|
uint64_t |
无符号 64 位整数 |
i128
|
__int128 |
有符号 128 位整数 |
u128
|
unsigned __int128
|
无符号 128 位整数 |
isize
|
intptr_t |
有符号指针大小的整数 |
usize
|
uintptr_t,
size_t
|
无符号指针大小的整数。另请参见 #5185 |
c_char
|
char |
用于与 C 的 ABI 兼容 |
c_short
|
short |
用于与 C 的 ABI 兼容 |
c_ushort
|
unsigned short
|
用于与 C 的 ABI 兼容 |
c_int
|
int |
用于与 C 的 ABI 兼容 |
c_uint
|
unsigned int |
用于与 C 的 ABI 兼容 |
c_long
|
long |
用于与 C 的 ABI 兼容 |
c_ulong
|
unsigned long
|
用于与 C 的 ABI 兼容 |
c_longlong
|
long long |
用于与 C 的 ABI 兼容 |
c_ulonglong
|
unsigned long long
|
用于与 C 的 ABI 兼容 |
c_longdouble
|
long double |
用于与 C 的 ABI 兼容 |
f16
|
_Float16 |
16 位浮点数(10 位尾数)IEEE-754-2008 binary16 |
f32
|
float |
32 位浮点数(23 位尾数)IEEE-754-2008 binary32 |
f64
|
double |
64 位浮点数(52 位尾数)IEEE-754-2008 binary64 |
f80
|
long double |
80 位浮点数(64 位尾数)IEEE-754-2008 80 位扩展精度 |
f128
|
_Float128 |
128 位浮点数(112 位尾数)IEEE-754-2008 binary128 |
bool
|
bool |
true
或
false
|
anyopaque
|
void |
用于类型擦除指针。 |
void
|
(无) |
始终为值
void{}
|
noreturn
|
(无) |
break、continue、return、unreachable
和
while
(true)
{}
的类型
|
type
|
(无) | 类型的类型 |
anyerror
|
(无) | 错误代码 |
comptime_int
|
(无) | 仅允许用于 comptime 已知值。整数字面量的类型。 |
comptime_float
|
(无) | 仅允许用于 comptime 已知值。浮点字面量的类型。 |
除了上述整数类型之外,还可以通过使用以 i 或
u
开头后跟数字的标识符来引用任意位宽的整数。例如,标识符
i7 表示有符号
7 位整数。整数类型允许的最大位宽为
65535。
另请参见:
原始值 §
| 名称 | 描述 |
|---|---|
true
和
false
|
bool
值
|
null
|
用于将可选类型设置为
null
|
undefined
|
用于保留值未指定 |
另请参见:
字符串字面量和 Unicode 码点字面量 §
字符串字面量是指向以 null 结尾的字节数组的常量单项指针。字符串字面量的类型编码了长度和以 null 结尾的事实,因此它们可以被强制转换为切片和以 null 结尾的指针。解引用字符串字面量将它们转换为数组。
由于 Zig 源代码是
UTF-8 编码的,源代码中字符串字面量内出现的任何非 ASCII
字节都会将其 UTF-8 含义带入 Zig
程序中字符串的内容;编译器不会修改这些字节。可以使用
\xNN 表示法将非 UTF-8
字节嵌入字符串字面量中。
索引包含非 ASCII 字节的字符串会返回单个字节,无论是否为有效的 UTF-8。
Unicode 码点字面量的类型为
comptime_int,与整数字面量相同。所有转义序列在字符串字面量和 Unicode 码点字面量中都是有效的。
const print = @import("std").debug.print;
const mem = @import("std").mem; // 将用于比较字节
pub fn main() void {
const bytes = "hello";
print("{}\n", .{@TypeOf(bytes)}); // *const [5:0]u8
print("{d}\n", .{bytes.len}); // 5
print("{c}\n", .{bytes[1]}); // 'e'
print("{d}\n", .{bytes[5]}); // 0
print("{}\n", .{'e' == '\x65'}); // true
print("{d}\n", .{'\u{1f4a9}'}); // 128169
print("{d}\n", .{'💯'}); // 128175
print("{u}\n", .{'⚡'});
print("{}\n", .{mem.eql(u8, "hello", "h\x65llo")}); // true
print("{}\n", .{mem.eql(u8, "💯", "\xf0\x9f\x92\xaf")}); // 也为 true
const invalid_utf8 = "\xff\xfe"; // 使用 \xNN 表示法可以创建非 UTF-8 字符串。
print("0x{x}\n", .{invalid_utf8[1]}); // 索引它们会返回单个字节...
print("0x{x}\n", .{"💯"[1]}); // ...索引非 ASCII 字符的中间部分也是如此
}
$ zig build-exe string_literals.zig $ ./string_literals *const [5:0]u8 5 e 0 true 128169 128175 ⚡ true true 0xfe 0x9f
另请参见:
转义序列 §
| 转义序列 | 名称 |
|---|---|
\n |
换行 |
\r |
回车 |
\t |
制表符 |
\\ |
反斜杠 |
\' |
单引号 |
\" |
双引号 |
\xNN |
十六进制 8 位字节值(2 位数字) |
\u{NNNNNN} |
十六进制 Unicode 标量值的 UTF-8 编码(1 位或更多位数字) |
注意,最大有效的 Unicode 标量值是
0x10ffff。
多行字符串字面量 §
多行字符串字面量没有转义,可以跨越多行。要开始多行字符串字面量,使用
\\
标记。就像注释一样,字符串字面量会持续到行尾。行尾不包含在字符串字面量中。但是,如果下一行以
\\
开头,则会追加换行符并且字符串字面量继续。
const hello_world_in_c =
\\#include <stdio.h>
\\
\\int main(int argc, char **argv) {
\\ printf("hello world\n");
\\ return 0;
\\}
;
另请参见:
赋值 §
使用
const
关键字为标识符赋值:
const x = 1234;
fn foo() void {
// 它在文件作用域和函数内部都有效。
const y = 5678;
// 一旦赋值,标识符就不能更改。
y += 1;
}
pub fn main() void {
foo();
}
$ zig build-exe constant_identifier_cannot_change.zig /home/andy/dev/zig/doc/langref/constant_identifier_cannot_change.zig:8:5: error: 无法赋值给常量 y += 1; ^ referenced by: main: /home/andy/dev/zig/doc/langref/constant_identifier_cannot_change.zig:12:8 callMain [inlined]: /home/andy/dev/zig/lib/std/start.zig:618:22 callMainWithArgs [inlined]: /home/andy/dev/zig/lib/std/start.zig:587:20 posixCallMainAndExit: /home/andy/dev/zig/lib/std/start.zig:542:36 2 reference(s) hidden; use '-freference-trace=6' to see all references
const
适用于标识符直接寻址的所有字节。指针有自己的常量性。
如果需要可以修改的变量,使用
var 关键字:
const print = @import("std").debug.print;
pub fn main() void {
var y: i32 = 5678;
y += 1;
print("{d}", .{y});
}
$ zig build-exe mutable_var.zig $ ./mutable_var 5679
变量必须初始化:
pub fn main() void {
var x: i32;
x = 1;
}
$ zig build-exe var_must_be_initialized.zig /home/andy/dev/zig/doc/langref/var_must_be_initialized.zig:2:15: error: 期望 '=',找到 ';' var x: i32; ^
undefined §
使用
undefined
让变量保持未初始化:
const print = @import("std").debug.print;
pub fn main() void {
var x: i32 = undefined;
x = 1;
print("{d}", .{x});
}
$ zig build-exe assign_undefined.zig $ ./assign_undefined 1
undefined
可以被强制转换为任何类型。一旦发生这种情况,就不再可能检测到该值是
undefined。undefined
意味着该值可以是任何值,甚至是根据类型来说毫无意义的值。用英语翻译,undefined
的意思是"不是一个有意义的值。使用这个值将是一个错误。该值将不被使用,或在使用之前被覆盖。"
在调试和
ReleaseSafe 模式下,Zig
会向未定义的内存写入
0xaa
字节。这是为了尽早捕获错误,并帮助在调试器中检测未定义内存的使用。然而,这种行为只是一个实现特性,而不是语言语义,因此不能保证代码可以观察到它。
解构 §
const print = @import("std").debug.print;
pub fn main() void {
var x: u32 = undefined;
var y: u32 = undefined;
var z: u32 = undefined;
const tuple = .{ 1, 2, 3 };
x, y, z = tuple;
print("tuple: x = {}, y = {}, z = {}\n", .{x, y, z});
const array = [_]u32{ 4, 5, 6 };
x, y, z = array;
print("array: x = {}, y = {}, z = {}\n", .{x, y, z});
const vector: @Vector(3, u32) = .{ 7, 8, 9 };
x, y, z = vector;
print("vector: x = {}, y = {}, z = {}\n", .{x, y, z});
}
$ zig build-exe destructuring_to_existing.zig $ ./destructuring_to_existing tuple: x = 1, y = 2, z = 3 array: x = 4, y = 5, z = 6 vector: x = 7, y = 8, z = 9
解构表达式只能出现在块内(即不能在容器作用域)。赋值的左侧必须由逗号分隔的列表组成,其中每个元素可以是左值(例如,现有的 `var`)或变量声明:
const print = @import("std").debug.print;
pub fn main() void {
var x: u32 = undefined;
const tuple = .{ 1, 2, 3 };
x, var y : u32, const z = tuple;
print("x = {}, y = {}, z = {}\n", .{x, y, z});
// y 是可变的
y = 100;
// 您可以使用 _ 丢弃不需要的值。
_, x, _ = tuple;
print("x = {}", .{x});
}
$ zig build-exe destructuring_mixed.zig $ ./destructuring_mixed x = 1, y = 2, z = 3 x = 2
解构可以使用
comptime
关键字作为前缀,在这种情况下,整个解构表达式将在
comptime 时求值。所有声明的
var 都将是
comptime
var,并且所有表达式(结果位置和被赋值的表达式)都将在
comptime 时求值。
另请参见:
Zig 测试 §
在一个或多个
test
声明中编写的代码可用于确保行为符合预期:
const std = @import("std");
test "expect addOne adds one to 41" {
// 标准库包含有用的函数来帮助创建测试。
// `expect` 是一个验证其参数为 true 的函数。
// 如果其参数为 false,它将返回错误以指示失败。
// `try` 用于将错误返回给测试运行器以通知它测试失败。
try std.testing.expect(addOne(41) == 42);
}
test addOne {
// 测试名称也可以使用标识符编写。
// 这是一个文档测试,用作 `addOne` 的文档。
try std.testing.expect(addOne(41) == 42);
}
/// 函数 `addOne` 将给定作为其参数的数字加一。
fn addOne(number: i32) i32 {
return number + 1;
}
$ zig test testing_introduction.zig 1/2 testing_introduction.test.expect addOne adds one to 41...OK 2/2 testing_introduction.decltest.addOne...OK All 2 tests passed.
testing_introduction.zig
代码示例测试函数
addOne,以确保在给定输入
41 时返回
42。从这个测试的角度来看,addOne
函数被称为被测试代码。
zig test 是一个工具,它使用
Zig 标准库提供的默认测试运行器作为其主入口点来创建和运行测试构建。在构建期间,在解析给定 Zig 源文件时找到的
test
声明将包含在默认测试运行器中以运行和报告。
上面显示的 shell 输出在 zig test 命令之后显示了两行。这些行由默认测试运行器打印到标准错误:
- 1/2 testing_introduction.test.expect addOne adds one to 41...
- 这样的行表示正在运行总测试数中的哪个测试。在这种情况下,1/2 表示正在运行总共两个测试中的第一个。请注意,当测试运行器程序的标准错误输出到终端时,如果测试成功,这些行将被清除。
- 2/2 testing_introduction.decltest.addOne...
- 当测试名称是标识符时,默认测试运行器使用文本 decltest 而不是 test。
- All 2 tests passed.
- 此行表示已通过的测试总数。
测试声明 §
测试声明包含关键字
test,后跟一个可选名称(写成字符串字面量或标识符),然后是一个包含任何在函数中允许的有效 Zig 代码的块。
非命名测试块始终在测试构建期间运行,并且不受跳过测试的限制。
测试声明类似于函数:它们有返回类型和代码块。测试的隐式返回类型是错误联合类型
anyerror!void,并且不能更改。当不使用 zig test 工具构建
Zig 源文件时,测试声明将从构建中省略。
测试声明可以写在被测试代码所在的同一文件中,或写在单独的 Zig 源文件中。由于测试声明是顶级声明,因此它们与顺序无关,可以在被测试代码之前或之后编写。
另请参见:
文档测试 §
使用标识符命名的测试声明是文档测试。标识符必须引用作用域中的另一个声明。文档测试像文档注释一样,作为关联声明的文档,并将出现在为该声明生成的文档中。
有效的文档测试应该是独立的,并专注于正在测试的声明,回答新用户可能对其接口或预期用法提出的问题,同时避免不必要或令人困惑的细节。文档测试不是文档注释的替代品,而是补充和伴随,提供由 zig test 验证的可测试的代码驱动示例。
测试失败 §
默认测试运行器会检查测试返回的错误。当测试返回错误时,该测试被视为失败,其错误返回跟踪会输出到标准错误。所有测试运行后将报告失败总数。
const std = @import("std");
test "expect this to fail" {
try std.testing.expect(false);
}
test "expect this to succeed" {
try std.testing.expect(true);
}
$ zig test testing_failure.zig 1/2 testing_failure.test.expect this to fail...FAIL (TestUnexpectedResult) /home/andy/dev/zig/lib/std/testing.zig:607:14: 0x102f019 in expect (std.zig) if (!ok) return error.TestUnexpectedResult; ^ /home/andy/dev/zig/doc/langref/testing_failure.zig:4:5: 0x102f078 in test.expect this to fail (testing_failure.zig) try std.testing.expect(false); ^ 2/2 testing_failure.test.expect this to succeed...OK 1 passed; 0 skipped; 1 failed. error: the following test command failed with exit code 1: /home/andy/dev/zig/.zig-cache/o/bac0cff07a7d3f5b652a5a9cf02e6de1/test --seed=0x7a2fdb1
跳过测试 §
跳过测试的一种方法是使用 zig test 命令行参数 --test-filter [text] 将它们过滤掉。这使得测试构建仅包含名称包含提供的过滤器文本的测试。请注意,即使使用 --test-filter [text] 命令行参数,非命名测试也会运行。
要以编程方式跳过测试,使
test 返回错误
error.SkipZigTest,默认测试运行器将把该测试视为被跳过。所有测试运行后将报告跳过测试的总数。
test "this will be skipped" {
return error.SkipZigTest;
}
$ zig test testing_skip.zig 1/1 testing_skip.test.this will be skipped...SKIP 0 passed; 1 skipped; 0 failed.
报告内存泄漏 §
当代码使用
Zig 标准库的测试分配器 std.testing.allocator 分配内存时,默认测试运行器将报告使用测试分配器发现的任何泄漏:
const std = @import("std");
test "detect leak" {
var list = std.array_list.Managed(u21).init(std.testing.allocator);
// 缺少 `defer list.deinit();`
try list.append('☔');
try std.testing.expect(list.items.len == 1);
}
$ zig test testing_detect_leak.zig 1/1 testing_detect_leak.test.detect leak...OK [gpa] (err): memory address 0x7f74a8aa0000 leaked: /home/andy/dev/zig/lib/std/array_list.zig:468:67: 0x10aa8fe in ensureTotalCapacityPrecise (std.zig) const new_memory = try self.allocator.alignedAlloc(T, alignment, new_capacity); ^ /home/andy/dev/zig/lib/std/array_list.zig:444:51: 0x107c9e4 in ensureTotalCapacity (std.zig) return self.ensureTotalCapacityPrecise(better_capacity); ^ /home/andy/dev/zig/lib/std/array_list.zig:494:41: 0x105590d in addOne (std.zig) try self.ensureTotalCapacity(newlen); ^ /home/andy/dev/zig/lib/std/array_list.zig:252:49: 0x1038771 in append (std.zig) const new_item_ptr = try self.addOne(); ^ /home/andy/dev/zig/doc/langref/testing_detect_leak.zig:6:20: 0x10350a9 in test.detect leak (testing_detect_leak.zig) try list.append('☔'); ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:218:25: 0x1174760 in mainTerminal (test_runner.zig) if (test_fn.func()) |_| { ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:66:28: 0x1170d81 in main (test_runner.zig) return mainTerminal(); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x116ab1d in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x116a3b1 in _start (std.zig) asm volatile (switch (native_arch) { ^ All 1 tests passed. 1 errors were logged. 1 tests leaked memory. error: the following test command failed with exit code 1: /home/andy/dev/zig/.zig-cache/o/4df377b3969e36bf7e0b2704790b75be/test --seed=0xabc34e97
另请参见:
检测测试构建 §
使用编译变量
@import("builtin").is_test
来检测测试构建:
const std = @import("std");
const builtin = @import("builtin");
const expect = std.testing.expect;
test "builtin.is_test" {
try expect(isATest());
}
fn isATest() bool {
return builtin.is_test;
}
$ zig test testing_detect_test.zig 1/1 testing_detect_test.test.builtin.is_test...OK All 1 tests passed.
测试输出和日志记录 §
默认测试运行器和 Zig 标准库的测试命名空间将消息输出到标准错误。
测试命名空间 §
Zig 标准库的
testing
命名空间包含有用的函数来帮助您创建测试。除了
expect
函数之外,本文档还使用了一些其他函数,如下例所示:
const std = @import("std");
test "expectEqual demo" {
const expected: i32 = 42;
const actual = 42;
// `expectEqual` 的第一个参数是已知的、预期的结果。
// 第二个参数是某个表达式的结果。
// actual 的类型会被转换为 expected 的类型。
try std.testing.expectEqual(expected, actual);
}
test "expectError demo" {
const expected_error = error.DemoError;
const actual_error_union: anyerror!void = error.DemoError;
// 当实际错误与预期错误不同时,`expectError` 将失败。
try std.testing.expectError(expected_error, actual_error_union);
}
$ zig test testing_namespace.zig 1/2 testing_namespace.test.expectEqual demo...OK 2/2 testing_namespace.test.expectError demo...OK All 2 tests passed.
Zig 标准库还包含比较切片、字符串等的函数。有关更多可用函数,请参阅
Zig 标准库中
std.testing 命名空间的其余部分。
测试工具文档 §
zig test 有一些影响编译的命令行参数。有关完整列表,请参阅 zig test --help。
变量 §
变量是内存存储单元。
在声明变量时,通常最好使用
const 而不是
var。这会减少人类和计算机在阅读代码时需要做的工作,并创造更多优化机会。
extern 关键字或
@extern
内置函数可用于链接从另一个对象导出的变量。export
关键字或
@export
内置函数可用于使变量在链接时对其他对象可用。在这两种情况下,变量的类型必须与
C ABI 兼容。
另请参见:
标识符 §
变量标识符永远不允许遮蔽外部作用域的标识符。
标识符必须以字母字符或下划线开头,后面可以跟任意数量的字母数字字符或下划线。它们不得与任何关键字重叠。请参阅关键字参考。
如果需要不符合这些要求的名称,例如用于与外部库链接,可以使用
@"" 语法。
const @"identifier with spaces in it" = 0xff;
const @"1SmallStep4Man" = 112358;
const c = @import("std").c;
pub extern "c" fn @"error"() void;
pub extern "c" fn @"fstat$INODE64"(fd: c.fd_t, buf: *c.Stat) c_int;
const Color = enum {
red,
@"really red",
};
const color: Color = .@"really red";
容器级变量 §
容器级变量具有静态生命周期,且与顺序无关并延迟分析。容器级变量的初始化值隐式为
comptime。如果容器级变量是
const,则其值为
comptime
已知,否则为运行时已知。
var y: i32 = add(10, x);
const x: i32 = add(12, 34);
test "container level variables" {
try expect(x == 46);
try expect(y == 56);
}
fn add(a: i32, b: i32) i32 {
return a + b;
}
const std = @import("std");
const expect = std.testing.expect;
$ zig test test_container_level_variables.zig 1/1 test_container_level_variables.test.container level variables...OK All 1 tests passed.
容器级变量可以在 struct、union、enum 或 opaque 内部声明:
const std = @import("std");
const expect = std.testing.expect;
test "namespaced container level variable" {
try expect(foo() == 1235);
try expect(foo() == 1236);
}
const S = struct {
var x: i32 = 1234;
};
fn foo() i32 {
S.x += 1;
return S.x;
}
$ zig test test_namespaced_container_level_variable.zig 1/1 test_namespaced_container_level_variable.test.namespaced container level variable...OK All 1 tests passed.
静态局部变量 §
还可以通过在函数内部使用容器来拥有具有静态生命周期的局部变量。
const std = @import("std");
const expect = std.testing.expect;
test "static local variable" {
try expect(foo() == 1235);
try expect(foo() == 1236);
}
fn foo() i32 {
const S = struct {
var x: i32 = 1234;
};
S.x += 1;
return S.x;
}
$ zig test test_static_local_variable.zig 1/1 test_static_local_variable.test.static local variable...OK All 1 tests passed.
线程局部变量 §
可以使用
threadlocal
关键字将变量指定为线程局部变量,这使得每个线程使用变量的单独实例:
const std = @import("std");
const assert = std.debug.assert;
threadlocal var x: i32 = 1234;
test "thread local storage" {
const thread1 = try std.Thread.spawn(.{}, testTls, .{});
const thread2 = try std.Thread.spawn(.{}, testTls, .{});
testTls();
thread1.join();
thread2.join();
}
fn testTls() void {
assert(x == 1234);
x += 1;
assert(x == 1235);
}
$ zig test test_thread_local_variables.zig 1/1 test_thread_local_variables.test.thread local storage...OK All 1 tests passed.
线程局部变量不能是
const。
局部变量 §
局部变量出现在函数、comptime 块和 @cImport 块中。
当局部变量是
const
时,意味着初始化后,变量的值不会改变。如果
const
变量的初始化值为
comptime 已知,则该变量也是
comptime 已知。
局部变量可以用
comptime
关键字限定。这会使变量的值为
comptime
已知,并且所有对变量的加载和存储都在程序的语义分析期间进行,而不是在运行时。所有在
comptime
表达式中声明的变量都隐式为
comptime 变量。
const std = @import("std");
const expect = std.testing.expect;
test "comptime vars" {
var x: i32 = 1;
comptime var y: i32 = 1;
x += 1;
y += 1;
try expect(x == 2);
try expect(y == 2);
if (y != 2) {
// 这个编译错误永远不会触发,因为 y 是一个 comptime 变量,
// 因此 `y != 2` 是一个 comptime 值,并且这个 if 是静态求值的。
@compileError("wrong y value");
}
}
$ zig test test_comptime_variables.zig 1/1 test_comptime_variables.test.comptime vars...OK All 1 tests passed.
整数 §
整数字面量 §
const decimal_int = 98222;
const hex_int = 0xff;
const another_hex_int = 0xFF;
const octal_int = 0o755;
const binary_int = 0b11110000;
// 下划线可以放置在两个数字之间作为视觉分隔符
const one_billion = 1_000_000_000;
const binary_mask = 0b1_1111_1111;
const permissions = 0o7_5_5;
const big_address = 0xFF80_0000_0000_0000;
运行时整数值 §
整数字面量没有大小限制,如果发生任何非法行为,编译器会捕获它。
但是,一旦整数值不再在编译时已知,它必须有一个已知的大小,并且容易受到安全检查的非法行为的影响。
fn divide(a: i32, b: i32) i32 {
return a / b;
}
在此函数中,值 a 和
b 仅在运行时已知,因此此除法运算容易受到整数溢出和除以零的影响。
诸如 + 和
- 之类的运算符会在整数溢出时导致非法行为。在所有目标上都提供了用于环绕和饱和算术的替代运算符。+%
和 -% 执行环绕算术,而 +| 和
-| 执行饱和算术。
Zig 支持任意位宽整数,通过使用后跟数字的
i 或
u 标识符来引用。例如,标识符
i7 表示有符号
7 位整数。整数类型允许的最大位宽为
65535。对于有符号整数类型,Zig 使用二进制补码表示法。
另请参见:
浮点数 §
Zig 具有以下浮点类型:
-
f16- IEEE-754-2008 binary16 -
f32- IEEE-754-2008 binary32 -
f64- IEEE-754-2008 binary64 -
f80- IEEE-754-2008 80 位扩展精度 -
f128- IEEE-754-2008 binary128 -
c_longdouble- 匹配目标 C ABI 的long double
浮点字面量 §
浮点字面量的类型为
comptime_float,它保证具有与最大的其他浮点类型相同的精度和操作,即
f128。
浮点字面量可以强制转换为任何浮点类型,并且在没有小数部分时可以转换为任何整数类型。
const floating_point = 123.0E+77;
const another_float = 123.0;
const yet_another = 123.0e+77;
const hex_floating_point = 0x103.70p-5;
const another_hex_float = 0x103.70;
const yet_another_hex_float = 0x103.70P-5;
// 下划线可以放置在两个数字之间作为视觉分隔符
const lightspeed = 299_792_458.000_000;
const nanosecond = 0.000_000_001;
const more_hex = 0x1234_5678.9ABC_CDEFp-10;
没有用于 NaN、无穷大或负无穷大的语法。对于这些特殊值,必须使用标准库:
const std = @import("std");
const inf = std.math.inf(f32);
const negative_inf = -std.math.inf(f64);
const nan = std.math.nan(f128);
浮点运算 §
默认情况下,浮点运算使用
Strict 模式,但您可以在每个块的基础上切换到
Optimized 模式:
const std = @import("std");
const big = @as(f64, 1 << 40);
export fn foo_strict(x: f64) f64 {
return x + big - big;
}
export fn foo_optimized(x: f64) f64 {
@setFloatMode(.optimized);
return x + big - big;
}
$ zig build-obj float_mode_obj.zig -O ReleaseFast
对于此测试,我们必须将代码分离到两个对象文件中——否则优化器会在编译时计算出所有值,而编译时在严格模式下运行。
const print = @import("std").debug.print;
extern fn foo_strict(x: f64) f64;
extern fn foo_optimized(x: f64) f64;
pub fn main() void {
const x = 0.001;
print("optimized = {}\n", .{foo_optimized(x)});
print("strict = {}\n", .{foo_strict(x)});
}
另请参阅:
运算符 §
Zig 中没有运算符重载。当你在 Zig 中看到一个运算符时,你知道它正在执行此表中的某些操作,而不会是其他操作。
运算符表 §
| 名称 | 语法 | 类型 | 备注 | 示例 |
|---|---|---|---|---|
| 加法 |
|
|
|
|
| 回绕加法 |
|
|
|
|
| 饱和加法 |
|
|
|
|
| 减法 |
|
|
|
|
| 回绕减法 |
|
|
|
|
| 饱和减法 |
|
|
|
|
| 取负 |
|
|
|
|
| 回绕取负 |
|
|
|
|
| 乘法 |
|
|
|
|
| 回绕乘法 |
|
|
|
|
| 饱和乘法 |
|
|
|
|
| 除法 |
|
|
||
| 取余除法 |
|
|
||
| 位左移 |
|
|
|
|
| 饱和位左移 |
|
|
|
|
| 位右移 |
|
|
||
| 按位与 |
|
|
|
|
| 按位或 |
|
|
|
|
| 按位异或 |
|
|
|
|
| 按位取反 |
|
|
||
| 可选值默认解包 |
|
如果 a 是
null, 返回 b("默认值"),
否则返回 a 的解包值。 注意
b 可以是
noreturn
类型的值。
|
|
|
| 可选值解包 |
|
等价于:
|
|
|
| 错误默认解包 |
|
如果 a 是
error, 返回 b("默认值"),
否则返回 a 的解包值。 注意
b 可以是
noreturn
类型的值。 err 是
error,并且在表达式
b 的作用域内。
|
|
|
| 逻辑与 |
|
如果 a 是
false,不计算 b 即返回
false。否则,返回 b。
|
|
|
| 逻辑或 |
|
如果 a 是
true, 不计算 b 即返回
true。否则,返回 b。
|
|
|
| 布尔取反 |
|
|
||
| 相等 |
|
如果 a 和 b 相等,返回
true,否则返回
false。 对操作数调用对等类型解析。
|
|
|
| 空值检查 |
|
如果 a 是
null,返回
true,否则返回
false。
|
|
|
| 不相等 |
|
如果 a 和 b 相等,返回
false,否则返回
true。 对操作数调用对等类型解析。
|
|
|
| 非空检查 |
|
如果 a 是
null,返回
false,否则返回
true。
|
|
|
| 大于 |
|
如果 a 大于 b,返回
true,否则返回
false。 对操作数调用对等类型解析。
|
|
|
| 大于或等于 |
|
如果 a 大于或等于 b,返回
true,否则返回
false。 对操作数调用对等类型解析。
|
|
|
| 小于 |
|
如果 a 小于 b,返回
true,否则返回
false。 对操作数调用对等类型解析。
|
|
|
| 小于或等于 |
|
如果 a 小于或等于 b,返回
true,否则返回
false。 对操作数调用对等类型解析。
|
|
|
| 数组拼接 |
|
|
|
|
| 数组重复 |
|
|
|
|
| 指针解引用 |
|
指针解引用。 |
|
|
| 取地址 |
|
所有类型 |
|
|
| 错误集合并 |
|
合并错误集 |
|
优先级 §
x() x[] x.y x.* x.?
a!b
x{}
!x -x -%x ~x &x ?x
* / % ** *% *| ||
+ - ++ +% -% +| -|
<< >> <<|
& ^ | orelse catch
== != < > <= >=
and
or
= *= *%= *|= /= %= += +%= +|= -= -%= -|= <<= <<|= >>= &= ^= |=
数组 §
const expect = @import("std").testing.expect;
const assert = @import("std").debug.assert;
const mem = @import("std").mem;
// 数组字面量
const message = [_]u8{ 'h', 'e', 'l', 'l', 'o' };
// 使用结果位置的替代初始化方式
const alt_message: [5]u8 = .{ 'h', 'e', 'l', 'l', 'o' };
comptime {
assert(mem.eql(u8, &message, &alt_message));
}
// 获取数组的大小
comptime {
assert(message.len == 5);
}
// 字符串字面量是指向数组的单项指针。
const same_message = "hello";
comptime {
assert(mem.eql(u8, &message, same_message));
}
test "iterate over an array" {
var sum: usize = 0;
for (message) |byte| {
sum += byte;
}
try expect(sum == 'h' + 'e' + 'l' * 2 + 'o');
}
// 可修改数组
var some_integers: [100]i32 = undefined;
test "modify an array" {
for (&some_integers, 0..) |*item, i| {
item.* = @intCast(i);
}
try expect(some_integers[10] == 10);
try expect(some_integers[99] == 99);
}
// 如果值在编译时已知,数组拼接可以工作
const part_one = [_]i32{ 1, 2, 3, 4 };
const part_two = [_]i32{ 5, 6, 7, 8 };
const all_of_it = part_one ++ part_two;
comptime {
assert(mem.eql(i32, &all_of_it, &[_]i32{ 1, 2, 3, 4, 5, 6, 7, 8 }));
}
// 记住字符串字面量是数组
const hello = "hello";
const world = "world";
const hello_world = hello ++ " " ++ world;
comptime {
assert(mem.eql(u8, hello_world, "hello world"));
}
// ** 用于重复模式
const pattern = "ab" ** 3;
comptime {
assert(mem.eql(u8, pattern, "ababab"));
}
// 初始化数组为零
const all_zero = [_]u16{0} ** 10;
comptime {
assert(all_zero.len == 10);
assert(all_zero[5] == 0);
}
// 使用编译时代码初始化数组
var fancy_array = init: {
var initial_value: [10]Point = undefined;
for (&initial_value, 0..) |*pt, i| {
pt.* = Point{
.x = @intCast(i),
.y = @intCast(i * 2),
};
}
break :init initial_value;
};
const Point = struct {
x: i32,
y: i32,
};
test "compile-time array initialization" {
try expect(fancy_array[4].x == 4);
try expect(fancy_array[4].y == 8);
}
// 调用函数初始化数组
var more_points = [_]Point{makePoint(3)} ** 10;
fn makePoint(x: i32) Point {
return Point{
.x = x,
.y = x * 2,
};
}
test "array initialization with function calls" {
try expect(more_points[4].x == 3);
try expect(more_points[4].y == 6);
try expect(more_points.len == 10);
}
$ zig test test_arrays.zig 1/4 test_arrays.test.iterate over an array...OK 2/4 test_arrays.test.modify an array...OK 3/4 test_arrays.test.compile-time array initialization...OK 4/4 test_arrays.test.array initialization with function calls...OK All 4 tests passed.
另请参阅:
多维数组 §
多维数组可以通过嵌套数组创建:
const std = @import("std");
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
const mat4x5 = [4][5]f32{
[_]f32{ 1.0, 0.0, 0.0, 0.0, 0.0 },
[_]f32{ 0.0, 1.0, 0.0, 1.0, 0.0 },
[_]f32{ 0.0, 0.0, 1.0, 0.0, 0.0 },
[_]f32{ 0.0, 0.0, 0.0, 1.0, 9.9 },
};
test "multidimensional arrays" {
// mat4x5 本身是一个数组的一维数组。
try expectEqual(mat4x5[1], [_]f32{ 0.0, 1.0, 0.0, 1.0, 0.0 });
// 通过索引外层数组,然后索引内层数组来访问二维数组。
try expect(mat4x5[3][4] == 9.9);
// 这里我们使用 for 循环进行迭代。
for (mat4x5, 0..) |row, row_index| {
for (row, 0..) |cell, column_index| {
if (row_index == column_index) {
try expect(cell == 1.0);
}
}
}
// 初始化一个多维数组为零。
const all_zero: [4][5]f32 = .{.{0} ** 5} ** 4;
try expect(all_zero[0][0] == 0);
}
$ zig test test_multidimensional_arrays.zig 1/1 test_multidimensional_arrays.test.multidimensional arrays...OK All 1 tests passed.
哨兵终止数组 §
语法
[N:x]T
描述了一个具有哨兵元素的数组,该元素的值为
x,位于对应于长度 N 的索引处。
const std = @import("std");
const expect = std.testing.expect;
test "0-terminated sentinel array" {
const array = [_:0]u8{ 1, 2, 3, 4 };
try expect(@TypeOf(array) == [4:0]u8);
try expect(array.len == 4);
try expect(array[4] == 0);
}
test "extra 0s in 0-terminated sentinel array" {
// 哨兵值可能会提前出现,但不会影响编译时的 'len'。
const array = [_:0]u8{ 1, 0, 0, 4 };
try expect(@TypeOf(array) == [4:0]u8);
try expect(array.len == 4);
try expect(array[4] == 0);
}
$ zig test test_null_terminated_array.zig 1/2 test_null_terminated_array.test.0-terminated sentinel array...OK 2/2 test_null_terminated_array.test.extra 0s in 0-terminated sentinel array...OK All 2 tests passed.
另请参阅:
解构数组 §
数组可以被解构:
const print = @import("std").debug.print;
fn swizzleRgbaToBgra(rgba: [4]u8) [4]u8 {
// 通过解构实现可读的交换
const r, const g, const b, const a = rgba;
return .{ b, g, r, a };
}
pub fn main() void {
const pos = [_]i32{ 1, 2 };
const x, const y = pos;
print("x = {}, y = {}\n", .{x, y});
const orange: [4]u8 = .{ 255, 165, 0, 255 };
print("{any}\n", .{swizzleRgbaToBgra(orange)});
}
$ zig build-exe destructuring_arrays.zig
$ ./destructuring_arrays
x = 1, y = 2
{ 0, 165, 255, 255 }
另请参阅:
向量 §
向量是一组布尔值、整数、浮点数或指针,如果可能的话,使用 SIMD 指令并行操作。 向量类型使用内置函数 @Vector 创建。
向量通常支持与其底层基类型相同的内置运算符。 唯一的例外是布尔向量上的关键字 `and` 和 `or`,因为这些运算符会影响控制流,而这对于向量是不允许的。 所有其他操作都是按元素执行的,并返回与输入向量相同长度的向量。这包括:
-
算术运算
(
+、-、/、*、@divFloor、@sqrt、@ceil、@log等) -
位运算
(
>>、<<、&、|、~等) -
比较运算符
(
<、>、==等) - 布尔取反 (
!)
禁止在标量(单个数字)和向量的混合体上使用数学运算符。Zig 提供了 @splat 内置函数来轻松地将标量转换为向量,并且它支持 @reduce 和数组索引语法将向量转换为标量。向量还支持与编译时已知长度的固定长度数组之间的赋值。
为了在向量内部和向量之间重新排列元素,Zig 提供了 @shuffle 和 @select 函数。
短于目标机器原生 SIMD 大小的向量操作通常会编译成单个 SIMD 指令,而长于目标机器原生 SIMD 大小的向量将编译成多个 SIMD 指令。如果给定操作在目标架构上没有 SIMD 支持,编译器将默认一次操作一个向量元素。Zig 支持任何编译时已知的向量长度,最高可达 2^32-1,尽管小的 2 的幂(2-64)是最典型的。注意,过长的向量长度(例如 2^20)可能会导致当前版本的 Zig 编译器崩溃。
const std = @import("std");
const expectEqual = std.testing.expectEqual;
test "Basic vector usage" {
// 向量具有编译时已知的长度和基类型。
const a = @Vector(4, i32){ 1, 2, 3, 4 };
const b = @Vector(4, i32){ 5, 6, 7, 8 };
// 数学运算按元素进行。
const c = a + b;
// 可以使用数组索引语法访问单个向量元素。
try expectEqual(6, c[0]);
try expectEqual(8, c[1]);
try expectEqual(10, c[2]);
try expectEqual(12, c[3]);
}
test "Conversion between vectors, arrays, and slices" {
// 向量和固定长度数组可以自动相互赋值
const arr1: [4]f32 = [_]f32{ 1.1, 3.2, 4.5, 5.6 };
const vec: @Vector(4, f32) = arr1;
const arr2: [4]f32 = vec;
try expectEqual(arr1, arr2);
// 你也可以使用 .* 从编译时已知长度的切片赋值到向量
const vec2: @Vector(2, f32) = arr1[1..3].*;
const slice: []const f32 = &arr1;
var offset: u32 = 1; // var 使其为运行时已知
_ = &offset; // 抑制 'var is never mutated' 错误
// 要从运行时已知的偏移量提取编译时已知的长度,
// 首先从起始偏移量提取一个新切片,然后提取一个
// 编译时已知长度的数组
const vec3: @Vector(2, f32) = slice[offset..][0..2].*;
try expectEqual(slice[offset], vec2[0]);
try expectEqual(slice[offset + 1], vec2[1]);
try expectEqual(vec2, vec3);
}
$ zig test test_vector.zig 1/2 test_vector.test.Basic vector usage...OK 2/2 test_vector.test.Conversion between vectors, arrays, and slices...OK All 2 tests passed.
TODO 讨论 C ABI 互操作
TODO 考虑建议使用 std.MultiArrayList
另请参阅:
解构向量 §
向量可以被解构:
const print = @import("std").debug.print;
// 模拟 punpckldq
pub fn unpack(x: @Vector(4, f32), y: @Vector(4, f32)) @Vector(4, f32) {
const a, const c, _, _ = x;
const b, const d, _, _ = y;
return .{ a, b, c, d };
}
pub fn main() void {
const x: @Vector(4, f32) = .{ 1.0, 2.0, 3.0, 4.0 };
const y: @Vector(4, f32) = .{ 5.0, 6.0, 7.0, 8.0 };
print("{}", .{unpack(x, y)});
}
$ zig build-exe destructuring_vectors.zig
$ ./destructuring_vectors
{ 1, 5, 2, 6 }
另请参阅:
指针 §
Zig 有两种指针:单项指针和多项指针。
-
*T- 指向恰好一个项的单项指针。- 支持解引用语法:
ptr.* -
支持切片语法:
ptr[0..1] - 支持指针减法:
ptr - ptr
- 支持解引用语法:
-
[*]T- 指向未知数量项的多项指针。- 支持索引语法:
ptr[i] -
支持切片语法:
ptr[start..end]和ptr[start..] -
支持指针-整数算术:
ptr + int、ptr - int - 支持指针减法:
ptr - ptr
T必须具有已知大小,这意味着它不能是anyopaque或任何其他不透明类型。 - 支持索引语法:
-
*[N]T- 指向 N 个项的指针,与指向数组的单项指针相同。- 支持索引语法:
array_ptr[i] -
支持切片语法:
array_ptr[start..end] -
支持 len 属性:
array_ptr.len -
支持指针减法:
array_ptr - array_ptr
- 支持索引语法:
-
[]T- 是一个切片(一个胖指针,包含一个类型为[*]T的指针和一个长度)。- 支持索引语法:
slice[i] -
支持切片语法:
slice[start..end] - 支持 len 属性:
slice.len
- 支持索引语法:
使用 &x 获取单项指针:
const expect = @import("std").testing.expect;
test "address of syntax" {
// 获取变量的地址:
const x: i32 = 1234;
const x_ptr = &x;
// 解引用指针:
try expect(x_ptr.* == 1234);
// 当获取 const 变量的地址时,会得到一个 const 单项指针。
try expect(@TypeOf(x_ptr) == *const i32);
// 如果想修改值,需要获取可变变量的地址:
var y: i32 = 5678;
const y_ptr = &y;
try expect(@TypeOf(y_ptr) == *i32);
y_ptr.* += 1;
try expect(y_ptr.* == 5679);
}
test "pointer array access" {
// 获取单个元素的地址会得到一个单项指针。
// 这种指针不支持指针算术。
var array = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
const ptr = &array[2];
try expect(@TypeOf(ptr) == *u8);
try expect(array[2] == 3);
ptr.* += 1;
try expect(array[2] == 4);
}
test "slice syntax" {
// 获取变量的指针:
var x: i32 = 1234;
const x_ptr = &x;
// 使用切片语法转换为数组指针:
const x_array_ptr = x_ptr[0..1];
try expect(@TypeOf(x_array_ptr) == *[1]i32);
// 强制转换为多项指针:
const x_many_ptr: [*]i32 = x_array_ptr;
try expect(x_many_ptr[0] == 1234);
}
$ zig test test_single_item_pointer.zig 1/3 test_single_item_pointer.test.address of syntax...OK 2/3 test_single_item_pointer.test.pointer array access...OK 3/3 test_single_item_pointer.test.slice syntax...OK All 3 tests passed.
Zig 支持指针算术。最好将指针赋值给
[*]T
并递增该变量。例如,直接递增来自切片的指针会破坏它。
const expect = @import("std").testing.expect;
test "pointer arithmetic with many-item pointer" {
const array = [_]i32{ 1, 2, 3, 4 };
var ptr: [*]const i32 = &array;
try expect(ptr[0] == 1);
ptr += 1;
try expect(ptr[0] == 2);
// 对多项指针进行无结束位置的切片等同于
// 指针算术运算:`ptr[start..] == ptr + start`
try expect(ptr[1..] == ptr + 1);
// 支持除切片外的任意两个指针之间基于元素大小的减法
try expect(&ptr[1] - &ptr[0] == 1);
}
test "pointer arithmetic with slices" {
var array = [_]i32{ 1, 2, 3, 4 };
var length: usize = 0; // 使用 var 使其在运行时已知
_ = &length; // 抑制 'var is never mutated' 错误
var slice = array[length..array.len];
try expect(slice[0] == 1);
try expect(slice.len == 4);
slice.ptr += 1;
// 现在切片处于不良状态,因为 len 没有更新
try expect(slice[0] == 2);
try expect(slice.len == 4);
}
$ zig test test_pointer_arithmetic.zig 1/2 test_pointer_arithmetic.test.pointer arithmetic with many-item pointer...OK 2/2 test_pointer_arithmetic.test.pointer arithmetic with slices...OK All 2 tests passed.
在 Zig 中,我们通常更喜欢使用切片而不是哨兵终止指针。 您可以使用切片语法将数组或指针转换为切片。
切片具有边界检查,因此可以防止此类非法行为。这是我们更喜欢切片而不是指针的原因之一。
const expect = @import("std").testing.expect;
test "pointer slicing" {
var array = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var start: usize = 2; // 使用 var 使其在运行时已知
_ = &start; // 抑制 'var is never mutated' 错误
const slice = array[start..4];
try expect(slice.len == 2);
try expect(array[3] == 4);
slice[1] += 1;
try expect(array[3] == 5);
}
$ zig test test_slice_bounds.zig 1/1 test_slice_bounds.test.pointer slicing...OK All 1 tests passed.
指针在编译期也可以工作,只要代码不依赖于未定义的内存布局:
const expect = @import("std").testing.expect;
test "comptime pointers" {
comptime {
var x: i32 = 1;
const ptr = &x;
ptr.* += 1;
x += 1;
try expect(ptr.* == 3);
}
}
$ zig test test_comptime_pointers.zig 1/1 test_comptime_pointers.test.comptime pointers...OK All 1 tests passed.
要将整数地址转换为指针,请使用
@ptrFromInt。 要将指针转换为整数,请使用
@intFromPtr:
const expect = @import("std").testing.expect;
test "@intFromPtr and @ptrFromInt" {
const ptr: *i32 = @ptrFromInt(0xdeadbee0);
const addr = @intFromPtr(ptr);
try expect(@TypeOf(addr) == usize);
try expect(addr == 0xdeadbee0);
}
$ zig test test_integer_pointer_conversion.zig 1/1 test_integer_pointer_conversion.test.@intFromPtr and @ptrFromInt...OK All 1 tests passed.
Zig 能够在编译期代码中保留内存地址,只要指针从未被解引用:
const expect = @import("std").testing.expect;
test "comptime @ptrFromInt" {
comptime {
// Zig 能够在编译期做到这一点,只要
// ptr 从未被解引用。
const ptr: *i32 = @ptrFromInt(0xdeadbee0);
const addr = @intFromPtr(ptr);
try expect(@TypeOf(addr) == usize);
try expect(addr == 0xdeadbee0);
}
}
$ zig test test_comptime_pointer_conversion.zig 1/1 test_comptime_pointer_conversion.test.comptime @ptrFromInt...OK All 1 tests passed.
@ptrCast
将指针的元素类型转换为另一种类型。这会创建一个新指针,根据通过它进行的加载和存储,可能会导致无法检测的非法行为。通常,如果可能的话,其他类型的类型转换比
@ptrCast
更可取。
const std = @import("std");
const expect = std.testing.expect;
test "pointer casting" {
const bytes align(@alignOf(u32)) = [_]u8{ 0x12, 0x12, 0x12, 0x12 };
const u32_ptr: *const u32 = @ptrCast(&bytes);
try expect(u32_ptr.* == 0x12121212);
// 即使这个例子也是人为的 - 有比指针转换更好的方法来完成上述操作。
// 例如,使用切片收窄转换:
const u32_value = std.mem.bytesAsSlice(u32, bytes[0..])[0];
try expect(u32_value == 0x12121212);
// 还有另一种方法,最直接的方法:
try expect(@as(u32, @bitCast(bytes)) == 0x12121212);
}
test "pointer child type" {
// 指针类型有一个 `child` 字段,它告诉你它们指向的类型。
try expect(@typeInfo(*u32).pointer.child == u32);
}
$ zig test test_pointer_casting.zig 1/2 test_pointer_casting.test.pointer casting...OK 2/2 test_pointer_casting.test.pointer child type...OK All 2 tests passed.
另请参阅:
volatile §
加载和存储被假定为没有副作用。如果给定的加载或存储应该有副作用,例如内存映射输入/输出(MMIO),请使用
volatile。 在以下代码中,对
mmio_ptr
的加载和存储保证全部发生且与源代码中的顺序相同:
const expect = @import("std").testing.expect;
test "volatile" {
const mmio_ptr: *volatile u8 = @ptrFromInt(0x12345678);
try expect(@TypeOf(mmio_ptr) == *volatile u8);
}
$ zig test test_volatile.zig 1/1 test_volatile.test.volatile...OK All 1 tests passed.
请注意,volatile
与并发和原子操作无关。
如果您看到代码将
volatile
用于内存映射输入/输出以外的用途,它可能是一个错误。
对齐 §
每个类型都有一个对齐 - 一个字节数,使得当该类型的值从内存加载或存储到内存时,内存地址必须能被这个数整除。您可以使用 @alignOf 找出任何类型的这个值。
对齐取决于 CPU 架构,但始终是 2 的幂,且小于
1 <<
29。
在 Zig 中,指针类型有一个对齐值。如果该值等于底层类型的对齐,则可以从类型中省略:
const std = @import("std");
const builtin = @import("builtin");
const expect = std.testing.expect;
test "variable alignment" {
var x: i32 = 1234;
const align_of_i32 = @alignOf(@TypeOf(x));
try expect(@TypeOf(&x) == *i32);
try expect(*i32 == *align(align_of_i32) i32);
if (builtin.target.cpu.arch == .x86_64) {
try expect(@typeInfo(*i32).pointer.alignment == 4);
}
}
$ zig test test_variable_alignment.zig 1/1 test_variable_alignment.test.variable alignment...OK All 1 tests passed.
与
*i32 可以强制转换为
*const
i32
的方式相同,具有更大对齐的指针可以隐式转换为具有更小对齐的指针,但反之则不行。
您可以在变量和函数上指定对齐。如果这样做,那么指向它们的指针将获得指定的对齐:
const expect = @import("std").testing.expect;
var foo: u8 align(4) = 100;
test "global variable alignment" {
try expect(@typeInfo(@TypeOf(&foo)).pointer.alignment == 4);
try expect(@TypeOf(&foo) == *align(4) u8);
const as_pointer_to_array: *align(4) [1]u8 = &foo;
const as_slice: []align(4) u8 = as_pointer_to_array;
const as_unaligned_slice: []u8 = as_slice;
try expect(as_unaligned_slice[0] == 100);
}
fn derp() align(@sizeOf(usize) * 2) i32 {
return 1234;
}
fn noop1() align(1) void {}
fn noop4() align(4) void {}
test "function alignment" {
try expect(derp() == 1234);
try expect(@TypeOf(derp) == fn () i32);
try expect(@TypeOf(&derp) == *align(@sizeOf(usize) * 2) const fn () i32);
noop1();
try expect(@TypeOf(noop1) == fn () void);
try expect(@TypeOf(&noop1) == *align(1) const fn () void);
noop4();
try expect(@TypeOf(noop4) == fn () void);
try expect(@TypeOf(&noop4) == *align(4) const fn () void);
}
$ zig test test_variable_func_alignment.zig 1/2 test_variable_func_alignment.test.global variable alignment...OK 2/2 test_variable_func_alignment.test.function alignment...OK All 2 tests passed.
如果您有一个对齐较小的指针或切片,但您知道它实际上具有更大的对齐,请使用 @alignCast 将指针更改为更对齐的指针。这在运行时是无操作的,但会插入安全检查:
const std = @import("std");
test "pointer alignment safety" {
var array align(4) = [_]u32{ 0x11111111, 0x11111111 };
const bytes = std.mem.sliceAsBytes(array[0..]);
try std.testing.expect(foo(bytes) == 0x11111111);
}
fn foo(bytes: []u8) u32 {
const slice4 = bytes[1..5];
const int_slice = std.mem.bytesAsSlice(u32, @as([]align(4) u8, @alignCast(slice4)));
return int_slice[0];
}
$ zig test test_incorrect_pointer_alignment.zig 1/1 test_incorrect_pointer_alignment.test.pointer alignment safety...thread 2895819 panic: incorrect alignment /home/andy/dev/zig/doc/langref/test_incorrect_pointer_alignment.zig:10:68: 0x102c2a8 in foo (test_incorrect_pointer_alignment.zig) const int_slice = std.mem.bytesAsSlice(u32, @as([]align(4) u8, @alignCast(slice4))); ^ /home/andy/dev/zig/doc/langref/test_incorrect_pointer_alignment.zig:6:31: 0x102c0d2 in test.pointer alignment safety (test_incorrect_pointer_alignment.zig) try std.testing.expect(foo(bytes) == 0x11111111); ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:218:25: 0x115cf30 in mainTerminal (test_runner.zig) if (test_fn.func()) |_| { ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:66:28: 0x1156151 in main (test_runner.zig) return mainTerminal(); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x114feed in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x114f781 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) error: the following test command crashed: /home/andy/dev/zig/.zig-cache/o/9cb7896b3cdf812f518129da5e21dc23/test --seed=0x441e5edd
allowzero §
此指针属性允许指针具有地址零。这仅在独立操作系统目标上需要,其中地址零是可映射的。如果您想表示空指针,请改用可选指针。带有
allowzero 的可选指针与指针的大小不同。在此代码示例中,如果指针没有
allowzero
属性,这将是指针转换无效空值
panic:
const std = @import("std");
const expect = std.testing.expect;
test "allowzero" {
var zero: usize = 0; // 使用 var 使其在运行时已知
_ = &zero; // 抑制 'var is never mutated' 错误
const ptr: *allowzero i32 = @ptrFromInt(zero);
try expect(@intFromPtr(ptr) == 0);
}
$ zig test test_allowzero.zig 1/1 test_allowzero.test.allowzero...OK All 1 tests passed.
哨兵终止指针 §
语法
[*:x]T
描述一个指针,其长度由哨兵值确定。这提供了防止缓冲区溢出和过度读取的保护。
const std = @import("std");
// 这也可以作为 `std.c.printf` 使用。
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;
pub fn main() anyerror!void {
_ = printf("Hello, world!\n"); // OK
const msg = "Hello, world!\n";
const non_null_terminated_msg: [msg.len]u8 = msg.*;
_ = printf(&non_null_terminated_msg);
}
$ zig build-exe sentinel-terminated_pointer.zig -lc /home/andy/dev/zig/doc/langref/sentinel-terminated_pointer.zig:11:16: error: expected type '[*:0]const u8', found '*const [14]u8' _ = printf(&non_null_terminated_msg); ^~~~~~~~~~~~~~~~~~~~~~~~ /home/andy/dev/zig/doc/langref/sentinel-terminated_pointer.zig:11:16: note: destination pointer requires '0' sentinel /home/andy/dev/zig/doc/langref/sentinel-terminated_pointer.zig:4:34: note: parameter type declared here pub extern "c" fn printf(format: [*:0]const u8, ...) c_int; ^~~~~~~~~~~~~ referenced by: callMain [inlined]: /home/andy/dev/zig/lib/std/start.zig:627:37 callMainWithArgs [inlined]: /home/andy/dev/zig/lib/std/start.zig:587:20 main: /home/andy/dev/zig/lib/std/start.zig:602:28 1 reference(s) hidden; use '-freference-trace=4' to see all references
另请参阅:
切片 §
切片是一个指针和一个长度。数组和切片之间的区别在于,数组的长度是类型的一部分,在编译期已知,而切片的长度在运行时已知。两者都可以使用
len 字段访问。
const expect = @import("std").testing.expect;
const expectEqualSlices = @import("std").testing.expectEqualSlices;
test "basic slices" {
var array = [_]i32{ 1, 2, 3, 4 };
var known_at_runtime_zero: usize = 0;
_ = &known_at_runtime_zero;
const slice = array[known_at_runtime_zero..array.len];
// 使用结果位置的替代初始化
const alt_slice: []const i32 = &.{ 1, 2, 3, 4 };
try expectEqualSlices(i32, slice, alt_slice);
try expect(@TypeOf(slice) == []i32);
try expect(&slice[0] == &array[0]);
try expect(slice.len == array.len);
// 如果使用编译期已知的起始和结束位置进行切片,结果是
// 指向数组的指针,而不是切片。
const array_ptr = array[0..array.len];
try expect(@TypeOf(array_ptr) == *[array.len]i32);
// 您可以通过两次切片来执行按长度切片。这允许编译器
// 执行某些优化,例如在起始位置仅在运行时已知时识别编译期已知的长度。
var runtime_start: usize = 1;
_ = &runtime_start;
const length = 2;
const array_ptr_len = array[runtime_start..][0..length];
try expect(@TypeOf(array_ptr_len) == *[length]i32);
// 在切片上使用取地址运算符可以得到单项指针。
try expect(@TypeOf(&slice[0]) == *i32);
// 使用 `ptr` 字段可以得到多项指针。
try expect(@TypeOf(slice.ptr) == [*]i32);
try expect(@intFromPtr(slice.ptr) == @intFromPtr(&slice[0]));
// 切片具有数组边界检查。如果您尝试访问超出边界的内容,
// 您将收到安全检查失败:
slice[10] += 1;
// 请注意,`slice.ptr` 不调用安全检查,而 `&slice[0]`
// 断言切片的 len > 0。
// 可以像这样创建空切片:
const empty1 = &[0]u8{};
// 如果类型已知,可以使用这个简写:
const empty2: []u8 = &.{};
try expect(empty1.len == 0);
try expect(empty2.len == 0);
// 零长度初始化始终可用于创建空切片,即使切片是可变的。
// 这是因为指向的数据长度为零位,所以其不可变性是无关紧要的。
}
$ zig test test_basic_slices.zig 1/1 test_basic_slices.test.basic slices...thread 2902466 panic: index out of bounds: index 10, len 4 /home/andy/dev/zig/doc/langref/test_basic_slices.zig:41:10: 0x102e3c0 in test.basic slices (test_basic_slices.zig) slice[10] += 1; ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:218:25: 0x1160b60 in mainTerminal (test_runner.zig) if (test_fn.func()) |_| { ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:66:28: 0x1159d81 in main (test_runner.zig) return mainTerminal(); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x1153b1d in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x11533b1 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) error: the following test command crashed: /home/andy/dev/zig/.zig-cache/o/0e584e3dac6333a0b2d5158992704660/test --seed=0x665d12a2
这是我们更喜欢切片而不是指针的原因之一。
const std = @import("std");
const expect = std.testing.expect;
const mem = std.mem;
const fmt = std.fmt;
test "using slices for strings" {
// Zig 没有字符串的概念。字符串字面量是指向
// 以空值终止的 u8 数组的常量指针,按照惯例,
// "字符串"参数预期为 UTF-8 编码的 u8 切片。
// 这里我们将 *const [5:0]u8 和 *const [6:0]u8 强制转换为 []const u8
const hello: []const u8 = "hello";
const world: []const u8 = "世界";
var all_together: [100]u8 = undefined;
// 您可以在数组上使用至少一个运行时已知索引的切片语法
// 将数组转换为切片。
var start: usize = 0;
_ = &start;
const all_together_slice = all_together[start..];
// 字符串连接示例。
const hello_world = try fmt.bufPrint(all_together_slice, "{s} {s}", .{ hello, world });
// 通常,您可以使用 UTF-8 而不用担心某些东西是否是
// 字符串。如果您不需要处理单个字符,则无需
// 解码。
try expect(mem.eql(u8, hello_world, "hello 世界"));
}
test "slice pointer" {
var array: [10]u8 = undefined;
const ptr = &array;
try expect(@TypeOf(ptr) == *[10]u8);
// 指向数组的指针可以像数组一样被切片:
var start: usize = 0;
var end: usize = 5;
_ = .{ &start, &end };
const slice = ptr[start..end];
// 切片是可变的,因为我们切片了一个可变指针。
try expect(@TypeOf(slice) == []u8);
slice[2] = 3;
try expect(array[2] == 3);
// 同样,使用编译期已知的索引进行切片将产生另一个指向
// 数组的指针:
const ptr2 = slice[2..3];
try expect(ptr2.len == 1);
try expect(ptr2[0] == 3);
try expect(@TypeOf(ptr2) == *[1]u8);
}
$ zig test test_slices.zig 1/2 test_slices.test.using slices for strings...OK 2/2 test_slices.test.slice pointer...OK All 2 tests passed.
另请参阅:
哨兵终止切片 §
语法
[:x]T
是一个具有运行时已知长度的切片,并且还保证在由长度索引的元素处有一个哨兵值。该类型不保证在此之前没有哨兵元素。哨兵终止切片允许对
len 索引进行元素访问。
const std = @import("std");
const expect = std.testing.expect;
test "0-terminated slice" {
const slice: [:0]const u8 = "hello";
try expect(slice.len == 5);
try expect(slice[5] == 0);
}
$ zig test test_null_terminated_slice.zig 1/1 test_null_terminated_slice.test.0-terminated slice...OK All 1 tests passed.
哨兵终止切片也可以使用切片语法的变体
data[start..end :x] 创建,其中
data 是多项指针、数组或切片,x
是哨兵值。
const std = @import("std");
const expect = std.testing.expect;
test "0-terminated slicing" {
var array = [_]u8{ 3, 2, 1, 0, 3, 2, 1, 0 };
var runtime_length: usize = 3;
_ = &runtime_length;
const slice = array[0..runtime_length :0];
try expect(@TypeOf(slice) == [:0]u8);
try expect(slice.len == 3);
}
$ zig test test_null_terminated_slicing.zig 1/1 test_null_terminated_slicing.test.0-terminated slicing...OK All 1 tests passed.
哨兵终止切片断言后备数据的哨兵位置中的元素实际上是哨兵值。如果不是这种情况,将导致安全检查的非法行为。
const std = @import("std");
const expect = std.testing.expect;
test "sentinel mismatch" {
var array = [_]u8{ 3, 2, 1, 0 };
// 从长度为 2 的数组创建哨兵终止切片
// 将导致值 `1` 占据哨兵元素位置。
// 这与指示的哨兵值 `0` 不匹配,并将导致
// 运行时 panic。
var runtime_length: usize = 2;
_ = &runtime_length;
const slice = array[0..runtime_length :0];
_ = slice;
}
$ zig test test_sentinel_mismatch.zig 1/1 test_sentinel_mismatch.test.sentinel mismatch...thread 2902472 panic: sentinel mismatch: expected 0, found 1 /home/andy/dev/zig/doc/langref/test_sentinel_mismatch.zig:13:24: 0x102c117 in test.sentinel mismatch (test_sentinel_mismatch.zig) const slice = array[0..runtime_length :0]; ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:218:25: 0x115cc90 in mainTerminal (test_runner.zig) if (test_fn.func()) |_| { ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:66:28: 0x1155eb1 in main (test_runner.zig) return mainTerminal(); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x114fc4d in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x114f4e1 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) error: the following test command crashed: /home/andy/dev/zig/.zig-cache/o/12c6cfa0971ea7c724c8448a09f20f6b/test --seed=0xb506c876
另请参阅:
struct §
// 声明一个结构体。
// Zig 对字段的顺序和结构体的大小不做保证,但
// 字段保证是 ABI 对齐的。
const Point = struct {
x: f32,
y: f32,
};
// 声明结构体的实例。
const p: Point = .{
.x = 0.12,
.y = 0.34,
};
// 结构体命名空间中的函数可以使用点语法调用。
const Vec3 = struct {
x: f32,
y: f32,
z: f32,
pub fn init(x: f32, y: f32, z: f32) Vec3 {
return Vec3{
.x = x,
.y = y,
.z = z,
};
}
pub fn dot(self: Vec3, other: Vec3) f32 {
return self.x * other.x + self.y * other.y + self.z * other.z;
}
};
test "dot product" {
const v1 = Vec3.init(1.0, 0.0, 0.0);
const v2 = Vec3.init(0.0, 1.0, 0.0);
try expect(v1.dot(v2) == 0.0);
// 除了可以使用点语法调用外,结构体方法
// 并不特殊。您可以像引用结构体内的任何其他声明一样
// 引用它们:
try expect(Vec3.dot(v1, v2) == 0.0);
}
// 结构体可以有声明。
// 结构体可以有 0 个字段。
const Empty = struct {
pub const PI = 3.14;
};
test "struct namespaced variable" {
try expect(Empty.PI == 3.14);
try expect(@sizeOf(Empty) == 0);
// 空结构体可以像往常一样实例化。
const does_nothing: Empty = .{};
_ = does_nothing;
}
// 结构体字段顺序由编译器决定,但是,基指针
// 可以从字段指针计算出来:
fn setYBasedOnX(x: *f32, y: f32) void {
const point: *Point = @fieldParentPtr("x", x);
point.y = y;
}
test "field parent pointer" {
var point = Point{
.x = 0.1234,
.y = 0.5678,
};
setYBasedOnX(&point.x, 0.9);
try expect(point.y == 0.9);
}
// 结构体可以从函数返回。
fn LinkedList(comptime T: type) type {
return struct {
pub const Node = struct {
prev: ?*Node,
next: ?*Node,
data: T,
};
first: ?*Node,
last: ?*Node,
len: usize,
};
}
test "linked list" {
// 在编译期调用的函数会被记忆化。
try expect(LinkedList(i32) == LinkedList(i32));
const list = LinkedList(i32){
.first = null,
.last = null,
.len = 0,
};
try expect(list.len == 0);
// 由于类型是一等值,您可以通过将类型
// 分配给变量来实例化类型:
const ListOfInts = LinkedList(i32);
try expect(ListOfInts == LinkedList(i32));
var node = ListOfInts.Node{
.prev = null,
.next = null,
.data = 1234,
};
const list2 = LinkedList(i32){
.first = &node,
.last = &node,
.len = 1,
};
// 当使用指向结构体的指针时,可以直接访问字段,
// 而无需显式解引用指针。
// 所以您可以这样做
try expect(list2.first.?.data == 1234);
// 而不是 try expect(list2.first.?.*.data == 1234);
}
const expect = @import("std").testing.expect;
$ zig test test_structs.zig 1/4 test_structs.test.dot product...OK 2/4 test_structs.test.struct namespaced variable...OK 3/4 test_structs.test.field parent pointer...OK 4/4 test_structs.test.linked list...OK All 4 tests passed.
默认字段值 §
每个结构体字段可能有一个表示默认字段值的表达式。此类表达式在编译期执行,并允许在结构体字面量表达式中省略该字段:
const Foo = struct {
a: i32 = 1234,
b: i32,
};
test "default struct initialization fields" {
const x: Foo = .{
.b = 5,
};
if (x.a + x.b != 1239) {
comptime unreachable;
}
}
$ zig test struct_default_field_values.zig 1/1 struct_default_field_values.test.default struct initialization fields...OK All 1 tests passed.
错误的默认字段值 §
默认字段值仅在从初始化中省略该字段不会违反结构体的数据不变量时才适用。
例如,这里是对默认结构体字段初始化的不当使用:
const Threshold = struct {
minimum: f32 = 0.25,
maximum: f32 = 0.75,
const Category = enum { low, medium, high };
fn categorize(t: Threshold, value: f32) Category {
assert(t.maximum >= t.minimum);
if (value < t.minimum) return .low;
if (value > t.maximum) return .high;
return .medium;
}
};
pub fn main() !void {
var threshold: Threshold = .{
.maximum = 0.20,
};
const category = threshold.categorize(0.90);
try std.fs.File.stdout().writeAll(@tagName(category));
}
const std = @import("std");
const assert = std.debug.assert;
$ zig build-exe bad_default_value.zig $ ./bad_default_value thread 2895237 panic: reached unreachable code /home/andy/dev/zig/lib/std/debug.zig:559:14: 0x1044179 in assert (std.zig) if (!ok) unreachable; // assertion failure ^ /home/andy/dev/zig/doc/langref/bad_default_value.zig:8:15: 0x113ec54 in categorize (bad_default_value.zig) assert(t.maximum >= t.minimum); ^ /home/andy/dev/zig/doc/langref/bad_default_value.zig:19:42: 0x113d444 in main (bad_default_value.zig) const category = threshold.categorize(0.90); ^ /home/andy/dev/zig/lib/std/start.zig:627:37: 0x113dca9 in posixCallMainAndExit (std.zig) const result = root.main() catch |err| { ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (process terminated by signal)
上面您可以看到忽略此原则的危险。默认字段值导致数据不变量被违反,从而导致非法行为。
要解决此问题,请从所有结构体字段中删除默认值,并提供一个命名的默认值:
const Threshold = struct {
minimum: f32,
maximum: f32,
const default: Threshold = .{
.minimum = 0.25,
.maximum = 0.75,
};
};
如果结构体值需要运行时已知的值才能在不违反数据不变量的情况下初始化,则使用接受这些运行时值的初始化方法,并填充其余字段。
extern struct §
extern
struct
具有与目标的 C ABI 匹配的内存布局。
如果不需要明确定义的内存布局,struct 是更好的选择,因为它对编译器的限制较少。
参见 packed struct,它具有其后备整数的 ABI,这对于建模标志很有用。
另请参阅:
packed struct §
与
enum
一样,packed
结构体基于以不同方式解释整数的概念。所有紧凑结构体都有一个后备整数,该整数由字段的总位数隐式确定,或显式指定。紧凑结构体具有明确定义的内存布局
- 与其后备整数完全相同的 ABI。
紧凑结构体的每个字段被解释为逻辑位序列,从最低有效位到最高有效位排列。允许的字段类型:
-
整数字段使用与其位宽完全相同的位数。例如,
u5将使用后备整数的 5 位。 - bool 字段恰好使用 1 位。
- enum 字段恰好使用其整数标签类型的位宽。
- packed union 字段恰好使用具有最大位宽的联合字段的位宽。
-
packed struct字段使用其后备整数的位。
这意味着
packed
struct
可以参与 @bitCast 或
@ptrCast
以重新解释内存。这甚至在编译期也有效:
const std = @import("std");
const native_endian = @import("builtin").target.cpu.arch.endian();
const expect = std.testing.expect;
const Full = packed struct {
number: u16,
};
const Divided = packed struct {
half1: u8,
quarter3: u4,
quarter4: u4,
};
test "@bitCast between packed structs" {
try doTheTest();
try comptime doTheTest();
}
fn doTheTest() !void {
try expect(@sizeOf(Full) == 2);
try expect(@sizeOf(Divided) == 2);
const full = Full{ .number = 0x1234 };
const divided: Divided = @bitCast(full);
try expect(divided.half1 == 0x34);
try expect(divided.quarter3 == 0x2);
try expect(divided.quarter4 == 0x1);
const ordered: [2]u8 = @bitCast(full);
switch (native_endian) {
.big => {
try expect(ordered[0] == 0x12);
try expect(ordered[1] == 0x34);
},
.little => {
try expect(ordered[0] == 0x34);
try expect(ordered[1] == 0x12);
},
}
}
$ zig test test_packed_structs.zig 1/1 test_packed_structs.test.@bitCast between packed structs...OK All 1 tests passed.
后备整数可以被推断或显式提供。当推断时,它将是无符号的。当显式提供时,其位宽将在编译期强制执行,以完全匹配字段的总位宽:
test "missized packed struct" {
const S = packed struct(u32) { a: u16, b: u8 };
_ = S{ .a = 4, .b = 2 };
}
$ zig test test_missized_packed_struct.zig /home/andy/dev/zig/doc/langref/test_missized_packed_struct.zig:2:29: error: backing integer type 'u32' has bit size 32 but the struct fields have a total bit size of 24 const S = packed struct(u32) { a: u16, b: u8 }; ^~~ referenced by: test.missized packed struct: /home/andy/dev/zig/doc/langref/test_missized_packed_struct.zig:2:22
Zig 允许获取非字节对齐字段的地址:
const std = @import("std");
const expect = std.testing.expect;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
var foo = BitField{
.a = 1,
.b = 2,
.c = 3,
};
test "pointer to non-byte-aligned field" {
const ptr = &foo.b;
try expect(ptr.* == 2);
}
$ zig test test_pointer_to_non-byte_aligned_field.zig 1/1 test_pointer_to_non-byte_aligned_field.test.pointer to non-byte-aligned field...OK All 1 tests passed.
然而,指向非字节对齐字段的指针具有特殊属性,在预期普通指针的地方不能传递:
const std = @import("std");
const expect = std.testing.expect;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
var bit_field = BitField{
.a = 1,
.b = 2,
.c = 3,
};
test "pointer to non-byte-aligned field" {
try expect(bar(&bit_field.b) == 2);
}
fn bar(x: *const u3) u3 {
return x.*;
}
$ zig test test_misaligned_pointer.zig /home/andy/dev/zig/doc/langref/test_misaligned_pointer.zig:17:20: error: expected type '*const u3', found '*align(1:3:1) u3' try expect(bar(&bit_field.b) == 2); ^~~~~~~~~~~~ /home/andy/dev/zig/doc/langref/test_misaligned_pointer.zig:17:20: note: pointer host size '1' cannot cast into pointer host size '0' /home/andy/dev/zig/doc/langref/test_misaligned_pointer.zig:17:20: note: pointer bit offset '3' cannot cast into pointer bit offset '0' /home/andy/dev/zig/doc/langref/test_misaligned_pointer.zig:20:11: note: parameter type declared here fn bar(x: *const u3) u3 { ^~~~~~~~~
在这种情况下,函数 bar 不能被调用,因为指向非
ABI 对齐字段的指针包含位偏移信息,但函数期望一个 ABI
对齐的指针。
指向非 ABI 对齐字段的指针与其宿主整数内的其他字段共享相同的地址:
const std = @import("std");
const expect = std.testing.expect;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
var bit_field = BitField{
.a = 1,
.b = 2,
.c = 3,
};
test "pointers of sub-byte-aligned fields share addresses" {
try expect(@intFromPtr(&bit_field.a) == @intFromPtr(&bit_field.b));
try expect(@intFromPtr(&bit_field.a) == @intFromPtr(&bit_field.c));
}
$ zig test test_packed_struct_field_address.zig 1/1 test_packed_struct_field_address.test.pointers of sub-byte-aligned fields share addresses...OK All 1 tests passed.
这可以通过 @bitOffsetOf 和 offsetOf 观察到:
const std = @import("std");
const expect = std.testing.expect;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
test "offsets of non-byte-aligned fields" {
comptime {
try expect(@bitOffsetOf(BitField, "a") == 0);
try expect(@bitOffsetOf(BitField, "b") == 3);
try expect(@bitOffsetOf(BitField, "c") == 6);
try expect(@offsetOf(BitField, "a") == 0);
try expect(@offsetOf(BitField, "b") == 0);
try expect(@offsetOf(BitField, "c") == 0);
}
}
$ zig test test_bitOffsetOf_offsetOf.zig 1/1 test_bitOffsetOf_offsetOf.test.offsets of non-byte-aligned fields...OK All 1 tests passed.
紧凑结构体的对齐方式与其后备整数相同,但是,指向紧凑结构体的过度对齐指针可以覆盖这一点:
const std = @import("std");
const expect = std.testing.expect;
const S = packed struct {
a: u32,
b: u32,
};
test "overaligned pointer to packed struct" {
var foo: S align(4) = .{ .a = 1, .b = 2 };
const ptr: *align(4) S = &foo;
const ptr_to_b: *u32 = &ptr.b;
try expect(ptr_to_b.* == 2);
}
$ zig test test_overaligned_packed_struct.zig 1/1 test_overaligned_packed_struct.test.overaligned pointer to packed struct...OK All 1 tests passed.
也可以设置结构体字段的对齐方式:
const std = @import("std");
const expectEqual = std.testing.expectEqual;
test "aligned struct fields" {
const S = struct {
a: u32 align(2),
b: u32 align(64),
};
var foo = S{ .a = 1, .b = 2 };
try expectEqual(64, @alignOf(S));
try expectEqual(*align(2) u32, @TypeOf(&foo.a));
try expectEqual(*align(64) u32, @TypeOf(&foo.b));
}
$ zig test test_aligned_struct_fields.zig 1/1 test_aligned_struct_fields.test.aligned struct fields...OK All 1 tests passed.
比较紧凑结构体会导致比较后备整数, 并且仅适用于
== 和 !=
运算符。
const std = @import("std");
const expect = std.testing.expect;
test "packed struct equality" {
const S = packed struct {
a: u4,
b: u4,
};
const x: S = .{ .a = 1, .b = 2 };
const y: S = .{ .b = 2, .a = 1 };
try expect(x == y);
}
$ zig test test_packed_struct_equality.zig 1/1 test_packed_struct_equality.test.packed struct equality...OK All 1 tests passed.
字段访问和赋值可以理解为对后备整数进行位移操作的简写。这些操作不是原子的, 因此在结合内存映射输入输出(MMIO)使用字段访问语法时要小心。不要在 volatile 指针上使用字段访问, 而是先构造一个完整的新值,然后将该值写入 volatile 指针。
pub const GpioRegister = packed struct(u8) {
GPIO0: bool,
GPIO1: bool,
GPIO2: bool,
GPIO3: bool,
reserved: u4 = 0,
};
const gpio: *volatile GpioRegister = @ptrFromInt(0x0123);
pub fn writeToGpio(new_states: GpioRegister) void {
// 不应该这样做的示例:
// 错误! gpio.GPIO0 = true; 错误!
// 而是这样做:
gpio.* = new_states;
}
结构体命名 §
由于所有结构体都是匿名的,Zig 会根据几条规则推断类型名称。
- 如果结构体在变量的初始化表达式中,它将以该变量命名。
-
如果结构体在
return表达式中,它将以它返回的函数命名,并附加序列化的参数值。 -
否则,结构体将获得一个名称,如
(filename.funcname__struct_ID)。 - 如果结构体在另一个结构体内声明,它将以父结构体和前述规则推断出的名称命名,用点分隔。
const std = @import("std");
pub fn main() void {
const Foo = struct {};
std.debug.print("variable: {s}\n", .{@typeName(Foo)});
std.debug.print("anonymous: {s}\n", .{@typeName(struct {})});
std.debug.print("function: {s}\n", .{@typeName(List(i32))});
}
fn List(comptime T: type) type {
return struct {
x: T,
};
}
$ zig build-exe struct_name.zig $ ./struct_name variable: struct_name.main.Foo anonymous: struct_name.main__struct_22691 function: struct_name.List(i32)
匿名结构体字面量 §
Zig 允许省略字面量的结构体类型。当结果被强制转换时, 结构体字面量将直接实例化结果位置, 无需复制:
const std = @import("std");
const expect = std.testing.expect;
const Point = struct { x: i32, y: i32 };
test "anonymous struct literal" {
const pt: Point = .{
.x = 13,
.y = 67,
};
try expect(pt.x == 13);
try expect(pt.y == 67);
}
$ zig test test_struct_result.zig 1/1 test_struct_result.test.anonymous struct literal...OK All 1 tests passed.
结构体类型可以被推断。这里结果位置 不包含类型,因此 Zig 推断类型:
const std = @import("std");
const expect = std.testing.expect;
test "fully anonymous struct" {
try check(.{
.int = @as(u32, 1234),
.float = @as(f64, 12.34),
.b = true,
.s = "hi",
});
}
fn check(args: anytype) !void {
try expect(args.int == 1234);
try expect(args.float == 12.34);
try expect(args.b);
try expect(args.s[0] == 'h');
try expect(args.s[1] == 'i');
}
$ zig test test_anonymous_struct.zig 1/1 test_anonymous_struct.test.fully anonymous struct...OK All 1 tests passed.
元组 §
匿名结构体可以在不指定字段名称的情况下创建,被称为"元组"。空元组看起来像
.{},可以在其中一个Hello World 示例中看到。
字段使用从 0 开始的数字隐式命名。因为它们的名称是整数,
所以不能使用
. 语法访问它们,除非也将它们包装在
@"" 中。@""
内的名称始终被识别为 标识符。
与数组一样,元组有一个 .len 字段,可以被索引(前提是索引在编译时已知) 并且可以与 ++ 和 ** 运算符一起使用。它们也可以使用 inline for 进行迭代。
const std = @import("std");
const expect = std.testing.expect;
test "tuple" {
const values = .{
@as(u32, 1234),
@as(f64, 12.34),
true,
"hi",
} ++ .{false} ** 2;
try expect(values[0] == 1234);
try expect(values[4] == false);
inline for (values, 0..) |v, i| {
if (i != 2) continue;
try expect(v);
}
try expect(values.len == 6);
try expect(values.@"3"[0] == 'h');
}
$ zig test test_tuples.zig 1/1 test_tuples.test.tuple...OK All 1 tests passed.
解构元组 §
元组可以被解构。
元组解构有助于从块中返回多个值:
const print = @import("std").debug.print;
pub fn main() void {
const digits = [_]i8 { 3, 8, 9, 0, 7, 4, 1 };
const min, const max = blk: {
var min: i8 = 127;
var max: i8 = -128;
for (digits) |digit| {
if (digit < min) min = digit;
if (digit > max) max = digit;
}
break :blk .{ min, max };
};
print("min = {}\n", .{ min });
print("max = {}\n", .{ max });
}
$ zig build-exe destructuring_block.zig $ ./destructuring_block min = 0 max = 9
元组解构有助于处理将多个值作为元组返回的函数和内置函数:
const print = @import("std").debug.print;
fn divmod(numerator: u32, denominator: u32) struct { u32, u32 } {
return .{ numerator / denominator, numerator % denominator };
}
pub fn main() void {
const div, const mod = divmod(10, 3);
print("10 / 3 = {}\n", .{div});
print("10 % 3 = {}\n", .{mod});
}
$ zig build-exe destructuring_return_value.zig $ ./destructuring_return_value 10 / 3 = 3 10 % 3 = 1
另见:
另见:
enum §
const expect = @import("std").testing.expect;
const mem = @import("std").mem;
// 声明一个枚举。
const Type = enum {
ok,
not_ok,
};
// 声明一个特定的枚举字段。
const c = Type.ok;
// 如果您想访问枚举的序数值,可以
// 指定标签类型。
const Value = enum(u2) {
zero,
one,
two,
};
// 现在您可以在 u2 和 Value 之间进行转换。
// 序数值从 0 开始,从前一个成员向上递增 1。
test "enum ordinal value" {
try expect(@intFromEnum(Value.zero) == 0);
try expect(@intFromEnum(Value.one) == 1);
try expect(@intFromEnum(Value.two) == 2);
}
// 您可以覆盖枚举的序数值。
const Value2 = enum(u32) {
hundred = 100,
thousand = 1000,
million = 1000000,
};
test "set enum ordinal value" {
try expect(@intFromEnum(Value2.hundred) == 100);
try expect(@intFromEnum(Value2.thousand) == 1000);
try expect(@intFromEnum(Value2.million) == 1000000);
}
// 您也可以仅覆盖某些值。
const Value3 = enum(u4) {
a,
b = 8,
c,
d = 4,
e,
};
test "enum implicit ordinal values and overridden values" {
try expect(@intFromEnum(Value3.a) == 0);
try expect(@intFromEnum(Value3.b) == 8);
try expect(@intFromEnum(Value3.c) == 9);
try expect(@intFromEnum(Value3.d) == 4);
try expect(@intFromEnum(Value3.e) == 5);
}
// 枚举可以有方法,与结构体和联合体相同。
// 枚举方法并不特殊,它们只是命名空间
// 函数,您可以使用点语法调用。
const Suit = enum {
clubs,
spades,
diamonds,
hearts,
pub fn isClubs(self: Suit) bool {
return self == Suit.clubs;
}
};
test "enum method" {
const p = Suit.spades;
try expect(!p.isClubs());
}
// 可以对枚举进行 switch。
const Foo = enum {
string,
number,
none,
};
test "enum switch" {
const p = Foo.number;
const what_is_it = switch (p) {
Foo.string => "this is a string",
Foo.number => "this is a number",
Foo.none => "this is a none",
};
try expect(mem.eql(u8, what_is_it, "this is a number"));
}
// @typeInfo 可用于访问枚举的整数标签类型。
const Small = enum {
one,
two,
three,
four,
};
test "std.meta.Tag" {
try expect(@typeInfo(Small).@"enum".tag_type == u2);
}
// @typeInfo 告诉我们字段数量和字段名称:
test "@typeInfo" {
try expect(@typeInfo(Small).@"enum".fields.len == 4);
try expect(mem.eql(u8, @typeInfo(Small).@"enum".fields[1].name, "two"));
}
// @tagName 给出枚举值的 [:0]const u8 表示:
test "@tagName" {
try expect(mem.eql(u8, @tagName(Small.three), "three"));
}
$ zig test test_enums.zig 1/8 test_enums.test.enum ordinal value...OK 2/8 test_enums.test.set enum ordinal value...OK 3/8 test_enums.test.enum implicit ordinal values and overridden values...OK 4/8 test_enums.test.enum method...OK 5/8 test_enums.test.enum switch...OK 6/8 test_enums.test.std.meta.Tag...OK 7/8 test_enums.test.@typeInfo...OK 8/8 test_enums.test.@tagName...OK All 8 tests passed.
另见:
extern enum §
默认情况下,枚举不保证与 C ABI 兼容:
const Foo = enum { a, b, c };
export fn entry(foo: Foo) void {
_ = foo;
}
$ zig build-obj enum_export_error.zig -target x86_64-linux /home/andy/dev/zig/doc/langref/enum_export_error.zig:2:17: error: parameter of type 'enum_export_error.Foo' not allowed in function with calling convention 'x86_64_sysv' export fn entry(foo: Foo) void { ^~~~~~~~ /home/andy/dev/zig/doc/langref/enum_export_error.zig:2:17: note: enum tag type 'u2' is not extern compatible /home/andy/dev/zig/doc/langref/enum_export_error.zig:2:17: note: only integers with 0, 8, 16, 32, 64 and 128 bits are extern compatible /home/andy/dev/zig/doc/langref/enum_export_error.zig:1:13: note: enum declared here const Foo = enum { a, b, c }; ^~~~~~~~~~~~~~~~ referenced by: root: /home/andy/dev/zig/lib/std/start.zig:3:22 comptime: /home/andy/dev/zig/lib/std/start.zig:31:9 2 reference(s) hidden; use '-freference-trace=4' to see all references
对于 C ABI 兼容的枚举,为枚举提供显式标签类型:
const Foo = enum(c_int) { a, b, c };
export fn entry(foo: Foo) void {
_ = foo;
}
$ zig build-obj enum_export.zig
枚举字面量 §
枚举字面量允许在不指定枚举类型的情况下指定枚举字段的名称:
const std = @import("std");
const expect = std.testing.expect;
const Color = enum {
auto,
off,
on,
};
test "enum literals" {
const color1: Color = .auto;
const color2 = Color.auto;
try expect(color1 == color2);
}
test "switch using enum literals" {
const color = Color.on;
const result = switch (color) {
.auto => false,
.on => true,
.off => false,
};
try expect(result);
}
$ zig test test_enum_literals.zig 1/2 test_enum_literals.test.enum literals...OK 2/2 test_enum_literals.test.switch using enum literals...OK All 2 tests passed.
非穷尽枚举 §
可以通过添加尾部 _ 字段创建非穷尽枚举。
枚举必须指定标签类型,并且不能消耗每个枚举值。
对非穷尽枚举使用 @enumFromInt 涉及 @intCast 到整数标签类型的安全语义,但除此之外始终会产生 定义良好的枚举值。
对非穷尽枚举的 switch 可以包含 _ 分支作为
else
分支的替代。 使用 _ 分支时,如果 switch
未处理所有已知标签名称,编译器会报错。
const std = @import("std");
const expect = std.testing.expect;
const Number = enum(u8) {
one,
two,
three,
_,
};
test "switch on non-exhaustive enum" {
const number = Number.one;
const result = switch (number) {
.one => true,
.two, .three => false,
_ => false,
};
try expect(result);
const is_one = switch (number) {
.one => true,
else => false,
};
try expect(is_one);
}
$ zig test test_switch_non-exhaustive.zig 1/1 test_switch_non-exhaustive.test.switch on non-exhaustive enum...OK All 1 tests passed.
union §
裸
union
将值可以是的一组可能类型定义为字段列表。一次只能有一个字段处于活动状态。
裸联合体的内存表示不受保证。
裸联合体不能用于重新解释内存。为此,请使用
@ptrCast,
或使用具有保证内存布局的
extern union 或
packed union。
访问非活动字段是
经过安全检查的非法行为:
const Payload = union {
int: i64,
float: f64,
boolean: bool,
};
test "simple union" {
var payload = Payload{ .int = 1234 };
payload.float = 12.34;
}
$ zig test test_wrong_union_access.zig 1/1 test_wrong_union_access.test.simple union...thread 2895385 panic: access of union field 'float' while field 'int' is active /home/andy/dev/zig/doc/langref/test_wrong_union_access.zig:8:12: 0x102c083 in test.simple union (test_wrong_union_access.zig) payload.float = 12.34; ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:218:25: 0x115cdb0 in mainTerminal (test_runner.zig) if (test_fn.func()) |_| { ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:66:28: 0x1155fd1 in main (test_runner.zig) return mainTerminal(); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x114fd6d in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x114f601 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) error: the following test command crashed: /home/andy/dev/zig/.zig-cache/o/ceece336399a577bb1b9c6460feb4406/test --seed=0xa290ca33
您可以通过赋值整个联合体来激活另一个字段:
const std = @import("std");
const expect = std.testing.expect;
const Payload = union {
int: i64,
float: f64,
boolean: bool,
};
test "simple union" {
var payload = Payload{ .int = 1234 };
try expect(payload.int == 1234);
payload = Payload{ .float = 12.34 };
try expect(payload.float == 12.34);
}
$ zig test test_simple_union.zig 1/1 test_simple_union.test.simple union...OK All 1 tests passed.
当标签是 comptime 已知名称时,要初始化联合体,请参阅 @unionInit。
标记联合体 §
联合体可以使用枚举标签类型声明。 这会将联合体变成标记联合体,使其有资格 与 switch 表达式一起使用。 标记联合体强制转换为其标签类型:类型强制转换:联合体和枚举。
const std = @import("std");
const expect = std.testing.expect;
const ComplexTypeTag = enum {
ok,
not_ok,
};
const ComplexType = union(ComplexTypeTag) {
ok: u8,
not_ok: void,
};
test "switch on tagged union" {
const c = ComplexType{ .ok = 42 };
try expect(@as(ComplexTypeTag, c) == ComplexTypeTag.ok);
switch (c) {
.ok => |value| try expect(value == 42),
.not_ok => unreachable,
}
}
test "get tag type" {
try expect(std.meta.Tag(ComplexType) == ComplexTypeTag);
}
$ zig test test_tagged_union.zig 1/2 test_tagged_union.test.switch on tagged union...OK 2/2 test_tagged_union.test.get tag type...OK All 2 tests passed.
为了在 switch 表达式中修改标记联合体的有效负载,
在变量名称前放置 * 使其成为指针:
const std = @import("std");
const expect = std.testing.expect;
const ComplexTypeTag = enum {
ok,
not_ok,
};
const ComplexType = union(ComplexTypeTag) {
ok: u8,
not_ok: void,
};
test "modify tagged union in switch" {
var c = ComplexType{ .ok = 42 };
switch (c) {
ComplexTypeTag.ok => |*value| value.* += 1,
ComplexTypeTag.not_ok => unreachable,
}
try expect(c.ok == 43);
}
$ zig test test_switch_modify_tagged_union.zig 1/1 test_switch_modify_tagged_union.test.modify tagged union in switch...OK All 1 tests passed.
联合体可以推断枚举标签类型。 此外,联合体可以像结构体和枚举一样具有方法。
const std = @import("std");
const expect = std.testing.expect;
const Variant = union(enum) {
int: i32,
boolean: bool,
// 推断枚举标签类型时可以省略 void。
none,
fn truthy(self: Variant) bool {
return switch (self) {
Variant.int => |x_int| x_int != 0,
Variant.boolean => |x_bool| x_bool,
Variant.none => false,
};
}
};
test "union method" {
var v1: Variant = .{ .int = 1 };
var v2: Variant = .{ .boolean = false };
var v3: Variant = .none;
try expect(v1.truthy());
try expect(!v2.truthy());
try expect(!v3.truthy());
}
$ zig test test_union_method.zig 1/1 test_union_method.test.union method...OK All 1 tests passed.
具有推断枚举标签类型的联合体也可以为其推断标签分配序数值。 这要求标签指定显式整数类型。 @intFromEnum 可用于访问与活动字段对应的序数值。
const std = @import("std");
const expect = std.testing.expect;
const Tagged = union(enum(u32)) {
int: i64 = 123,
boolean: bool = 67,
};
test "tag values" {
const int: Tagged = .{ .int = -40 };
try expect(@intFromEnum(int) == 123);
const boolean: Tagged = .{ .boolean = false };
try expect(@intFromEnum(boolean) == 67);
}
$ zig test test_tagged_union_with_tag_values.zig 1/1 test_tagged_union_with_tag_values.test.tag values...OK All 1 tests passed.
@tagName 可用于返回表示字段名称的
comptime
[:0]const
u8
值:
const std = @import("std");
const expect = std.testing.expect;
const Small2 = union(enum) {
a: i32,
b: bool,
c: u8,
};
test "@tagName" {
try expect(std.mem.eql(u8, @tagName(Small2.a), "a"));
}
$ zig test test_tagName.zig 1/1 test_tagName.test.@tagName...OK All 1 tests passed.
extern union §
extern
union
的内存布局保证与目标 C ABI 兼容。
另见:
packed union §
packed
union
具有定义良好的内存布局,并且有资格 位于
packed struct 中。
匿名联合体字面量 §
匿名结构体字面量语法可用于在不指定类型的情况下初始化联合体:
const std = @import("std");
const expect = std.testing.expect;
const Number = union {
int: i32,
float: f64,
};
test "anonymous union literal syntax" {
const i: Number = .{ .int = 42 };
const f = makeNumber();
try expect(i.int == 42);
try expect(f.float == 12.34);
}
fn makeNumber() Number {
return .{ .float = 12.34 };
}
$ zig test test_anonymous_union.zig 1/1 test_anonymous_union.test.anonymous union literal syntax...OK All 1 tests passed.
opaque §
opaque {}
声明一个具有未知(但非零)大小和对齐方式的新类型。
它可以包含与 structs、unions
和 enums 相同的声明。
这通常用于与不公开结构体详细信息的 C 代码交互时的类型安全。 示例:
const Derp = opaque {};
const Wat = opaque {};
extern fn bar(d: *Derp) void;
fn foo(w: *Wat) callconv(.c) void {
bar(w);
}
test "call foo" {
foo(undefined);
}
$ zig test test_opaque.zig /home/andy/dev/zig/doc/langref/test_opaque.zig:6:9: error: expected type '*test_opaque.Derp', found '*test_opaque.Wat' bar(w); ^ /home/andy/dev/zig/doc/langref/test_opaque.zig:6:9: note: pointer type child 'test_opaque.Wat' cannot cast into pointer type child 'test_opaque.Derp' /home/andy/dev/zig/doc/langref/test_opaque.zig:2:13: note: opaque declared here const Wat = opaque {}; ^~~~~~~~~ /home/andy/dev/zig/doc/langref/test_opaque.zig:1:14: note: opaque declared here const Derp = opaque {}; ^~~~~~~~~ /home/andy/dev/zig/doc/langref/test_opaque.zig:4:18: note: parameter type declared here extern fn bar(d: *Derp) void; ^~~~~ referenced by: test.call foo: /home/andy/dev/zig/doc/langref/test_opaque.zig:10:8
块 §
块用于限制变量声明的作用域:
test "access variable after block scope" {
{
var x: i32 = 1;
_ = &x;
}
x += 1;
}
$ zig test test_blocks.zig /home/andy/dev/zig/doc/langref/test_blocks.zig:6:5: error: use of undeclared identifier 'x' x += 1; ^
块是表达式。当带有标签时,break
可用于 从块返回值:
const std = @import("std");
const expect = std.testing.expect;
test "labeled break from labeled block expression" {
var y: i32 = 123;
const x = blk: {
y += 1;
break :blk y;
};
try expect(x == 124);
try expect(y == 124);
}
$ zig test test_labeled_break.zig 1/1 test_labeled_break.test.labeled break from labeled block expression...OK All 1 tests passed.
这里,blk 可以是任何名称。
另见:
遮蔽 §
标识符永远不允许通过使用相同名称来"隐藏"其他标识符:
const pi = 3.14;
test "inside test block" {
// 让我们甚至进入另一个块
{
var pi: i32 = 1234;
}
}
$ zig test test_shadowing.zig /home/andy/dev/zig/doc/langref/test_shadowing.zig:6:13: error: local variable shadows declaration of 'pi' var pi: i32 = 1234; ^~ /home/andy/dev/zig/doc/langref/test_shadowing.zig:1:1: note: declared here const pi = 3.14; ^~~~~~~~~~~~~~~
因此,当您阅读 Zig 代码时,您始终可以依赖标识符在其定义的作用域内始终如一地表示相同的含义。但是请注意,如果 作用域是分开的,则可以使用相同的名称:
test "separate scopes" {
{
const pi = 3.14;
_ = pi;
}
{
var pi: bool = true;
_ = π
}
}
$ zig test test_scopes.zig 1/1 test_scopes.test.separate scopes...OK All 1 tests passed.
空块 §
空块等同于
void{}:
const std = @import("std");
const expect = std.testing.expect;
test {
const a = {};
const b = void{};
try expect(@TypeOf(a) == void);
try expect(@TypeOf(b) == void);
try expect(a == b);
}
$ zig test test_empty_block.zig 1/1 test_empty_block.test_0...OK All 1 tests passed.
switch §
const std = @import("std");
const builtin = @import("builtin");
const expect = std.testing.expect;
test "switch simple" {
const a: u64 = 10;
const zz: u64 = 103;
// switch 表达式的所有分支必须能够被强制转换为
// 通用类型。
//
// 分支不能贯穿。如果需要贯穿行为,请组合
// case 并使用 if。
const b = switch (a) {
// 可以通过 ',' 组合多个 case
1, 2, 3 => 0,
// 可以使用 ... 语法指定范围。这些范围包含
// 两端。
5...100 => 1,
// 分支可以任意复杂。
101 => blk: {
const c: u64 = 5;
break :blk c * 2 + 1;
},
// 允许对任意表达式进行 switch,只要该
// 表达式在编译时已知。
zz => zz,
blk: {
const d: u32 = 5;
const e: u32 = 100;
break :blk d + e;
} => 107,
// else 分支捕获所有尚未捕获的内容。
// 除非处理整个值范围,否则 else 分支是
// 强制性的。
else => 9,
};
try expect(b == 1);
}
// Switch 表达式可以在函数外部使用:
const os_msg = switch (builtin.target.os.tag) {
.linux => "we found a linux user",
else => "not a linux user",
};
// 在函数内部,如果目标表达式在编译时已知,
// switch 语句会隐式地在编译时求值。
test "switch inside function" {
switch (builtin.target.os.tag) {
.fuchsia => {
// 在 fuchsia 以外的操作系统上,块甚至不会被分析,
// 因此不会触发此编译错误。
// 在 fuchsia 上会触发此编译错误。
@compileError("fuchsia not supported");
},
else => {},
}
}
$ zig test test_switch.zig 1/2 test_switch.test.switch simple...OK 2/2 test_switch.test.switch inside function...OK All 2 tests passed.
switch
可用于捕获标记联合体的字段值。可以通过在捕获变量名称前放置
* 来修改字段值, 将其转换为指针。
const expect = @import("std").testing.expect;
test "switch on tagged union" {
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 } };
// 允许对更复杂的枚举进行 switch。
const b = switch (a) {
// 匹配允许使用捕获组,并将返回匹配的枚举
// 值。如果两个 case 的有效负载类型相同,
// 它们可以放入同一个 switch 分支。
Item.a, Item.e => |item| item,
// 可以使用 `*` 语法获取匹配值的引用。
Item.c => |*item| blk: {
item.*.x += 1;
break :blk 6;
},
// 如果 case 类型穷尽处理,则不需要 else
Item.d => 8,
};
try expect(b == 6);
try expect(a.c.x == 2);
}
$ zig test test_switch_tagged_union.zig 1/1 test_switch_tagged_union.test.switch on tagged union...OK All 1 tests passed.
另见:
穷尽 Switch §
当
switch
表达式没有
else 子句时,
它必须穷尽地列出所有可能的值。不这样做会导致编译错误:
const Color = enum {
auto,
off,
on,
};
test "穷尽切换" {
const color = Color.off;
switch (color) {
Color.auto => {},
Color.on => {},
}
}
$ zig test test_unhandled_enumeration_value.zig /home/andy/dev/zig/doc/langref/test_unhandled_enumeration_value.zig:9:5: error: switch 必须处理所有可能性 switch (color) { ^~~~~~ /home/andy/dev/zig/doc/langref/test_unhandled_enumeration_value.zig:3:5: note: 未处理的枚举值: 'off' off, ^~~ /home/andy/dev/zig/doc/langref/test_unhandled_enumeration_value.zig:1:15: note: 枚举 'test_unhandled_enumeration_value.Color' 在此声明 const Color = enum { ^~~~
使用枚举字面量进行切换 §
枚举字面量可以方便地与
switch
一起使用,以避免 重复指定枚举或联合类型:
const std = @import("std");
const expect = std.testing.expect;
const Color = enum {
auto,
off,
on,
};
test "switch 中的枚举字面量" {
const color = Color.off;
const result = switch (color) {
.auto => false,
.on => false,
.off => true,
};
try expect(result);
}
$ zig test test_exhaustive_switch.zig 1/1 test_exhaustive_switch.test.switch 中的枚举字面量...OK All 1 tests passed.
带标签的 switch §
当 switch 语句带有标签时,可以从
break 或
continue
中引用它。
break 将从
switch
返回一个值。
针对 switch 的
continue
必须有一个
操作数。当执行时,它将跳转到匹配的分支,就好像
switch 使用
continue
的操作数替换初始 switch 值再次执行一样。
const std = @import("std");
test "switch continue" {
sw: switch (@as(i32, 5)) {
5 => continue :sw 4,
// `continue` 可以在单个 switch 分支中多次出现。
2...4 => |v| {
if (v > 3) {
continue :sw 2;
} else if (v == 3) {
// `break` 可以针对带标签的循环。
break :sw;
}
continue :sw 1;
},
1 => return,
else => unreachable,
}
}
$ zig test test_switch_continue.zig 1/1 test_switch_continue.test.switch continue...OK All 1 tests passed.
从语义上讲,这等价于以下循环:
const std = @import("std");
test "switch continue, 等价循环" {
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,
}
}
}
$ zig test test_switch_continue_equivalent.zig 1/1 test_switch_continue_equivalent.test.switch continue, 等价循环...OK All 1 tests passed.
这可以提高(例如)状态机的清晰度,其中语法
continue :sw
.next_state
是明确的、显式的且易于理解。
然而,这个功能的设计初衷是对数组的每个元素进行 switch,其中使用单个 switch 可以提高清晰度和性能:
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const Instruction = enum {
add,
mul,
end,
};
fn evaluate(initial_stack: []const i32, code: []const Instruction) !i32 {
var buffer: [8]i32 = undefined;
var stack = std.ArrayListUnmanaged(i32).initBuffer(&buffer);
try stack.appendSliceBounded(initial_stack);
var ip: usize = 0;
return vm: switch (code[ip]) {
// 因为 `continue` 之后的所有代码都是不可达的,这个分支不会
// 提供结果。
.add => {
try stack.appendBounded(stack.pop().? + stack.pop().?);
ip += 1;
continue :vm code[ip];
},
.mul => {
try stack.appendBounded(stack.pop().? * stack.pop().?);
ip += 1;
continue :vm code[ip];
},
.end => stack.pop().?,
};
}
test "evaluate" {
const result = try evaluate(&.{ 7, 2, -3 }, &.{ .mul, .add, .end });
try expectEqual(1, result);
}
$ zig test test_switch_dispatch_loop.zig 1/1 test_switch_dispatch_loop.test.evaluate...OK All 1 tests passed.
如果
continue
的操作数是
comptime
已知的,那么它可以被降低为对相关 case
的无条件分支。这样的分支被完美预测,因此
通常执行速度非常快。
如果操作数是运行时已知的,每个
continue 都可以
内联嵌入条件分支(理想情况下通过跳转表),这 允许 CPU
独立于任何其他分支预测其目标。基于
循环的降低会强制每个分支通过相同的分派点,阻碍分支预测。
内联 Switch 分支 §
Switch 分支可以标记为
inline
以为其可能具有的每个值生成
该分支的主体,使捕获的值成为comptime。
const std = @import("std");
const expect = std.testing.expect;
const expectError = std.testing.expectError;
fn isFieldOptional(comptime T: type, field_index: usize) !bool {
const fields = @typeInfo(T).@"struct".fields;
return switch (field_index) {
// 这个分支被分析两次,每次 `idx` 都是一个
// 编译期已知的值。
inline 0, 1 => |idx| @typeInfo(fields[idx].type) == .optional,
else => return error.IndexOutOfBounds,
};
}
const Struct1 = struct { a: u32, b: ?u32 };
test "将 @typeInfo 用于运行时值" {
var index: usize = 0;
try expect(!try isFieldOptional(Struct1, index));
index += 1;
try expect(try isFieldOptional(Struct1, index));
index += 1;
try expectError(error.IndexOutOfBounds, isFieldOptional(Struct1, index));
}
// 对 `Struct1` 的 `isFieldOptional` 调用被展开为等价于
// 这个函数:
fn isFieldOptionalUnrolled(field_index: usize) !bool {
return switch (field_index) {
0 => false,
1 => true,
else => return error.IndexOutOfBounds,
};
}
$ zig test test_inline_switch.zig 1/1 test_inline_switch.test.将 @typeInfo 用于运行时值...OK All 1 tests passed.
inline
关键字也可以与范围结合使用:
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| @typeInfo(fields[idx].type) == .optional,
else => return error.IndexOutOfBounds,
};
}
inline
else
分支可以用作
inline
for
循环的类型安全替代:
const std = @import("std");
const expect = std.testing.expect;
const SliceTypeA = extern struct {
len: usize,
ptr: [*]u32,
};
const SliceTypeB = extern struct {
ptr: [*]SliceTypeA,
len: usize,
};
const AnySlice = union(enum) {
a: SliceTypeA,
b: SliceTypeB,
c: []const u8,
d: []AnySlice,
};
fn withFor(any: AnySlice) usize {
const Tag = @typeInfo(AnySlice).@"union".tag_type.?;
inline for (@typeInfo(Tag).@"enum".fields) |field| {
// 使用 `inline for` 函数被生成为
// 一系列 `if` 语句,依赖优化器
// 将其转换为 switch。
if (field.value == @intFromEnum(any)) {
return @field(any, field.name).len;
}
}
// 当使用 `inline for` 时,编译器不知道每个
// 可能的情况都已处理,需要显式的 `unreachable`。
unreachable;
}
fn withSwitch(any: AnySlice) usize {
return switch (any) {
// 使用 `inline else` 函数被显式生成为
// 所需的 switch,编译器可以检查
// 每个可能的情况是否已处理。
inline else => |slice| slice.len,
};
}
test "inline for 和 inline else 的相似性" {
const any = AnySlice{ .c = "hello" };
try expect(withFor(any) == 5);
try expect(withSwitch(any) == 5);
}
$ zig test test_inline_else.zig 1/1 test_inline_else.test.inline for 和 inline else 的相似性...OK All 1 tests passed.
当对联合使用内联分支时,可以使用额外的捕获来获取联合的枚举标签值。
const std = @import("std");
const expect = std.testing.expect;
const U = union(enum) {
a: u32,
b: f32,
};
fn getNum(u: U) u32 {
switch (u) {
// 这里 `num` 是一个运行时已知值,它要么是
// `u.a` 要么是 `u.b`,而 `tag` 是 `u` 的编译期已知标签值。
inline else => |num, tag| {
if (tag == .b) {
return @intFromFloat(num);
}
return num;
},
}
}
test "test" {
const u = U{ .b = 42 };
try expect(getNum(u) == 42);
}
$ zig test test_inline_switch_union_tag.zig 1/1 test_inline_switch_union_tag.test.test...OK All 1 tests passed.
另请参阅:
while §
while 循环用于重复执行表达式,直到某个条件不再为真。
const expect = @import("std").testing.expect;
test "while 基础" {
var i: usize = 0;
while (i < 10) {
i += 1;
}
try expect(i == 10);
}
$ zig test test_while.zig 1/1 test_while.test.while 基础...OK All 1 tests passed.
使用
break 提前退出
while 循环。
const expect = @import("std").testing.expect;
test "while break" {
var i: usize = 0;
while (true) {
if (i == 10)
break;
i += 1;
}
try expect(i == 10);
}
$ zig test test_while_break.zig 1/1 test_while_break.test.while break...OK All 1 tests passed.
使用
continue
跳回循环的开头。
const expect = @import("std").testing.expect;
test "while continue" {
var i: usize = 0;
while (true) {
i += 1;
if (i < 10)
continue;
break;
}
try expect(i == 10);
}
$ zig test test_while_continue.zig 1/1 test_while_continue.test.while continue...OK All 1 tests passed.
while 循环支持 continue
表达式,它在循环继续时执行。continue
关键字会遵守这个表达式。
const expect = @import("std").testing.expect;
test "while 循环 continue 表达式" {
var i: usize = 0;
while (i < 10) : (i += 1) {}
try expect(i == 10);
}
test "while 循环 continue 表达式, 更复杂" {
var i: usize = 1;
var j: usize = 1;
while (i * j < 2000) : ({
i *= 2;
j *= 3;
}) {
const my_ij = i * j;
try expect(my_ij < 2000);
}
}
$ zig test test_while_continue_expression.zig 1/2 test_while_continue_expression.test.while 循环 continue 表达式...OK 2/2 test_while_continue_expression.test.while 循环 continue 表达式, 更复杂...OK All 2 tests passed.
while 循环是表达式。表达式的结果是 while 循环的
else
子句的结果,该子句在 while 循环的条件测试为 false
时执行。
break,类似于 return,接受一个值参数。这是
while
表达式的结果。 当你从 while 循环
break 时,else
分支不会被求值。
const expect = @import("std").testing.expect;
test "while else" {
try expect(rangeHasNumber(0, 10, 5));
try expect(!rangeHasNumber(0, 10, 15));
}
fn rangeHasNumber(begin: usize, end: usize, number: usize) bool {
var i = begin;
return while (i < end) : (i += 1) {
if (i == number) {
break true;
}
} else false;
}
$ zig test test_while_else.zig 1/1 test_while_else.test.while else...OK All 1 tests passed.
带标签的 while §
当
while
循环带有标签时,可以从嵌套循环内的
break 或
continue
中引用它:
test "嵌套 break" {
outer: while (true) {
while (true) {
break :outer;
}
}
}
test "嵌套 continue" {
var i: usize = 0;
outer: while (i < 10) : (i += 1) {
while (true) {
continue :outer;
}
}
}
$ zig test test_while_nested_break.zig 1/2 test_while_nested_break.test.嵌套 break...OK 2/2 test_while_nested_break.test.嵌套 continue...OK All 2 tests passed.
while 与可选值 §
就像 if 表达式一样,while 循环可以将可选值作为条件并捕获负载。当遇到 null 时循环退出。
当 |x| 语法出现在
while
表达式上时, while 条件必须具有可选类型。
else
分支在可选迭代中是允许的。在这种情况下,它将在遇到第一个
null 值时执行。
const expect = @import("std").testing.expect;
test "while null 捕获" {
var sum1: u32 = 0;
numbers_left = 3;
while (eventuallyNullSequence()) |value| {
sum1 += value;
}
try expect(sum1 == 3);
// null 捕获与 else 块
var sum2: u32 = 0;
numbers_left = 3;
while (eventuallyNullSequence()) |value| {
sum2 += value;
} else {
try expect(sum2 == 3);
}
// null 捕获与 continue 表达式
var i: u32 = 0;
var sum3: u32 = 0;
numbers_left = 3;
while (eventuallyNullSequence()) |value| : (i += 1) {
sum3 += value;
}
try expect(i == 3);
}
var numbers_left: u32 = undefined;
fn eventuallyNullSequence() ?u32 {
return if (numbers_left == 0) null else blk: {
numbers_left -= 1;
break :blk numbers_left;
};
}
$ zig test test_while_null_capture.zig 1/1 test_while_null_capture.test.while null 捕获...OK All 1 tests passed.
while 与错误联合 §
就像 if 表达式一样,while 循环可以将错误联合作为条件并捕获负载或错误代码。当条件导致错误代码时,else 分支被求值并且循环结束。
当
else |x|
语法出现在
while
表达式上时, while 条件必须具有错误联合类型。
const expect = @import("std").testing.expect;
test "while 错误联合捕获" {
var sum1: u32 = 0;
numbers_left = 3;
while (eventuallyErrorSequence()) |value| {
sum1 += value;
} else |err| {
try expect(err == error.ReachedZero);
}
}
var numbers_left: u32 = undefined;
fn eventuallyErrorSequence() anyerror!u32 {
return if (numbers_left == 0) error.ReachedZero else blk: {
numbers_left -= 1;
break :blk numbers_left;
};
}
$ zig test test_while_error_capture.zig 1/1 test_while_error_capture.test.while 错误联合捕获...OK All 1 tests passed.
内联 while §
while 循环可以被内联。这会导致循环被展开,这允许代码执行一些只能在编译时完成的操作,例如将类型用作一等值。
const expect = @import("std").testing.expect;
test "内联 while 循环" {
comptime var i = 0;
var sum: usize = 0;
inline while (i < 3) : (i += 1) {
const T = switch (i) {
0 => f32,
1 => i8,
2 => bool,
else => unreachable,
};
sum += typeNameLength(T);
}
try expect(sum == 9);
}
fn typeNameLength(comptime T: type) usize {
return @typeName(T).len;
}
$ zig test test_inline_while.zig 1/1 test_inline_while.test.内联 while 循环...OK All 1 tests passed.
建议仅出于以下原因之一使用
inline 循环:
- 你需要循环在编译期执行才能使语义生效。
- 你有基准测试证明以这种方式强制展开循环可测量地更快。
另请参阅:
for §
const expect = @import("std").testing.expect;
test "for 基础" {
const items = [_]i32{ 4, 5, 3, 4, 0 };
var sum: i32 = 0;
// For 循环迭代切片和数组。
for (items) |value| {
// 支持 Break 和 continue。
if (value == 0) {
continue;
}
sum += value;
}
try expect(sum == 16);
// 要迭代切片的一部分,请重新切片。
for (items[0..1]) |value| {
sum += value;
}
try expect(sum == 20);
// 要访问迭代索引,请指定第二个条件以及
// 第二个捕获值。
var sum2: i32 = 0;
for (items, 0..) |_, i| {
try expect(@TypeOf(i) == usize);
sum2 += @as(i32, @intCast(i));
}
try expect(sum2 == 10);
// 要迭代连续整数,请使用范围语法。
// 无界范围始终是编译错误。
var sum3: usize = 0;
for (0..5) |i| {
sum3 += i;
}
try expect(sum3 == 10);
}
test "多对象 for" {
const items = [_]usize{ 1, 2, 3 };
const items2 = [_]usize{ 4, 5, 6 };
var count: usize = 0;
// 迭代多个对象。
// 所有长度在循环开始时必须相等,否则会发生可检测的
// 非法行为。
for (items, items2) |i, j| {
count += i + j;
}
try expect(count == 21);
}
test "for 引用" {
var items = [_]i32{ 3, 4, 2 };
// 通过引用迭代切片,方法是
// 指定捕获值是指针。
for (&items) |*value| {
value.* += 1;
}
try expect(items[0] == 4);
try expect(items[1] == 5);
try expect(items[2] == 3);
}
test "for else" {
// For 允许附加一个 else,与 while 循环相同。
const items = [_]?i32{ 3, 4, null, 5 };
// For 循环也可以用作表达式。
// 与 while 循环类似,当你从 for 循环中断时,else 分支不会被求值。
var sum: i32 = 0;
const result = for (items) |value| {
if (value != null) {
sum += value.?;
}
} else blk: {
try expect(sum == 12);
break :blk sum;
};
try expect(result == 12);
}
$ zig test test_for.zig 1/4 test_for.test.for 基础...OK 2/4 test_for.test.多对象 for...OK 3/4 test_for.test.for 引用...OK 4/4 test_for.test.for else...OK All 4 tests passed.
带标签的 for §
当
for
循环带有标签时,可以从嵌套循环内的
break 或
continue
中引用它:
const std = @import("std");
const expect = std.testing.expect;
test "嵌套 break" {
var count: usize = 0;
outer: for (1..6) |_| {
for (1..6) |_| {
count += 1;
break :outer;
}
}
try expect(count == 1);
}
test "嵌套 continue" {
var count: usize = 0;
outer: for (1..9) |_| {
for (1..6) |_| {
count += 1;
continue :outer;
}
}
try expect(count == 8);
}
$ zig test test_for_nested_break.zig 1/2 test_for_nested_break.test.嵌套 break...OK 2/2 test_for_nested_break.test.嵌套 continue...OK All 2 tests passed.
内联 for §
for 循环可以被内联。这会导致循环被展开,这允许代码执行一些只能在编译时完成的操作,例如将类型用作一等值。 内联 for 循环的捕获值和迭代器值是编译期已知的。
const expect = @import("std").testing.expect;
test "内联 for 循环" {
const nums = [_]i32{ 2, 4, 6 };
var sum: usize = 0;
inline for (nums) |i| {
const T = switch (i) {
2 => f32,
4 => i8,
6 => bool,
else => unreachable,
};
sum += typeNameLength(T);
}
try expect(sum == 9);
}
fn typeNameLength(comptime T: type) usize {
return @typeName(T).len;
}
$ zig test test_inline_for.zig 1/1 test_inline_for.test.内联 for 循环...OK All 1 tests passed.
建议仅出于以下原因之一使用
inline 循环:
- 你需要循环在编译期执行才能使语义生效。
- 你有基准测试证明以这种方式强制展开循环可测量地更快。
另请参阅:
if §
// If 表达式有三种用法,对应三种类型:
// * bool
// * ?T
// * anyerror!T
const expect = @import("std").testing.expect;
test "if 表达式" {
// If 表达式用于代替三元表达式。
const a: u32 = 5;
const b: u32 = 4;
const result = if (a != b) 47 else 3089;
try expect(result == 47);
}
test "if 布尔值" {
// If 表达式测试布尔条件。
const a: u32 = 5;
const b: u32 = 4;
if (a != b) {
try expect(true);
} else if (a == 9) {
unreachable;
} else {
unreachable;
}
}
test "if 错误联合" {
// If 表达式测试错误。
// 注意 else 上的 |err| 捕获。
const a: anyerror!u32 = 0;
if (a) |value| {
try expect(value == 0);
} else |err| {
_ = err;
unreachable;
}
const b: anyerror!u32 = error.BadValue;
if (b) |value| {
_ = value;
unreachable;
} else |err| {
try expect(err == error.BadValue);
}
// else 和 |err| 捕获是严格要求的。
if (a) |value| {
try expect(value == 0);
} else |_| {}
// 要仅检查错误值,请使用空块表达式。
if (b) |_| {} else |err| {
try expect(err == error.BadValue);
}
// 使用指针捕获通过引用访问值。
var c: anyerror!u32 = 3;
if (c) |*value| {
value.* = 9;
} else |_| {
unreachable;
}
if (c) |value| {
try expect(value == 9);
} else |_| {
unreachable;
}
}
$ zig test test_if.zig 1/3 test_if.test.if 表达式...OK 2/3 test_if.test.if 布尔值...OK 3/3 test_if.test.if 错误联合...OK All 3 tests passed.
if 与可选值 §
const expect = @import("std").testing.expect;
test "if 可选值" {
// If 表达式测试 null。
const a: ?u32 = 0;
if (a) |value| {
try expect(value == 0);
} else {
unreachable;
}
const b: ?u32 = null;
if (b) |_| {
unreachable;
} else {
try expect(true);
}
// else 不是必需的。
if (a) |value| {
try expect(value == 0);
}
// 要仅测试 null,请使用二进制相等运算符。
if (b == null) {
try expect(true);
}
// 使用指针捕获通过引用访问值。
var c: ?u32 = 3;
if (c) |*value| {
value.* = 2;
}
if (c) |value| {
try expect(value == 2);
} else {
unreachable;
}
}
test "if 错误联合与可选值" {
// If 表达式在展开可选值之前先测试错误。
// |optional_value| 捕获的类型是 ?u32。
const a: anyerror!?u32 = 0;
if (a) |optional_value| {
try expect(optional_value.? == 0);
} else |err| {
_ = err;
unreachable;
}
const b: anyerror!?u32 = null;
if (b) |optional_value| {
try expect(optional_value == null);
} else |_| {
unreachable;
}
const c: anyerror!?u32 = error.BadValue;
if (c) |optional_value| {
_ = optional_value;
unreachable;
} else |err| {
try expect(err == error.BadValue);
}
// 每次使用指针捕获通过引用访问值。
var d: anyerror!?u32 = 3;
if (d) |*optional_value| {
if (optional_value.*) |*value| {
value.* = 9;
}
} else |_| {
unreachable;
}
if (d) |optional_value| {
try expect(optional_value.? == 9);
} else |_| {
unreachable;
}
}
$ zig test test_if_optionals.zig 1/2 test_if_optionals.test.if 可选值...OK 2/2 test_if_optionals.test.if 错误联合与可选值...OK All 2 tests passed.
另请参阅:
defer §
在作用域退出时无条件执行表达式。
const std = @import("std");
const expect = std.testing.expect;
const print = std.debug.print;
fn deferExample() !usize {
var a: usize = 1;
{
defer a = 2;
a = 1;
}
try expect(a == 2);
a = 5;
return a;
}
test "defer 基础" {
try expect((try deferExample()) == 5);
}
$ zig test test_defer.zig 1/1 test_defer.test.defer 基础...OK All 1 tests passed.
Defer 表达式按相反顺序求值。
const std = @import("std");
const print = std.debug.print;
pub fn main() void {
print("\n", .{});
defer {
print("1 ", .{});
}
defer {
print("2 ", .{});
}
if (false) {
// 如果 defer 从未执行,则它们不会运行。
defer {
print("3 ", .{});
}
}
}
$ zig build-exe defer_unwind.zig $ ./defer_unwind 2 1
在 defer 表达式内不允许使用 return 语句。
fn deferInvalidExample() !void {
defer {
return error.DeferError;
}
return error.DeferError;
}
$ zig test test_invalid_defer.zig /home/andy/dev/zig/doc/langref/test_invalid_defer.zig:3:9: error: 不能从 defer 表达式返回 return error.DeferError; ^~~~~~~~~~~~~~~~~~~~~~~ /home/andy/dev/zig/doc/langref/test_invalid_defer.zig:2:5: note: defer 表达式在此 defer { ^~~~~
另请参阅:
unreachable §
在调试和ReleaseSafe模式下,
unreachable
会发出对 panic 的调用,消息为
reached unreachable code。
在ReleaseFast和ReleaseSmall模式下,优化器使用永远不会命中
unreachable
代码的假设来执行优化。
基础 §
// unreachable 用于断言控制流永远不会到达特定位置:
test "基础数学" {
const x = 1;
const y = 2;
if (x + y != 3) {
unreachable;
}
}
$ zig test test_unreachable.zig 1/1 test_unreachable.test.基础数学...OK All 1 tests passed.
实际上,这就是 std.debug.assert 的实现方式:
// 这是 std.debug.assert 的实现方式
fn assert(ok: bool) void {
if (!ok) unreachable; // 断言失败
}
// 这个测试将失败,因为我们命中了 unreachable。
test "这将失败" {
assert(false);
}
$ zig test test_assertion_failure.zig 1/1 test_assertion_failure.test.这将失败...thread 2902460 panic: reached unreachable code /home/andy/dev/zig/doc/langref/test_assertion_failure.zig:3:14: 0x102c039 in assert (test_assertion_failure.zig) if (!ok) unreachable; // 断言失败 ^ /home/andy/dev/zig/doc/langref/test_assertion_failure.zig:8:11: 0x102c00e in test.这将失败 (test_assertion_failure.zig) assert(false); ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:218:25: 0x115cb50 in mainTerminal (test_runner.zig) if (test_fn.func()) |_| { ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:66:28: 0x1155d71 in main (test_runner.zig) return mainTerminal(); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x114fb0d in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x114f3a1 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) error: the following test command crashed: /home/andy/dev/zig/.zig-cache/o/2d8b23c255add16f67e238437a2ca75f/test --seed=0xf5bf1bba
在编译时 §
const assert = @import("std").debug.assert;
test "unreachable 的类型" {
comptime {
// unreachable 的类型是 noreturn。
// 然而,这个断言仍然会编译失败,因为
// unreachable 表达式是编译错误。
assert(@TypeOf(unreachable) == noreturn);
}
}
$ zig test test_comptime_unreachable.zig /home/andy/dev/zig/doc/langref/test_comptime_unreachable.zig:10:16: error: 不可达代码 assert(@TypeOf(unreachable) == noreturn); ^~~~~~~~~~~~~~~~~~~~ /home/andy/dev/zig/doc/langref/test_comptime_unreachable.zig:10:24: note: 控制流在此处被转移 assert(@TypeOf(unreachable) == noreturn); ^~~~~~~~~~~
另请参阅:
noreturn §
noreturn
是以下的类型:
-
break -
continue -
return -
unreachable -
while (true) {}
当一起解析类型时,例如
if 子句或
switch 分支,
noreturn
类型与所有其他类型兼容。考虑:
fn foo(condition: bool, b: u32) void {
const a = if (condition) b else return;
_ = a;
@panic("用 a 做些什么");
}
test "noreturn" {
foo(false, 1);
}
$ zig test test_noreturn.zig 1/1 test_noreturn.test.noreturn...OK All 1 tests passed.
noreturn
的另一个用例是 exit 函数:
const std = @import("std");
const builtin = @import("builtin");
const native_arch = builtin.cpu.arch;
const expect = std.testing.expect;
const WINAPI: std.builtin.CallingConvention = if (native_arch == .x86) .{ .x86_stdcall = .{} } else .c;
extern "kernel32" fn ExitProcess(exit_code: c_uint) callconv(WINAPI) noreturn;
test "foo" {
const value = bar() catch ExitProcess(1);
try expect(value == 1234);
}
fn bar() anyerror!u32 {
return 1234;
}
$ zig test test_noreturn_from_exit.zig -target x86_64-windows --test-no-exec
函数 §
const std = @import("std");
const builtin = @import("builtin");
const native_arch = builtin.cpu.arch;
const expect = std.testing.expect;
// 函数这样声明
fn add(a: i8, b: i8) i8 {
if (a == 0) {
return b;
}
return a + b;
}
// export 修饰符使函数在生成的目标文件中外部可见,并使其使用 C ABI。
export fn sub(a: i8, b: i8) i8 {
return a - b;
}
// extern 修饰符用于声明一个函数,该函数将在静态链接时在链接时解析,或
// 在动态链接时在运行时解析。extern 关键字后的引号标识符指定包含该函数的
// 库。(例如 "c" -> libc.so)
// callconv 修饰符改变函数的调用约定。
extern "kernel32" fn ExitProcess(exit_code: u32) callconv(.winapi) noreturn;
extern "c" fn atan2(a: f64, b: f64) f64;
// @branchHint 内置函数可用于告诉优化器某个函数很少被调用("cold")。
fn abort() noreturn {
@branchHint(.cold);
while (true) {}
}
// naked 调用约定使函数没有任何函数序言或尾声。
// 这在与汇编集成时很有用。
fn _start() callconv(.naked) noreturn {
abort();
}
// inline 调用约定强制函数在所有调用点内联。
// 如果函数无法内联,则是编译时错误。
inline fn shiftLeftOne(a: u32) u32 {
return a << 1;
}
// pub 修饰符允许在导入时函数可见。
// 另一个文件可以使用 @import 并调用 sub2
pub fn sub2(a: i8, b: i8) i8 {
return a - b;
}
// 函数指针以 `*const ` 为前缀。
const Call2Op = *const fn (a: i8, b: i8) i8;
fn doOp(fnCall: Call2Op, op1: i8, op2: i8) i8 {
return fnCall(op1, op2);
}
test "function" {
try expect(doOp(add, 5, 6) == 11);
try expect(doOp(sub2, 5, 6) == -1);
}
$ zig test test_functions.zig 1/1 test_functions.test.function...OK All 1 tests passed.
函数体和函数指针之间有所不同。 函数体是 comptime 专用类型,而函数指针可以在运行时已知。
传值参数 §
作为参数传递的基本类型,如整数和浮点数,会被复制,然后副本在函数体中可用。这称为"传值"。 复制基本类型基本上是免费的,通常只需要设置一个寄存器即可。
结构体、联合体和数组有时可以更高效地以引用方式传递,因为复制可能会根据大小而变得任意昂贵。当这些类型作为参数传递时,Zig 可能会选择复制并传值,或者传引用,Zig 会决定哪种方式更快。 这在一定程度上是因为参数是不可变的。
const Point = struct {
x: i32,
y: i32,
};
fn foo(point: Point) i32 {
// 这里,`point` 可能是引用,也可能是副本。函数体
// 可以忽略差异并将其视为值。获取参数地址时要非常小心 -
// 应该将其视为当函数返回时地址将失效。
return point.x + point.y;
}
const expect = @import("std").testing.expect;
test "pass struct to function" {
try expect(foo(Point{ .x = 1, .y = 2 }) == 3);
}
$ zig test test_pass_by_reference_or_value.zig 1/1 test_pass_by_reference_or_value.test.pass struct to function...OK All 1 tests passed.
对于外部函数,Zig 遵循 C ABI 以传值方式传递结构体和联合体。
函数参数类型推断 §
函数参数可以用
anytype
代替类型声明。 在这种情况下,参数类型将在调用函数时推断。
使用 @TypeOf 和
@typeInfo
获取有关推断类型的信息。
const expect = @import("std").testing.expect;
fn addFortyTwo(x: anytype) @TypeOf(x) {
return x + 42;
}
test "fn type inference" {
try expect(addFortyTwo(1) == 43);
try expect(@TypeOf(addFortyTwo(1)) == comptime_int);
const y: i64 = 2;
try expect(addFortyTwo(y) == 44);
try expect(@TypeOf(addFortyTwo(y)) == i64);
}
$ zig test test_fn_type_inference.zig 1/1 test_fn_type_inference.test.fn type inference...OK All 1 tests passed.
inline fn §
在函数定义中添加
inline
关键字会使该函数在调用点语义内联。这不是一个可能被优化过程观察到的提示,而是对函数调用中涉及的类型和值有影响。
与普通函数调用不同,内联函数调用点的编译时已知的参数被视为编译时参数。这可能会一直传播到返回值:
const std = @import("std");
pub fn main() void {
if (foo(1200, 34) != 1234) {
@compileError("bad");
}
}
inline fn foo(a: i32, b: i32) i32 {
std.debug.print("runtime a = {} b = {}", .{ a, b });
return a + b;
}
$ zig build-exe inline_call.zig $ ./inline_call runtime a = 1200 b = 34
如果移除 inline,测试将失败并产生编译错误而不是通过。
通常最好让编译器决定何时内联函数,除了以下场景:
- 为了调试目的改变调用栈中的栈帧数量。
- 强制参数的编译时性传播到函数的返回值,如上例所示。
- 实际性能测量要求这样做。
注意
inline
实际上限制了编译器被允许做的事情。这可能会损害二进制大小、编译速度,甚至运行时性能。
函数反射 §
const std = @import("std");
const math = std.math;
const testing = std.testing;
test "fn reflection" {
try testing.expect(@typeInfo(@TypeOf(testing.expect)).@"fn".params[0].type.? == bool);
try testing.expect(@typeInfo(@TypeOf(testing.tmpDir)).@"fn".return_type.? == testing.TmpDir);
try testing.expect(@typeInfo(@TypeOf(math.Log2Int)).@"fn".is_generic);
}
$ zig test test_fn_reflection.zig 1/1 test_fn_reflection.test.fn reflection...OK All 1 tests passed.
错误 §
错误集类型 §
错误集类似于 enum。 然而,整个编译过程中的每个错误名称都被分配一个大于 0 的无符号整数。你可以多次声明相同的错误名称,如果这样做,它会被分配相同的整数值。
错误集类型默认为
u16,但如果通过命令行参数
--error-limit [num]
提供了不同错误值的最大数量,则会使用能够表示所有错误值所需的最小位数的整数类型。
你可以将错误从子集强制转换为超集:
const std = @import("std");
const FileOpenError = error{
AccessDenied,
OutOfMemory,
FileNotFound,
};
const AllocationError = error{
OutOfMemory,
};
test "coerce subset to superset" {
const err = foo(AllocationError.OutOfMemory);
try std.testing.expect(err == FileOpenError.OutOfMemory);
}
fn foo(err: AllocationError) FileOpenError {
return err;
}
$ zig test test_coerce_error_subset_to_superset.zig 1/1 test_coerce_error_subset_to_superset.test.coerce subset to superset...OK All 1 tests passed.
但你不能将错误从超集强制转换为子集:
const FileOpenError = error{
AccessDenied,
OutOfMemory,
FileNotFound,
};
const AllocationError = error{
OutOfMemory,
};
test "coerce superset to subset" {
foo(FileOpenError.OutOfMemory) catch {};
}
fn foo(err: FileOpenError) AllocationError {
return err;
}
$ zig test test_coerce_error_superset_to_subset.zig /home/andy/dev/zig/doc/langref/test_coerce_error_superset_to_subset.zig:16:12: error: expected type 'error{OutOfMemory}', found 'error{AccessDenied,FileNotFound,OutOfMemory}' return err; ^~~ /home/andy/dev/zig/doc/langref/test_coerce_error_superset_to_subset.zig:16:12: note: 'error.AccessDenied' not a member of destination error set /home/andy/dev/zig/doc/langref/test_coerce_error_superset_to_subset.zig:16:12: note: 'error.FileNotFound' not a member of destination error set /home/andy/dev/zig/doc/langref/test_coerce_error_superset_to_subset.zig:15:28: note: function return type declared here fn foo(err: FileOpenError) AllocationError { ^~~~~~~~~~~~~~~ referenced by: test.coerce superset to subset: /home/andy/dev/zig/doc/langref/test_coerce_error_superset_to_subset.zig:12:8
有一个声明只有 1 个值的错误集的快捷方式,然后获取该值:
const err = error.FileNotFound;
这等价于:
const err = (error{FileNotFound}).FileNotFound;
这在使用推断错误集时变得有用。
全局错误集 §
anyerror
指的是全局错误集。
这是包含整个编译单元中所有错误的错误集,即它是所有其他错误集的并集。
你可以将任何错误集强制转换为全局错误集,你也可以显式地将全局错误集的错误转换为非全局错误集。这会插入一个语言级断言以确保错误值确实在目标错误集中。
通常应该避免使用全局错误集,因为它阻止编译器在编译时知道可能的错误。在编译时知道错误集对于生成的文档和有用的错误消息更好,例如在 switch 中忘记可能的错误值。
错误联合类型 §
错误集类型和普通类型可以用
!
二元运算符组合成错误联合类型。你使用错误联合类型的频率可能比单独使用错误集类型更高。
这是一个将字符串解析为 64 位整数的函数:
const std = @import("std");
const maxInt = std.math.maxInt;
pub fn parseU64(buf: []const u8, radix: u8) !u64 {
var x: u64 = 0;
for (buf) |c| {
const digit = charToDigit(c);
if (digit >= radix) {
return error.InvalidChar;
}
// x *= radix
var ov = @mulWithOverflow(x, radix);
if (ov[1] != 0) return error.OverFlow;
// x += digit
ov = @addWithOverflow(ov[0], digit);
if (ov[1] != 0) return error.OverFlow;
x = ov[0];
}
return x;
}
fn charToDigit(c: u8) u8 {
return switch (c) {
'0'...'9' => c - '0',
'A'...'Z' => c - 'A' + 10,
'a'...'z' => c - 'a' + 10,
else => maxInt(u8),
};
}
test "parse u64" {
const result = try parseU64("1234", 10);
try std.testing.expect(result == 1234);
}
$ zig test error_union_parsing_u64.zig 1/1 error_union_parsing_u64.test.parse u64...OK All 1 tests passed.
注意返回类型是
!u64。这意味着函数要么返回一个无符号 64
位整数,要么返回一个错误。我们在
! 左侧省略了错误集,因此错误集是推断的。
在函数定义中,你可以看到一些返回错误的 return
语句,在底部有一个返回
u64 的 return
语句。 两种类型都强制转换为
anyerror!u64。
如何使用这个函数取决于你想要做什么。以下是几种情况之一:
- 如果返回了错误,你想提供一个默认值。
- 如果返回了错误,那么你想返回相同的错误。
- 你完全确定它不会返回错误,所以想无条件地解包它。
- 你想对每个可能的错误采取不同的行动。
catch §
如果你想提供一个默认值,可以使用
catch
二元运算符:
const parseU64 = @import("error_union_parsing_u64.zig").parseU64;
fn doAThing(str: []u8) void {
const number = parseU64(str, 10) catch 13;
_ = number; // ...
}
在这段代码中,number
将等于成功解析的字符串,或者默认值 13。二元
catch
运算符右侧的类型必须匹配解包后的错误联合类型,或者是
noreturn
类型。
如果你想在使用
catch
后执行一些逻辑后提供默认值,你可以将
catch 与命名块结合使用:
const parseU64 = @import("error_union_parsing_u64.zig").parseU64;
fn doAThing(str: []u8) void {
const number = parseU64(str, 10) catch blk: {
// do things
break :blk 13;
};
_ = number; // number is now initialized
}
try §
假设你想在得到错误时返回该错误,否则继续函数逻辑:
const parseU64 = @import("error_union_parsing_u64.zig").parseU64;
fn doAThing(str: []u8) !void {
const number = parseU64(str, 10) catch |err| return err;
_ = number; // ...
}
有一个快捷方式。try
表达式:
const parseU64 = @import("error_union_parsing_u64.zig").parseU64;
fn doAThing(str: []u8) !void {
const number = try parseU64(str, 10);
_ = number; // ...
}
try
计算一个错误联合表达式。如果它是一个错误,则从当前函数返回相同的错误。否则,表达式的结果是解包后的值。
也许你完全确定一个表达式永远不会是错误。 在这种情况下,你可以这样做:
const number =
parseU64("1234",
10)
catch
unreachable;
这里我们确信 "1234" 将成功解析。所以我们在右侧放置
unreachable
值。
unreachable
调用安全检查的非法行为,所以在 Debug 和
ReleaseSafe
模式下,默认触发安全恐慌。因此,当我们调试应用程序时,如果这里确实有意外错误,应用程序会适当地崩溃。
你可能想对每种情况采取不同的行动。为此,我们结合 if 和 switch 表达式:
fn doAThing(str: []u8) void {
if (parseU64(str, 10)) |number| {
doSomethingWithNumber(number);
} else |err| switch (err) {
error.Overflow => {
// handle overflow...
},
// we promise that InvalidChar won't happen (or crash in debug mode if it does)
error.InvalidChar => unreachable,
}
}
最后,你可能只想处理某些错误。为此,你可以在
else
分支中捕获未处理的错误,现在它包含一个更窄的错误集:
fn doAnotherThing(str: []u8) error{InvalidChar}!void {
if (parseU64(str, 10)) |number| {
doSomethingWithNumber(number);
} else |err| switch (err) {
error.Overflow => {
// handle overflow...
},
else => |leftover_err| return leftover_err,
}
}
你必须使用变量捕获语法。如果你不需要变量,可以用
_ 捕获并避免使用
switch。
fn doADifferentThing(str: []u8) void {
if (parseU64(str, 10)) |number| {
doSomethingWithNumber(number);
} else |_| {
// do as you'd like
}
}
errdefer §
错误处理的另一个组件是 defer 语句。 除了无条件的
defer,Zig 还有
errdefer,它在块退出路径上当且仅当函数从块返回错误时计算延迟表达式。
示例:
fn createFoo(param: i32) !Foo {
const foo = try tryToAllocateFoo();
// 现在我们已经分配了 foo。如果函数失败,我们需要释放它。
// 但如果函数成功,我们想返回它。
errdefer deallocateFoo(foo);
const tmp_buf = allocateTmpBuffer() orelse return error.OutOfMemory;
// tmp_buf 确实是一个临时资源,我们肯定想在这个块离开作用域之前清理它
defer deallocateTmpBuffer(tmp_buf);
if (param > 1337) return error.InvalidParam;
// 这里 errdefer 不会运行,因为我们从函数返回成功。
// 但是 defer 会运行!
return foo;
}
这样做的好处是,你可以获得健壮的错误处理,而不会有冗长和认知开销,不用试图确保覆盖每个退出路径。释放代码总是直接跟随分配代码。
errdefer
语句可以选择性地捕获错误:
const std = @import("std");
fn captureError(captured: *?anyerror) !void {
errdefer |err| {
captured.* = err;
}
return error.GeneralFailure;
}
test "errdefer capture" {
var captured: ?anyerror = null;
if (captureError(&captured)) unreachable else |err| {
try std.testing.expectEqual(error.GeneralFailure, captured.?);
try std.testing.expectEqual(error.GeneralFailure, err);
}
}
$ zig test test_errdefer_capture.zig 1/1 test_errdefer_capture.test.errdefer capture...OK All 1 tests passed.
关于错误处理的其他一些要点:
-
这些原语提供了足够的表现力,使得未能检查错误成为编译错误是完全实用的。如果你真的想忽略错误,可以添加
catch unreachable,并在 Debug 和 ReleaseSafe 模式下如果你的假设错误时获得崩溃的额外好处。 - 由于 Zig 理解错误类型,它可以预先加权分支以支持不发生错误。这只是一个小的优化好处,在其他语言中是无法获得的。
另见:
错误联合使用 ! 二元运算符创建。
你可以使用编译时反射来访问错误联合的子类型:
const expect = @import("std").testing.expect;
test "error union" {
var foo: anyerror!i32 = undefined;
// 从错误联合的子类型强制转换:
foo = 1234;
// 从错误集强制转换:
foo = error.SomeError;
// 使用编译时反射访问错误联合的负载类型:
try comptime expect(@typeInfo(@TypeOf(foo)).error_union.payload == i32);
// 使用编译时反射访问错误联合的错误集类型:
try comptime expect(@typeInfo(@TypeOf(foo)).error_union.error_set == anyerror);
}
$ zig test test_error_union.zig 1/1 test_error_union.test.error union...OK All 1 tests passed.
合并错误集 §
使用
||
运算符将两个错误集合并在一起。结果错误集包含两个错误集的错误。左侧的文档注释覆盖右侧的文档注释。在这个例子中,C.PathNotFound
的文档注释是 A doc comment。
这对于根据
comptime
分支返回不同错误集的函数特别有用。例如,Zig 标准库使用
LinuxFileOpenError || WindowsFileOpenError
作为打开文件的错误集。
const A = error{
NotDir,
/// A doc comment
PathNotFound,
};
const B = error{
OutOfMemory,
/// B doc comment
PathNotFound,
};
const C = A || B;
fn foo() C!void {
return error.NotDir;
}
test "merge error sets" {
if (foo()) {
@panic("unexpected");
} else |err| switch (err) {
error.OutOfMemory => @panic("unexpected"),
error.PathNotFound => @panic("unexpected"),
error.NotDir => {},
}
}
$ zig test test_merging_error_sets.zig 1/1 test_merging_error_sets.test.merge error sets...OK All 1 tests passed.
推断错误集 §
因为 Zig 中的许多函数都返回可能的错误,Zig
支持推断错误集。
要推断函数的错误集,在函数的返回类型前加上
! 运算符,如 !T:
// 使用推断的错误集
pub fn add_inferred(comptime T: type, a: T, b: T) !T {
const ov = @addWithOverflow(a, b);
if (ov[1] != 0) return error.Overflow;
return ov[0];
}
// 使用显式的错误集
pub fn add_explicit(comptime T: type, a: T, b: T) Error!T {
const ov = @addWithOverflow(a, b);
if (ov[1] != 0) return error.Overflow;
return ov[0];
}
const Error = error{
Overflow,
};
const std = @import("std");
test "inferred error set" {
if (add_inferred(u8, 255, 1)) |_| unreachable else |err| switch (err) {
error.Overflow => {}, // ok
}
}
$ zig test test_inferred_error_sets.zig 1/1 test_inferred_error_sets.test.inferred error set...OK All 1 tests passed.
当函数具有推断的错误集时,该函数变为泛型,因此使用它做某些事情变得更加困难,例如获取函数指针,或拥有在不同构建目标之间一致的错误集。此外,推断的错误集与递归不兼容。
在这些情况下,建议使用显式错误集。你通常可以从空错误集开始,让编译错误引导你完成该集合。
这些限制可能会在 Zig 的未来版本中克服。
错误返回跟踪 §
错误返回跟踪显示了代码中错误返回到调用函数的所有点。这使得在任何地方使用 try 变得实用,然后如果错误最终从应用程序中冒出来,仍然能够知道发生了什么。
pub fn main() !void {
try foo(12);
}
fn foo(x: i32) !void {
if (x >= 5) {
try bar();
} else {
try bang2();
}
}
fn bar() !void {
if (baz()) {
try quux();
} else |err| switch (err) {
error.FileNotFound => try hello(),
}
}
fn baz() !void {
try bang1();
}
fn quux() !void {
try bang2();
}
fn hello() !void {
try bang2();
}
fn bang1() !void {
return error.FileNotFound;
}
fn bang2() !void {
return error.PermissionDenied;
}
$ zig build-exe error_return_trace.zig $ ./error_return_trace error: PermissionDenied /home/andy/dev/zig/doc/langref/error_return_trace.zig:34:5: 0x113d36c in bang1 (error_return_trace.zig) return error.FileNotFound; ^ /home/andy/dev/zig/doc/langref/error_return_trace.zig:22:5: 0x113d3b6 in baz (error_return_trace.zig) try bang1(); ^ /home/andy/dev/zig/doc/langref/error_return_trace.zig:38:5: 0x113d3ec in bang2 (error_return_trace.zig) return error.PermissionDenied; ^ /home/andy/dev/zig/doc/langref/error_return_trace.zig:30:5: 0x113d496 in hello (error_return_trace.zig) try bang2(); ^ /home/andy/dev/zig/doc/langref/error_return_trace.zig:17:31: 0x113d56e in bar (error_return_trace.zig) error.FileNotFound => try hello(), ^ /home/andy/dev/zig/doc/langref/error_return_trace.zig:7:9: 0x113d654 in foo (error_return_trace.zig) try bar(); ^ /home/andy/dev/zig/doc/langref/error_return_trace.zig:2:5: 0x113d71b in main (error_return_trace.zig) try foo(12); ^
仔细看这个例子。这不是栈跟踪。
你可以看到最终冒出来的错误是
PermissionDenied, 但最初引发这一切的错误是
FileNotFound。在
bar 函数中,代码处理了原始错误代码,然后从
switch
语句返回另一个错误。错误返回跟踪清楚地显示了这一点,而栈跟踪看起来像这样:
pub fn main() void {
foo(12);
}
fn foo(x: i32) void {
if (x >= 5) {
bar();
} else {
bang2();
}
}
fn bar() void {
if (baz()) {
quux();
} else {
hello();
}
}
fn baz() bool {
return bang1();
}
fn quux() void {
bang2();
}
fn hello() void {
bang2();
}
fn bang1() bool {
return false;
}
fn bang2() void {
@panic("PermissionDenied");
}
$ zig build-exe stack_trace.zig $ ./stack_trace thread 2902479 panic: PermissionDenied /home/andy/dev/zig/doc/langref/stack_trace.zig:38:5: 0x1140e6c in bang2 (stack_trace.zig) @panic("PermissionDenied"); ^ /home/andy/dev/zig/doc/langref/stack_trace.zig:30:10: 0x11414ac in hello (stack_trace.zig) bang2(); ^ /home/andy/dev/zig/doc/langref/stack_trace.zig:17:14: 0x1140e23 in bar (stack_trace.zig) hello(); ^ /home/andy/dev/zig/doc/langref/stack_trace.zig:7:12: 0x1140ab8 in foo (stack_trace.zig) bar(); ^ /home/andy/dev/zig/doc/langref/stack_trace.zig:2:8: 0x113f871 in main (stack_trace.zig) foo(12); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113eabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113e351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (process terminated by signal)
这里,栈跟踪没有解释 bar 中的控制流如何到达
hello() 调用。
必须打开调试器或进一步检测应用程序才能找出原因。另一方面,错误返回跟踪确切地显示了错误是如何冒出来的。
这个调试功能使得在健壮处理所有错误条件的代码上快速迭代变得更容易。这意味着 Zig 开发人员会自然而然地发现自己编写正确、健壮的代码,以提高开发速度。
错误返回跟踪在 Debug 构建中默认启用,在 ReleaseFast、ReleaseSafe 和 ReleaseSmall 构建中默认禁用。
有几种方法可以激活此错误返回跟踪功能:
- 从 main 返回一个错误
-
错误最终到达
catch unreachable且你没有覆盖默认的 panic 处理程序 -
使用
errorReturnTrace
访问当前返回跟踪。你可以使用
std.debug.dumpStackTrace打印它。当在没有错误返回跟踪支持的情况下构建时,此函数返回编译时已知的 null。
实现细节 §
要分析性能成本,有两种情况:
- 当没有返回错误时
- 当返回错误时
对于没有返回错误的情况,成本是单个内存写操作,仅在调用图中第一个调用可失败函数的不可失败函数中,即当返回
void
的函数调用返回
error
的函数时。 这是为了在栈内存中初始化此结构:
pub const StackTrace = struct {
index: usize,
instruction_addresses: [N]usize,
};
这里,N 是由调用图分析确定的最大函数调用深度。递归被忽略并计为 2。
指向
StackTrace
的指针作为秘密参数传递给每个可以返回错误的函数,但它总是第一个参数,所以它可能位于寄存器中并保持在那里。
对于没有发生错误的路径来说就是这样。在性能方面实际上是免费的。
当为返回错误的函数生成代码时,就在
return
语句之前(仅对于返回错误的
return
语句),Zig 生成对此函数的调用:
// 在 LLVM IR 中标记为 "no-inline"
fn __zig_return_error(stack_trace: *StackTrace) void {
stack_trace.instruction_addresses[stack_trace.index] = @returnAddress();
stack_trace.index = (stack_trace.index + 1) % N;
}
成本是 2 个数学操作加上一些内存读写。访问的内存受到限制,应该在错误返回冒泡期间保持缓存。
至于代码大小成本,return 语句之前的 1
个函数调用没什么大不了的。即便如此,我也有计划将对
__zig_return_error
的调用变为尾调用,这将代码大小成本降低到实际为零。在没有错误返回跟踪的代码中是
return
语句的内容,在有错误返回跟踪的代码中可以变成跳转指令。
可选值 §
Zig 在不影响效率或可读性的情况下提供安全的一个领域是可选类型。
问号象征可选类型。你可以通过在类型前面放一个问号将类型转换为可选类型,像这样:
// 普通整数
const normal_int: i32 = 1234;
// 可选整数
const optional_int: ?i32 = 5678;
现在变量 optional_int 可以是
i32,或者 null。
与其说整数,不如说指针。空引用是许多运行时异常的来源,甚至被指责为计算机科学的最大错误。
Zig 没有它们。
相反,你可以使用可选指针。这在内部编译为普通指针,因为我们知道可以将 0 用作可选类型的 null 值。但编译器可以检查你的工作并确保你不会将 null 分配给不能为 null 的东西。
通常没有 null 的缺点是它使代码编写起来更冗长。但是,让我们比较一些等效的 C 代码和 Zig 代码。
任务:调用 malloc,如果结果为 null,返回 null。
C 代码
// malloc 原型包含在内以供参考
void *malloc(size_t size);
struct Foo *do_a_thing(void) {
char *ptr = malloc(1234);
if (!ptr) return NULL;
// ...
}
Zig 代码
// malloc 原型包含在内以供参考
extern fn malloc(size: usize) ?[*]u8;
fn doAThing() ?*Foo {
const ptr = malloc(1234) orelse return null;
_ = ptr; // ...
}
这里,Zig 至少和 C
一样方便,如果不是更方便的话。并且,"ptr" 的类型是
[*]u8
而不是
?[*]u8。orelse
关键字解包了可选类型,因此
ptr 在函数中使用的任何地方都保证是非 null
的。
你可能看到的另一种形式的 NULL 检查看起来像这样:
void do_a_thing(struct Foo *foo) {
// do some stuff
if (foo) {
do_something_with_foo(foo);
}
// do some stuff
}
在 Zig 中你可以完成同样的事情:
const Foo = struct {};
fn doSomethingWithFoo(foo: *Foo) void {
_ = foo;
}
fn doAThing(optional_foo: ?*Foo) void {
// do some stuff
if (optional_foo) |foo| {
doSomethingWithFoo(foo);
}
// do some stuff
}
再次,这里值得注意的是,在 if 块内,foo
不再是可选指针,它是一个指针,不能为 null。
这样做的一个好处是,将指针作为参数的函数可以使用
"nonnull" 属性注释 - 在
GCC
中是 __attribute__((nonnull))。
优化器有时可以根据指针参数不能为 null 做出更好的决策。
可选类型 §
可选值通过在类型前面放置
?
创建。你可以使用编译时反射访问可选值的子类型:
const expect = @import("std").testing.expect;
test "optional type" {
// 声明一个可选值并从 null 强制转换:
var foo: ?i32 = null;
// 从可选值的子类型强制转换
foo = 1234;
// 使用编译时反射访问可选值的子类型:
try comptime expect(@typeInfo(@TypeOf(foo)).optional.child == i32);
}
$ zig test test_optional_type.zig 1/1 test_optional_type.test.optional type...OK All 1 tests passed.
null §
就像 undefined 一样,null
有自己的类型,使用它的唯一方法是将其转换为不同的类型:
const optional_value: ?i32 = null;
可选指针 §
可选指针保证与指针大小相同。可选值的
null
保证是地址 0。
const expect = @import("std").testing.expect;
test "optional pointers" {
// 指针不能为 null。如果你想要一个 null 指针,使用可选
// 前缀 `?` 使指针类型成为可选的。
var ptr: ?*i32 = null;
var x: i32 = 1;
ptr = &x;
try expect(ptr.?.* == 1);
// 可选指针与普通指针大小相同,因为指针值 0 用作 null 值。
try expect(@sizeOf(?*i32) == @sizeOf(*i32));
}
$ zig test test_optional_pointer.zig 1/1 test_optional_pointer.test.optional pointers...OK All 1 tests passed.
另见:
类型转换 §
类型转换将一种类型的值转换为另一种类型。 Zig 对已知完全安全且明确的转换使用类型强制转换,对不希望意外发生的转换使用显式转换。 还有第三种类型转换,称为同级类型解析,用于在给定多个操作数类型的情况下必须决定结果类型的情况。
类型强制转换 §
当需要一种类型,但提供了不同的类型时,就会发生类型强制转换:
test "类型强制转换 - 变量声明" {
const a: u8 = 1;
const b: u16 = a;
_ = b;
}
test "类型强制转换 - 函数调用" {
const a: u8 = 1;
foo(a);
}
fn foo(b: u16) void {
_ = b;
}
test "类型强制转换 - @as 内置函数" {
const a: u8 = 1;
const b = @as(u16, a);
_ = b;
}
$ zig test test_type_coercion.zig 1/3 test_type_coercion.test.type coercion - variable declaration...OK 2/3 test_type_coercion.test.type coercion - function call...OK 3/3 test_type_coercion.test.type coercion - @as builtin...OK All 3 tests passed.
只有在如何从一种类型转换到另一种类型完全明确且转换保证安全时,才允许类型强制转换。有一个例外,即 C 指针。
类型强制转换:更严格的限定 §
在运行时具有相同表示的值可以被转换以增加限定符的严格性,无论限定符嵌套多深:
-
const- 允许从非 const 转换到 const -
volatile- 允许从非 volatile 转换到 volatile -
align- 允许从较大对齐转换到较小对齐 - 错误集 到超集是允许的
这些转换在运行时是无操作的,因为值的表示不会改变。
test "类型强制转换 - const 限定" {
var a: i32 = 1;
const b: *i32 = &a;
foo(b);
}
fn foo(_: *const i32) void {}
$ zig test test_no_op_casts.zig 1/1 test_no_op_casts.test.type coercion - const qualification...OK All 1 tests passed.
此外,指针可以强制转换为 const 可选指针:
const std = @import("std");
const expect = std.testing.expect;
const mem = std.mem;
test "将 *[1][*:0]const u8 转换为 []const ?[*:0]const u8" {
const window_name = [1][*:0]const u8{"window name"};
const x: []const ?[*:0]const u8 = &window_name;
try expect(mem.eql(u8, mem.span(x[0].?), "window name"));
}
$ zig test test_pointer_coerce_const_optional.zig 1/1 test_pointer_coerce_const_optional.test.cast *[1][*:0]const u8 to []const ?[*:0]const u8...OK All 1 tests passed.
类型强制转换:整数和浮点数拓宽 §
整数 可以强制转换为能够表示旧类型所有值的整数类型,同样地,浮点数 可以强制转换为能够表示旧类型所有值的浮点类型。
const std = @import("std");
const builtin = @import("builtin");
const expect = std.testing.expect;
const mem = std.mem;
test "整数拓宽" {
const a: u8 = 250;
const b: u16 = a;
const c: u32 = b;
const d: u64 = c;
const e: u64 = d;
const f: u128 = e;
try expect(f == a);
}
test "无符号整数隐式转换为有符号整数" {
const a: u8 = 250;
const b: i16 = a;
try expect(b == 250);
}
test "浮点数拓宽" {
const a: f16 = 12.34;
const b: f32 = a;
const c: f64 = b;
const d: f128 = c;
try expect(d == a);
}
$ zig test test_integer_widening.zig 1/3 test_integer_widening.test.integer widening...OK 2/3 test_integer_widening.test.implicit unsigned integer to signed integer...OK 3/3 test_integer_widening.test.float widening...OK All 3 tests passed.
类型强制转换:浮点数到整数 §
编译错误是合适的,因为这个有歧义的表达式给编译器留下了两种关于强制转换的选择。
-
将
54.0转换为comptime_int得到@as(comptime_int, 10),再转换为@as(f32, 10) -
将
5转换为comptime_float得到@as(comptime_float, 10.8),再转换为@as(f32, 10.8)
// 浮点数到整数的编译时强制转换
test "隐式转换为 comptime_int" {
const f: f32 = 54.0 / 5;
_ = f;
}
$ zig test test_ambiguous_coercion.zig /home/andy/dev/zig/doc/langref/test_ambiguous_coercion.zig:3:25: error: ambiguous coercion of division operands 'comptime_float' and 'comptime_int'; non-zero remainder '4' const f: f32 = 54.0 / 5; ~~~~~^~~
类型强制转换:切片、数组和指针 §
const std = @import("std");
const expect = std.testing.expect;
// 您可以将数组的常量指针赋值给带有元素类型 const 修饰符的切片。
// 这对于字符串字面量特别有用。
test "*const [N]T 转换为 []const T" {
const x1: []const u8 = "hello";
const x2: []const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
try expect(std.mem.eql(u8, x1, x2));
const y: []const f32 = &[2]f32{ 1.2, 3.4 };
try expect(y[0] == 1.2);
}
// 同样,当目标类型是错误联合类型时,它也适用。
test "*const [N]T 转换为 E![]const T" {
const x1: anyerror![]const u8 = "hello";
const x2: anyerror![]const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
try expect(std.mem.eql(u8, try x1, try x2));
const y: anyerror![]const f32 = &[2]f32{ 1.2, 3.4 };
try expect((try y)[0] == 1.2);
}
// 同样,当目标类型是可选类型时,它也适用。
test "*const [N]T 转换为 ?[]const T" {
const x1: ?[]const u8 = "hello";
const x2: ?[]const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
try expect(std.mem.eql(u8, x1.?, x2.?));
const y: ?[]const f32 = &[2]f32{ 1.2, 3.4 };
try expect(y.?[0] == 1.2);
}
// 在这种转换中,数组长度成为切片长度。
test "*[N]T 转换为 []T" {
var buf: [5]u8 = "hello".*;
const x: []u8 = &buf;
try expect(std.mem.eql(u8, x, "hello"));
const buf2 = [2]f32{ 1.2, 3.4 };
const x2: []const f32 = &buf2;
try expect(std.mem.eql(f32, x2, &[2]f32{ 1.2, 3.4 }));
}
// 数组的单项指针可以强制转换为多项指针。
test "*[N]T 转换为 [*]T" {
var buf: [5]u8 = "hello".*;
const x: [*]u8 = &buf;
try expect(x[4] == 'o');
// x[5] 会是未捕获的越界指针解引用!
}
// 同样,当目标类型是可选类型时,它也适用。
test "*[N]T 转换为 ?[*]T" {
var buf: [5]u8 = "hello".*;
const x: ?[*]u8 = &buf;
try expect(x.?[4] == 'o');
}
// 单项指针可以转换为长度为 1 的单项数组。
test "*T 转换为 *[1]T" {
var x: i32 = 1234;
const y: *[1]i32 = &x;
const z: [*]i32 = y;
try expect(z[0] == 1234);
}
// 哨兵终止的切片可以强制转换为哨兵终止的指针
test "[:x]T 转换为 [*:x]T" {
const buf: [:0]const u8 = "hello";
const buf2: [*:0]const u8 = buf;
try expect(buf2[4] == 'o');
}
$ zig test test_coerce_slices_arrays_and_pointers.zig 1/8 test_coerce_slices_arrays_and_pointers.test.*const [N]T to []const T...OK 2/8 test_coerce_slices_arrays_and_pointers.test.*const [N]T to E![]const T...OK 3/8 test_coerce_slices_arrays_and_pointers.test.*const [N]T to ?[]const T...OK 4/8 test_coerce_slices_arrays_and_pointers.test.*[N]T to []T...OK 5/8 test_coerce_slices_arrays_and_pointers.test.*[N]T to [*]T...OK 6/8 test_coerce_slices_arrays_and_pointers.test.*[N]T to ?[*]T...OK 7/8 test_coerce_slices_arrays_and_pointers.test.*T to *[1]T...OK 8/8 test_coerce_slices_arrays_and_pointers.test.[:x]T to [*:x]T...OK All 8 tests passed.
另请参阅:
类型强制转换:可选类型 §
可选类型 的载荷类型,以及 null,可以强制转换为可选类型。
const std = @import("std");
const expect = std.testing.expect;
test "强制转换为可选类型" {
const x: ?i32 = 1234;
const y: ?i32 = null;
try expect(x.? == 1234);
try expect(y == null);
}
$ zig test test_coerce_optionals.zig 1/1 test_coerce_optionals.test.coerce to optionals...OK All 1 tests passed.
可选类型在 错误联合类型 内部嵌套时也有效:
const std = @import("std");
const expect = std.testing.expect;
test "强制转换为错误联合类型包装的可选类型" {
const x: anyerror!?i32 = 1234;
const y: anyerror!?i32 = null;
try expect((try x).? == 1234);
try expect((try y) == null);
}
$ zig test test_coerce_optional_wrapped_error_union.zig 1/1 test_coerce_optional_wrapped_error_union.test.coerce to optionals wrapped in error union...OK All 1 tests passed.
类型强制转换:错误联合类型 §
错误联合类型 的载荷类型,以及 错误集类型,可以强制转换为错误联合类型:
const std = @import("std");
const expect = std.testing.expect;
test "强制转换为错误联合类型" {
const x: anyerror!i32 = 1234;
const y: anyerror!i32 = error.Failure;
try expect((try x) == 1234);
try std.testing.expectError(error.Failure, y);
}
$ zig test test_coerce_to_error_union.zig 1/1 test_coerce_to_error_union.test.coercion to error unions...OK All 1 tests passed.
类型强制转换:编译时已知数字 §
当一个数字在 编译时 已知可以在目标类型中表示时,它可以被强制转换:
const std = @import("std");
const expect = std.testing.expect;
test "当值在编译时已知适合时,将较大的整数类型强制转换为较小的类型" {
const x: u64 = 255;
const y: u8 = x;
try expect(y == 255);
}
$ zig test test_coerce_large_to_small.zig 1/1 test_coerce_large_to_small.test.coercing large integer type to smaller one when value is comptime-known to fit...OK All 1 tests passed.
类型强制转换:联合类型和枚举 §
标记联合类型可以强制转换为枚举,而枚举可以强制转换为标记联合类型,前提是它们在 编译时 已知是联合类型的一个字段,该字段只有一个可能的值,例如 void:
const std = @import("std");
const expect = std.testing.expect;
const E = enum {
one,
two,
three,
};
const U = union(E) {
one: i32,
two: f32,
three,
};
const U2 = union(enum) {
a: void,
b: f32,
fn tag(self: U2) usize {
switch (self) {
.a => return 1,
.b => return 2,
}
}
};
test "联合类型和枚举之间的强制转换" {
const u = U{ .two = 12.34 };
const e: E = u; // 将联合类型强制转换为枚举
try expect(e == E.two);
const three = E.three;
const u_2: U = three; // 将枚举强制转换为联合类型
try expect(u_2 == E.three);
const u_3: U = .three; // 将枚举字面量强制转换为联合类型
try expect(u_3 == E.three);
const u_4: U2 = .a; // 将枚举字面量强制转换为具有推断枚举标记类型的联合类型。
try expect(u_4.tag() == 1);
// 以下示例无效。
// error: coercion from enum '@TypeOf(.enum_literal)' to union 'test_coerce_unions_enum.U2' must initialize 'f32' field 'b'
//var u_5: U2 = .b;
//try expect(u_5.tag() == 2);
}
$ zig test test_coerce_unions_enums.zig 1/1 test_coerce_unions_enums.test.coercion between unions and enums...OK All 1 tests passed.
另请参阅:
类型强制转换:undefined §
undefined 可以强制转换为任何类型。
类型强制转换:元组到数组 §
元组 可以强制转换为数组,如果所有字段都具有相同的类型。
const std = @import("std");
const expect = std.testing.expect;
const Tuple = struct { u8, u8 };
test "从同质元组强制转换为数组" {
const tuple: Tuple = .{ 5, 6 };
const array: [2]u8 = tuple;
_ = array;
}
$ zig test test_coerce_tuples_arrays.zig 1/1 test_coerce_tuples_arrays.test.coercion from homogeneous tuple to array...OK All 1 tests passed.
显式转换 §
显式转换通过 内置函数 执行。 有些显式转换是安全的;有些则不是。 有些显式转换执行语言级断言;有些则不执行。 有些显式转换在运行时是无操作的;有些则不是。
- @bitCast - 改变类型但保持位表示
- @alignCast - 使指针具有更大的对齐
- @enumFromInt - 根据整数标记值获取枚举值
- @errorFromInt - 根据整数值获取错误代码
- @errorCast - 转换为更小的错误集
- @floatCast - 将较大的浮点数转换为较小的浮点数
- @floatFromInt - 将整数转换为浮点数值
- @intCast - 在整数类型之间转换
- @intFromBool - 将 true 转换为 1,false 转换为 0
- @intFromEnum - 获取枚举或标记联合类型的整数标记值
- @intFromError - 获取错误代码的整数值
- @intFromFloat - 获取浮点数值的整数部分
- @intFromPtr - 获取指针的地址
- @ptrFromInt - 将地址转换为指针
- @ptrCast - 在指针类型之间转换
- @truncate - 在整数类型之间转换,截断位
对等类型解析 §
对等类型解析发生在以下位置:
这种类型解析选择一个所有对等类型都可以强制转换为的类型。以下是一些示例:
const std = @import("std");
const expect = std.testing.expect;
const mem = std.mem;
test "对等解析整数拓宽" {
const a: i8 = 12;
const b: i16 = 34;
const c = a + b;
try expect(c == 46);
try expect(@TypeOf(c) == i16);
}
test "对等解析不同大小的数组为 const 切片" {
try expect(mem.eql(u8, boolToStr(true), "true"));
try expect(mem.eql(u8, boolToStr(false), "false"));
try comptime expect(mem.eql(u8, boolToStr(true), "true"));
try comptime expect(mem.eql(u8, boolToStr(false), "false"));
}
fn boolToStr(b: bool) []const u8 {
return if (b) "true" else "false";
}
test "对等解析数组和 const 切片" {
try testPeerResolveArrayConstSlice(true);
try comptime testPeerResolveArrayConstSlice(true);
}
fn testPeerResolveArrayConstSlice(b: bool) !void {
const value1 = if (b) "aoeu" else @as([]const u8, "zz");
const value2 = if (b) @as([]const u8, "zz") else "aoeu";
try expect(mem.eql(u8, value1, "aoeu"));
try expect(mem.eql(u8, value2, "zz"));
}
test "对等类型解析: ?T 和 T" {
try expect(peerTypeTAndOptionalT(true, false).? == 0);
try expect(peerTypeTAndOptionalT(false, false).? == 3);
comptime {
try expect(peerTypeTAndOptionalT(true, false).? == 0);
try expect(peerTypeTAndOptionalT(false, false).? == 3);
}
}
fn peerTypeTAndOptionalT(c: bool, b: bool) ?usize {
if (c) {
return if (b) null else @as(usize, 0);
}
return @as(usize, 3);
}
test "对等类型解析: *[0]u8 和 []const u8" {
try expect(peerTypeEmptyArrayAndSlice(true, "hi").len == 0);
try expect(peerTypeEmptyArrayAndSlice(false, "hi").len == 1);
comptime {
try expect(peerTypeEmptyArrayAndSlice(true, "hi").len == 0);
try expect(peerTypeEmptyArrayAndSlice(false, "hi").len == 1);
}
}
fn peerTypeEmptyArrayAndSlice(a: bool, slice: []const u8) []const u8 {
if (a) {
return &[_]u8{};
}
return slice[0..1];
}
test "对等类型解析: *[0]u8, []const u8, 和 anyerror![]u8" {
{
var data = "hi".*;
const slice = data[0..];
try expect((try peerTypeEmptyArrayAndSliceAndError(true, slice)).len == 0);
try expect((try peerTypeEmptyArrayAndSliceAndError(false, slice)).len == 1);
}
comptime {
var data = "hi".*;
const slice = data[0..];
try expect((try peerTypeEmptyArrayAndSliceAndError(true, slice)).len == 0);
try expect((try peerTypeEmptyArrayAndSliceAndError(false, slice)).len == 1);
}
}
fn peerTypeEmptyArrayAndSliceAndError(a: bool, slice: []u8) anyerror![]u8 {
if (a) {
return &[_]u8{};
}
return slice[0..1];
}
test "对等类型解析: *const T 和 ?*T" {
const a: *const usize = @ptrFromInt(0x123456780);
const b: ?*usize = @ptrFromInt(0x123456780);
try expect(a == b);
try expect(b == a);
}
test "对等类型解析: 错误联合类型 switch" {
// 只有当错误情况仅是 switch 表达式时,非错误和错误情况才是对等的;
// 模式 `if (x) {...} else |err| blk: { switch (err) {...} }` 不认为
// 非错误和错误情况是对等的。
var a: error{ A, B, C }!u32 = 0;
_ = &a;
const b = if (a) |x|
x + 3
else |err| switch (err) {
error.A => 0,
error.B => 1,
error.C => null,
};
try expect(@TypeOf(b) == ?u32);
// 只有当错误情况仅是 switch 表达式时,非错误和错误情况才是对等的;
// 模式 `x catch |err| blk: { switch (err) {...} }` 不认为解包的 `x`
// 和错误情况是对等的。
const c = a catch |err| switch (err) {
error.A => 0,
error.B => 1,
error.C => null,
};
try expect(@TypeOf(c) == ?u32);
}
$ zig test test_peer_type_resolution.zig 1/8 test_peer_type_resolution.test.peer resolve int widening...OK 2/8 test_peer_type_resolution.test.peer resolve arrays of different size to const slice...OK 3/8 test_peer_type_resolution.test.peer resolve array and const slice...OK 4/8 test_peer_type_resolution.test.peer type resolution: ?T and T...OK 5/8 test_peer_type_resolution.test.peer type resolution: *[0]u8 and []const u8...OK 6/8 test_peer_type_resolution.test.peer type resolution: *[0]u8, []const u8, and anyerror![]u8...OK 7/8 test_peer_type_resolution.test.peer type resolution: *const T and ?*T...OK 8/8 test_peer_type_resolution.test.peer type resolution: error union switch...OK All 8 tests passed.
零位类型 §
对于某些类型,@sizeOf 为 0:
- void
-
整数
u0和i0。 - 长度为 0 的 数组 和 向量,或元素类型为零位类型的数组和向量。
- 只有一个标签的 enum。
- 所有字段都是零位类型的 struct。
- 只有一个字段且该字段为零位类型的 union。
这些类型只能有一个可能的值,因此需要 0 位来表示。使用这些类型的代码不会包含在最终生成的代码中:
export fn entry() void {
var x: void = {};
var y: void = {};
x = y;
y = x;
}
当这转换成机器代码时,entry
的主体中不生成代码,即使在
Debug 模式下也是如此。例如,在
x86_64 上:
0000000000000010 <entry>:
10: 55 push %rbp
11: 48 89 e5 mov %rsp,%rbp
14: 5d pop %rbp
15: c3 retq
这些汇编指令没有与 void 值相关联的任何代码 - 它们只执行函数调用序言和尾声。
void §
void
可用于实例化泛型类型。例如,给定一个
Map(Key, Value),可以为
Value 类型传递
void
来使其变成一个 Set:
const std = @import("std");
const expect = std.testing.expect;
test "使用 void 将 HashMap 转换为集合" {
var map = std.AutoHashMap(i32, void).init(std.testing.allocator);
defer map.deinit();
try map.put(1, {});
try map.put(2, {});
try expect(map.contains(2));
try expect(!map.contains(3));
_ = map.remove(2);
try expect(!map.contains(2));
}
$ zig test test_void_in_hashmap.zig 1/1 test_void_in_hashmap.test.turn HashMap into a set with void...OK All 1 tests passed.
注意,这与为哈希映射值使用虚拟值不同。通过使用
void
作为值的类型,哈希映射条目类型没有值字段,因此哈希映射占用的空间更少。此外,所有处理存储和加载值的代码都被删除,如上所示。
void 不同于
anyopaque。
void
的已知大小为 0 字节,而
anyopaque
的大小未知但非零。
类型为
void
的表达式是唯一可以忽略其值的表达式。例如,忽略非
void
表达式会导致编译错误:
test "忽略表达式值" {
foo();
}
fn foo() i32 {
return 1234;
}
$ zig test test_expression_ignored.zig /home/andy/dev/zig/doc/langref/test_expression_ignored.zig:2:8: error: value of type 'i32' ignored foo(); ~~~^~ /home/andy/dev/zig/doc/langref/test_expression_ignored.zig:2:8: note: all non-void values must be used /home/andy/dev/zig/doc/langref/test_expression_ignored.zig:2:8: note: to discard the value, assign it to '_'
但是,如果表达式的类型为
void,则不会有错误。表达式结果可以通过将它们赋值给
_ 来显式忽略。
test "void 被忽略" {
returnsVoid();
}
test "显式忽略表达式值" {
_ = foo();
}
fn returnsVoid() void {}
fn foo() i32 {
return 1234;
}
$ zig test test_void_ignored.zig 1/2 test_void_ignored.test.void is ignored...OK 2/2 test_void_ignored.test.explicitly ignoring expression value...OK All 2 tests passed.
结果位置语义 §
在编译期间,每个 Zig
表达式和子表达式都被分配可选的结果位置信息。这些信息规定了表达式应具有的类型(其结果类型),以及结果值应放置在内存中的位置(其结果位置)。该信息是可选的,因为并非每个表达式都有这些信息:例如,赋值给
_
既不提供关于表达式类型的任何信息,也不提供具体的内存位置来放置它。
作为一个激励性示例,考虑语句
const x:
u32 =
42;。这里的类型注释为初始化表达式
42 提供了
u32
的结果类型,指示编译器将此整数(最初类型为
comptime_int)强制转换为此类型。我们很快会看到更多示例。
这不是实现细节:上面概述的逻辑被编码到 Zig 语言规范中,是该语言中类型推断的主要机制。该系统统称为"结果位置语义"。
结果类型 §
结果类型在可能的情况下通过表达式递归传播。例如,如果表达式
&e 的结果类型为
*u32,则 e 被赋予
u32
的结果类型,允许语言在获取引用之前执行此强制转换。
结果类型机制被诸如
@intCast
之类的转换内置函数所使用。这些内置函数不是以类型作为参数来指定要转换到的类型,而是使用它们的结果类型来确定此信息。结果类型通常从上下文中得知;在不知道的情况下,可以使用
@as
内置函数来显式提供结果类型。
我们可以如下分解简单表达式的每个组成部分的结果类型:
const expectEqual = @import("std").testing.expectEqual;
test "结果类型通过结构体初始化器传播" {
const S = struct { x: u32 };
const val: u64 = 123;
const s: S = .{ .x = @intCast(val) };
// .{ .x = @intCast(val) } 由于类型注释,结果类型为 `S`
// @intCast(val) 由于字段 `S.x` 的类型,结果类型为 `u32`
// val 没有结果类型,因为它允许是任何整数类型
try expectEqual(@as(u32, 123), s.x);
}
$ zig test result_type_propagation.zig 1/1 result_type_propagation.test.result type propagates through struct initializer...OK All 1 tests passed.
此结果类型信息对于上述转换内置函数以及避免构造强制转换前的值和在某些情况下避免显式类型强制转换都很有用。下表详细说明了一些常见表达式如何传播结果类型,其中
x 和 y 是任意子表达式。
| 表达式 | 父结果类型 | 子表达式结果类型 |
|---|---|---|
const
val: T = x
|
- | x 是 T |
var
val: T = x
|
- | x 是 T |
val = x |
- |
x 是
@TypeOf(val)
|
@as(T, x)
|
- | x 是 T |
&x |
*T |
x 是 T |
&x |
[]T |
x 是
T 的某个数组
|
f(x) |
- |
x 具有
f 的第一个参数的类型
|
.{x} |
T |
x 是
@FieldType(T,
"0")
|
.{ .a = x }
|
T |
x 是
@FieldType(T,
"a")
|
T{x} |
- |
x 是
@FieldType(T,
"0")
|
T{ .a = x }
|
- |
x 是
@FieldType(T,
"a")
|
@Type(x)
|
- |
x 是
std.builtin.Type
|
@typeInfo(x)
|
- |
x 是
type
|
x << y
|
- |
y 是
std.math.Log2IntCeil(@TypeOf(x))
|
结果位置 §
除了结果类型信息之外,每个表达式还可以被可选地分配一个结果位置:值必须直接写入的指针。该系统可用于在初始化数据结构时防止中间副本,这对于必须具有固定内存地址("固定"类型)的类型可能很重要。
当编译简单的赋值表达式
x = e 时,许多语言会在栈上创建临时值
e,然后将其赋值给
x,在此过程中可能执行类型强制转换。Zig
采用不同的方法。表达式 e 被赋予与
x 的类型匹配的结果类型,以及
&x 的结果位置。对于许多
e
的语法形式,这没有实际影响。但是,在使用更复杂的语法形式时,它可能产生重要的语义效果。
例如,如果表达式
.{ .a = x, .b = y } 的结果位置为
ptr,则 x 被赋予
&ptr.a 的结果位置,y 被赋予
&ptr.b
的结果位置。如果没有这个系统,这个表达式将完全在栈上构造一个临时结构体值,然后才将其复制到目标地址。本质上,Zig
将赋值
foo = .{ .a = x, .b = y } 解糖为两个语句
foo.a = x; foo.b = y;。
当赋值聚合值时,如果初始化表达式依赖于聚合的前一个值,这有时可能很重要。最容易演示这一点的方法是尝试交换结构体或数组的字段 - 以下逻辑看起来合理,但实际上不是:
const expect = @import("std").testing.expect;
test "尝试使用数组初始化器交换数组元素" {
var arr: [2]u32 = .{ 1, 2 };
arr = .{ arr[1], arr[0] };
// 前一行等价于以下两行:
// arr[0] = arr[1];
// arr[1] = arr[0];
// 所以这失败了!
try expect(arr[0] == 2); // 成功
try expect(arr[1] == 1); // 失败
}
$ zig test result_location_interfering_with_swap.zig 1/1 result_location_interfering_with_swap.test.attempt to swap array elements with array initializer...FAIL (TestUnexpectedResult) /home/andy/dev/zig/lib/std/testing.zig:607:14: 0x102f019 in expect (std.zig) if (!ok) return error.TestUnexpectedResult; ^ /home/andy/dev/zig/doc/langref/result_location_interfering_with_swap.zig:10:5: 0x102f144 in test.attempt to swap array elements with array initializer (result_location_interfering_with_swap.zig) try expect(arr[1] == 1); // fails ^ 0 passed; 0 skipped; 1 failed. error: the following test command failed with exit code 1: /home/andy/dev/zig/.zig-cache/o/d439bc8d3e0f685e13e3c778e438793a/test --seed=0x9b2332d1
下表详细说明了一些常见表达式如何传播结果位置,其中
x 和
y
是任意子表达式。请注意,某些表达式即使它们本身具有结果位置,也无法向子表达式提供有意义的结果位置。
| 表达式 | 结果位置 | 子表达式结果位置 |
|---|---|---|
const
val: T = x
|
- |
x 的结果位置为
&val
|
var
val: T = x
|
- |
x 的结果位置为
&val
|
val = x |
- |
x 的结果位置为
&val
|
@as(T, x)
|
ptr |
x 没有结果位置 |
&x |
ptr |
x 没有结果位置 |
f(x) |
ptr |
x 没有结果位置 |
.{x} |
ptr |
x 的结果位置为
&ptr[0]
|
.{ .a = x }
|
ptr |
x 的结果位置为
&ptr.a
|
T{x} |
ptr |
x
没有结果位置(类型初始化器不传播结果位置)
|
T{ .a = x }
|
ptr |
x
没有结果位置(类型初始化器不传播结果位置)
|
@Type(x)
|
ptr |
x 没有结果位置 |
@typeInfo(x)
|
ptr |
x 没有结果位置 |
x << y
|
ptr |
x 和
y 没有结果位置
|
comptime §
Zig 重视表达式是否在编译时已知的概念。 这个概念在几个不同的地方使用,这些构建块用于保持语言的小巧、可读和强大。
介绍编译时概念 §
编译时参数 §
编译时参数是 Zig 实现泛型的方式。它是编译时鸭子类型。
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
fn gimmeTheBiggerFloat(a: f32, b: f32) f32 {
return max(f32, a, b);
}
fn gimmeTheBiggerInteger(a: u64, b: u64) u64 {
return max(u64, a, b);
}
在 Zig
中,类型是一等公民。它们可以被赋值给变量,作为参数传递给函数,并从函数返回。但是,它们只能在编译时已知的表达式中使用,这就是为什么上述片段中的参数
T 必须用
comptime 标记。
comptime
参数意味着:
- 在调用点,该值必须在编译时已知,否则会导致编译错误。
- 在函数定义中,该值在编译时已知。
例如,如果我们向上述片段引入另一个函数:
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
test "try to pass a runtime type" {
foo(false);
}
fn foo(condition: bool) void {
const result = max(if (condition) f32 else u64, 1234, 5678);
_ = result;
}
$ zig test test_unresolved_comptime_value.zig /home/andy/dev/zig/doc/langref/test_unresolved_comptime_value.zig:8:28: error: unable to resolve comptime value const result = max(if (condition) f32 else u64, 1234, 5678); ^~~~~~~~~ /home/andy/dev/zig/doc/langref/test_unresolved_comptime_value.zig:8:24: note: argument to comptime parameter must be comptime-known const result = max(if (condition) f32 else u64, 1234, 5678); ^~~~~~~~~~~~~~~~~~~~~~~~~~~ /home/andy/dev/zig/doc/langref/test_unresolved_comptime_value.zig:1:8: note: parameter declared comptime here fn max(comptime T: type, a: T, b: T) T { ^~~~~~~~ referenced by: test.try to pass a runtime type: /home/andy/dev/zig/doc/langref/test_unresolved_comptime_value.zig:5:8
这是一个错误,因为程序员尝试将仅在运行时已知的值传递给期望编译时已知值的函数。
另一种导致错误的情况是,如果我们传递的类型在分析函数时违反了类型检查器的规则。这就是编译时鸭子类型的含义。
例如:
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
test "try to compare bools" {
_ = max(bool, true, false);
}
$ zig test test_comptime_mismatched_type.zig /home/andy/dev/zig/doc/langref/test_comptime_mismatched_type.zig:2:18: error: operator > not allowed for type 'bool' return if (a > b) a else b; ~~^~~ referenced by: test.try to compare bools: /home/andy/dev/zig/doc/langref/test_comptime_mismatched_type.zig:5:12
另一方面,在带有
comptime
参数的函数定义内部,该值在编译时是已知的。这意味着如果我们愿意,实际上可以让它对
bool 类型起作用:
fn max(comptime T: type, a: T, b: T) T {
if (T == bool) {
return a or b;
} else if (a > b) {
return a;
} else {
return b;
}
}
test "try to compare bools" {
try @import("std").testing.expect(max(bool, false, true) == true);
}
$ zig test test_comptime_max_with_bool.zig 1/1 test_comptime_max_with_bool.test.try to compare bools...OK All 1 tests passed.
这可以工作,因为当条件在编译时已知时,Zig 会隐式内联
if
表达式,并且编译器保证它将跳过对未采用分支的分析。
这意味着在这种情况下为
max 生成的实际函数如下所示:
fn max(a: bool, b: bool) bool {
{
return a or b;
}
}
所有处理编译时已知值的代码都被消除了,我们只剩下完成任务所需的运行时代码。
对于
switch
表达式也是如此 -
当目标表达式在编译时已知时,它们会被隐式内联。
编译时变量 §
在 Zig 中,程序员可以将变量标记为
comptime。这向编译器保证变量的每次加载和存储都在编译时执行。任何违反此规则的行为都会导致编译错误。
这与我们可以
inline
循环的事实相结合,使我们能够编写部分在编译时求值、部分在运行时求值的函数。
例如:
const expect = @import("std").testing.expect;
const CmdFn = struct {
name: []const u8,
func: fn (i32) i32,
};
const cmd_fns = [_]CmdFn{
CmdFn{ .name = "one", .func = one },
CmdFn{ .name = "two", .func = two },
CmdFn{ .name = "three", .func = three },
};
fn one(value: i32) i32 {
return value + 1;
}
fn two(value: i32) i32 {
return value + 2;
}
fn three(value: i32) i32 {
return value + 3;
}
fn performFn(comptime prefix_char: u8, start_value: i32) i32 {
var result: i32 = start_value;
comptime var i = 0;
inline while (i < cmd_fns.len) : (i += 1) {
if (cmd_fns[i].name[0] == prefix_char) {
result = cmd_fns[i].func(result);
}
}
return result;
}
test "perform fn" {
try expect(performFn('t', 1) == 6);
try expect(performFn('o', 0) == 1);
try expect(performFn('w', 99) == 99);
}
$ zig test test_comptime_evaluation.zig 1/1 test_comptime_evaluation.test.perform fn...OK All 1 tests passed.
这个例子有点牵强,因为编译时求值组件是不必要的;如果全部在运行时完成,此代码也能正常工作。但它确实生成了不同的代码。在这个例子中,函数
performFn 针对提供的不同
prefix_char 值生成了三次:
// 来自这一行:
// expect(performFn('t', 1) == 6);
fn performFn(start_value: i32) i32 {
var result: i32 = start_value;
result = two(result);
result = three(result);
return result;
}
// 来自这一行:
// expect(performFn('o', 0) == 1);
fn performFn(start_value: i32) i32 {
var result: i32 = start_value;
result = one(result);
return result;
}
// 来自这一行:
// expect(performFn('w', 99) == 99);
fn performFn(start_value: i32) i32 {
var result: i32 = start_value;
_ = &result;
return result;
}
注意,即使在调试构建中也会发生这种情况。 这不是一种编写更优化代码的方式,而是一种确保应该在编译时发生的事情确实在编译时发生的方式。这可以捕获更多错误并允许表达能力,在其他语言中需要使用宏、生成的代码或预处理器才能实现。
编译时表达式 §
在 Zig
中,给定表达式是在编译时已知还是在运行时已知很重要。程序员可以使用
comptime
表达式来保证表达式将在编译时求值。如果无法做到这一点,编译器将发出错误。例如:
extern fn exit() noreturn;
test "foo" {
comptime {
exit();
}
}
$ zig test test_comptime_call_extern_function.zig /home/andy/dev/zig/doc/langref/test_comptime_call_extern_function.zig:5:13: error: comptime call of extern function exit(); ~~~~^~ /home/andy/dev/zig/doc/langref/test_comptime_call_extern_function.zig:4:5: note: 'comptime' keyword forces comptime evaluation comptime { ^~~~~~~~
程序在编译时调用
exit()(或任何其他外部函数)是没有意义的,所以这是一个编译错误。然而,comptime
表达式的作用远不止有时会导致编译错误。
在
comptime
表达式中:
-
所有变量都是
comptime变量。 -
所有
if、while、for和switch表达式在编译时求值,或者如果无法做到则发出编译错误。 -
所有
return和try表达式都是无效的(除非函数本身在编译时调用)。 - 所有具有运行时副作用或依赖于运行时值的代码都会发出编译错误。
- 所有函数调用都会导致编译器在编译时解释函数,如果函数试图做具有全局运行时副作用的事情,则发出编译错误。
这意味着程序员可以创建一个在编译时和运行时都被调用的函数,而无需对函数进行修改。
让我们看一个例子:
const expect = @import("std").testing.expect;
fn fibonacci(index: u32) u32 {
if (index < 2) return index;
return fibonacci(index - 1) + fibonacci(index - 2);
}
test "fibonacci" {
// 在运行时测试 fibonacci
try expect(fibonacci(7) == 13);
// 在编译时测试 fibonacci
try comptime expect(fibonacci(7) == 13);
}
$ zig test test_fibonacci_recursion.zig 1/1 test_fibonacci_recursion.test.fibonacci...OK All 1 tests passed.
想象一下,如果我们忘记了递归函数的基本情况并尝试运行测试:
const expect = @import("std").testing.expect;
fn fibonacci(index: u32) u32 {
//if (index < 2) return index;
return fibonacci(index - 1) + fibonacci(index - 2);
}
test "fibonacci" {
try comptime expect(fibonacci(7) == 13);
}
$ zig test test_fibonacci_comptime_overflow.zig /home/andy/dev/zig/doc/langref/test_fibonacci_comptime_overflow.zig:5:28: error: overflow of integer type 'u32' with value '-1' return fibonacci(index - 1) + fibonacci(index - 2); ~~~~~~^~~ /home/andy/dev/zig/doc/langref/test_fibonacci_comptime_overflow.zig:5:21: note: called at comptime here (7 times) return fibonacci(index - 1) + fibonacci(index - 2); ~~~~~~~~~^~~~~~~~~~~ /home/andy/dev/zig/doc/langref/test_fibonacci_comptime_overflow.zig:9:34: note: called at comptime here try comptime expect(fibonacci(7) == 13); ~~~~~~~~~^~~
编译器产生一个错误,这是尝试在编译时求值函数的堆栈跟踪。
幸运的是,我们使用了无符号整数,所以当我们尝试从 0 减去 1 时,它触发了非法行为,如果编译器知道它发生了,这总是一个编译错误。 但是如果我们使用有符号整数会发生什么?
const assert = @import("std").debug.assert;
fn fibonacci(index: i32) i32 {
//if (index < 2) return index;
return fibonacci(index - 1) + fibonacci(index - 2);
}
test "fibonacci" {
try comptime assert(fibonacci(7) == 13);
}
编译器应该注意到在编译时求值此函数花费了超过 1000 个分支,因此发出错误并放弃。如果程序员想要增加编译时计算的预算,他们可以使用名为 @setEvalBranchQuota 的内置函数将默认数字 1000 更改为其他值。
然而,编译器中存在一个设计缺陷,导致它发生栈溢出而不是在这里表现出正确的行为。对此我深感抱歉。我希望在下一个版本之前解决这个问题。
如果我们修复了基本情况,但在
expect 行中放入了错误的值会怎样?
const assert = @import("std").debug.assert;
fn fibonacci(index: i32) i32 {
if (index < 2) return index;
return fibonacci(index - 1) + fibonacci(index - 2);
}
test "fibonacci" {
try comptime assert(fibonacci(7) == 99999);
}
$ zig test test_fibonacci_comptime_unreachable.zig /home/andy/dev/zig/lib/std/debug.zig:559:14: error: reached unreachable code if (!ok) unreachable; // assertion failure ^~~~~~~~~~~ /home/andy/dev/zig/doc/langref/test_fibonacci_comptime_unreachable.zig:9:24: note: called at comptime here try comptime assert(fibonacci(7) == 99999); ~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
在容器级别(在任何函数之外),所有表达式都隐式地是
comptime
表达式。这意味着我们可以使用函数来初始化复杂的静态数据。例如:
const first_25_primes = firstNPrimes(25);
const sum_of_first_25_primes = sum(&first_25_primes);
fn firstNPrimes(comptime n: usize) [n]i32 {
var prime_list: [n]i32 = undefined;
var next_index: usize = 0;
var test_number: i32 = 2;
while (next_index < prime_list.len) : (test_number += 1) {
var test_prime_index: usize = 0;
var is_prime = true;
while (test_prime_index < next_index) : (test_prime_index += 1) {
if (test_number % prime_list[test_prime_index] == 0) {
is_prime = false;
break;
}
}
if (is_prime) {
prime_list[next_index] = test_number;
next_index += 1;
}
}
return prime_list;
}
fn sum(numbers: []const i32) i32 {
var result: i32 = 0;
for (numbers) |x| {
result += x;
}
return result;
}
test "variable values" {
try @import("std").testing.expect(sum_of_first_25_primes == 1060);
}
$ zig test test_container-level_comptime_expressions.zig 1/1 test_container-level_comptime_expressions.test.variable values...OK All 1 tests passed.
当我们编译这个程序时,Zig 生成的常量带有预先计算好的答案。以下是生成的 LLVM IR 中的行:
@0 = internal unnamed_addr constant [25 x i32] [i32 2, i32 3, i32 5, i32 7, i32 11, i32 13, i32 17, i32 19, i32 23, i32 29, i32 31, i32 37, i32 41, i32 43, i32 47, i32 53, i32 59, i32 61, i32 67, i32 71, i32 73, i32 79, i32 83, i32 89, i32 97]
@1 = internal unnamed_addr constant i32 1060
请注意,我们不需要对这些函数的语法做任何特殊处理。例如,我们可以按原样使用
sum
函数调用一个长度和值仅在运行时已知的数字切片。
泛型数据结构 §
Zig 使用编译时能力来实现泛型数据结构,而不引入任何特殊情况语法。
这是一个泛型 List 数据结构的例子。
fn List(comptime T: type) type {
return struct {
items: []T,
len: usize,
};
}
// 可以通过传入一个类型来实例化泛型 List 数据结构:
var buffer: [10]i32 = undefined;
var list = List(i32){
.items = &buffer,
.len = 0,
};
就是这样。它是一个返回匿名
struct 的函数。
出于错误消息和调试的目的,Zig
从创建匿名结构时调用的函数名称和参数推断出名称
"List(i32)"。
要显式地为类型命名,我们将其分配给一个常量。
const Node = struct {
next: ?*Node,
name: []const u8,
};
var node_a = Node{
.next = null,
.name = "Node A",
};
var node_b = Node{
.next = &node_a,
.name = "Node B",
};
在这个例子中,Node 结构引用了自己。
这可以工作,因为所有顶层声明都是顺序无关的。
只要编译器可以确定结构的大小,它就可以自由引用自己。
在这种情况下,Node
作为指针引用自己,指针在编译时具有明确定义的大小,所以它可以正常工作。
案例研究:Zig 中的 print §
将所有这些结合在一起,让我们看看 print 在
Zig 中是如何工作的。
const print = @import("std").debug.print;
const a_number: i32 = 1234;
const a_string = "foobar";
pub fn main() void {
print("here is a string: '{s}' here is a number: {}\n", .{ a_string, a_number });
}
$ zig build-exe print.zig $ ./print here is a string: 'foobar' here is a number: 1234
让我们深入了解这个实现,看看它是如何工作的:
const Writer = struct {
/// 调用 print 然后刷新缓冲区。
pub fn print(self: *Writer, comptime format: []const u8, args: anytype) anyerror!void {
const State = enum {
start,
open_brace,
close_brace,
};
comptime var start_index: usize = 0;
comptime var state = State.start;
comptime var next_arg: usize = 0;
inline for (format, 0..) |c, i| {
switch (state) {
State.start => switch (c) {
'{' => {
if (start_index < i) try self.write(format[start_index..i]);
state = State.open_brace;
},
'}' => {
if (start_index < i) try self.write(format[start_index..i]);
state = State.close_brace;
},
else => {},
},
State.open_brace => switch (c) {
'{' => {
state = State.start;
start_index = i;
},
'}' => {
try self.printValue(args[next_arg]);
next_arg += 1;
state = State.start;
start_index = i + 1;
},
's' => {
continue;
},
else => @compileError("Unknown format character: " ++ [1]u8{c}),
},
State.close_brace => switch (c) {
'}' => {
state = State.start;
start_index = i;
},
else => @compileError("Single '}' encountered in format string"),
},
}
}
comptime {
if (args.len != next_arg) {
@compileError("Unused arguments");
}
if (state != State.start) {
@compileError("Incomplete format string: " ++ format);
}
}
if (start_index < format.len) {
try self.write(format[start_index..format.len]);
}
try self.flush();
}
fn write(self: *Writer, value: []const u8) !void {
_ = self;
_ = value;
}
pub fn printValue(self: *Writer, value: anytype) !void {
_ = self;
_ = value;
}
fn flush(self: *Writer) !void {
_ = self;
}
};
这是一个概念验证实现;标准库中的实际函数具有更多格式化功能。
请注意,这不是硬编码到 Zig 编译器中的;这是标准库中的用户代码。
当从上面的示例代码分析此函数时,Zig 部分求值该函数并生成一个实际上看起来像这样的函数:
pub fn print(self: *Writer, arg0: []const u8, arg1: i32) !void {
try self.write("here is a string: '");
try self.printValue(arg0);
try self.write("' here is a number: ");
try self.printValue(arg1);
try self.write("\n");
try self.flush();
}
printValue
是一个接受任意类型参数的函数,并根据类型执行不同的操作:
const Writer = struct {
pub fn printValue(self: *Writer, value: anytype) !void {
switch (@typeInfo(@TypeOf(value))) {
.int => {
return self.writeInt(value);
},
.float => {
return self.writeFloat(value);
},
.pointer => {
return self.write(value);
},
else => {
@compileError("Unable to print type '" ++ @typeName(@TypeOf(value)) ++ "'");
},
}
}
fn write(self: *Writer, value: []const u8) !void {
_ = self;
_ = value;
}
fn writeInt(self: *Writer, value: anytype) !void {
_ = self;
_ = value;
}
fn writeFloat(self: *Writer, value: anytype) !void {
_ = self;
_ = value;
}
};
现在,如果我们给
print 传递太多参数会发生什么?
const print = @import("std").debug.print;
const a_number: i32 = 1234;
const a_string = "foobar";
test "print too many arguments" {
print("here is a string: '{s}' here is a number: {}\n", .{
a_string,
a_number,
a_number,
});
}
$ zig test test_print_too_many_args.zig /home/andy/dev/zig/lib/std/Io/Writer.zig:717:18: error: unused argument in 'here is a string: '{s}' here is a number: {} ' 1 => @compileError("unused argument in '" ++ fmt ++ "'"), ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ referenced by: print__anon_454: /home/andy/dev/zig/lib/std/debug.zig:231:23 test.print too many arguments: /home/andy/dev/zig/doc/langref/test_print_too_many_args.zig:7:10
Zig 为程序员提供了防止自己犯错所需的工具。
Zig
不关心格式参数是否是字符串字面量,只关心它是可以强制转换为
[]const
u8
的编译时已知值:
const print = @import("std").debug.print;
const a_number: i32 = 1234;
const a_string = "foobar";
const fmt = "here is a string: '{s}' here is a number: {}\n";
pub fn main() void {
print(fmt, .{ a_string, a_number });
}
$ zig build-exe print_comptime-known_format.zig $ ./print_comptime-known_format here is a string: 'foobar' here is a number: 1234
这可以正常工作。
Zig 不会在编译器中对字符串格式化进行特殊处理,而是公开足够的能力来在用户空间完成此任务。它这样做而不在 Zig 之上引入另一种语言,例如宏语言或预处理器语言。它完全是 Zig。
另请参阅:
汇编 §
对于某些用例,可能需要直接控制 Zig 程序生成的机器码,而不是依赖于 Zig 的代码生成。对于这些情况,可以使用内联汇编。以下是在 x86_64 Linux 上使用内联汇编实现 Hello, World 的示例:
pub fn main() noreturn {
const msg = "hello world\n";
_ = syscall3(SYS_write, STDOUT_FILENO, @intFromPtr(msg), msg.len);
_ = syscall1(SYS_exit, 0);
unreachable;
}
pub const SYS_write = 1;
pub const SYS_exit = 60;
pub const STDOUT_FILENO = 1;
pub fn syscall1(number: usize, arg1: usize) usize {
return asm volatile ("syscall"
: [ret] "={rax}" (-> usize),
: [number] "{rax}" (number),
[arg1] "{rdi}" (arg1),
: .{ .rcx = true, .r11 = true });
}
pub fn syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) usize {
return asm volatile ("syscall"
: [ret] "={rax}" (-> usize),
: [number] "{rax}" (number),
[arg1] "{rdi}" (arg1),
[arg2] "{rsi}" (arg2),
[arg3] "{rdx}" (arg3),
: .{ .rcx = true, .r11 = true });
}
$ zig build-exe inline_assembly.zig -target x86_64-linux $ ./inline_assembly hello world
剖析语法:
pub fn syscall1(number: usize, arg1: usize) usize {
// 内联汇编是一个返回值的表达式。
// `asm` 关键字开始表达式。
return asm
// `volatile` 是一个可选修饰符,告诉 Zig 这个
// 内联汇编表达式有副作用。没有
// `volatile`,如果结果未使用,Zig 允许删除内联汇编
// 代码。
volatile (
// 接下来是一个编译时字符串,即汇编代码。
// 在此字符串中,可以在需要寄存器的地方使用 `%[ret]`、`%[number]`
// 或 `%[arg1]`,以指定
// Zig 用于参数或返回值的寄存器,
// 如果使用寄存器约束字符串。但在
// 下面的代码中,没有使用这个。可以通过
// 双百分号转义来获得字面 `%`: `%%`。
// 多行字符串语法在这里通常很方便。
\\syscall
// 接下来是输出。将来 Zig 可能
// 支持多个输出,取决于
// https://github.com/ziglang/zig/issues/215 如何解决。
// 允许没有输出,在这种情况下
// 这个冒号将直接跟在输入的冒号后面。
:
// 这指定了在上面汇编字符串的 `%[ret]` 语法中
// 要使用的名称。此示例不使用它,
// 但语法是强制性的。
[ret]
// 接下来是输出约束字符串。此功能在
// Zig 中仍被视为不稳定,因此必须使用 LLVM/GCC 文档
// 来理解语义。
// http://releases.llvm.org/10.0.0/docs/LangRef.html#inline-asm-constraint-string
// https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
// 在此示例中,约束字符串表示"此
// 内联汇编指令的结果值是 $rax 中的任何值"。
"={rax}"
// 接下来是值绑定,或 `->` 然后是类型。
// 类型是内联汇编表达式的结果类型。
// 如果是值绑定,则会使用 `%[ret]` 语法
// 来引用绑定到该值的寄存器。
(-> usize),
// 接下来是输入列表。
// 这些输入的约束意味着,"当执行汇编代码时,
// $rax 应具有 `number` 的值,$rdi 应具有
// `arg1` 的值"。允许任意数量的输入参数,
// 包括无。
: [number] "{rax}" (number),
[arg1] "{rdi}" (arg1),
// 接下来是破坏列表。这些声明一组寄存器,
// 执行此汇编代码后其值将不会保留。
// 这些不包括输出或输入寄存器。特殊破坏
// 值 "memory" 表示汇编写入任意未声明的
// 内存位置 - 不仅是声明的间接
// 输出指向的内存。在此示例中,我们列出了 $rcx 和 $r11,因为已知
// 内核系统调用不保留这些寄存器。
: .{ .rcx = true, .r11 = true });
}
对于 x86 和 x86_64 目标,语法是 AT&T 语法,而不是更流行的 Intel 语法。这是由于技术限制;汇编解析由 LLVM 提供,其对 Intel 语法的支持有问题且测试不充分。
有一天 Zig 可能会有自己的汇编器。这将允许它更无缝地集成到语言中,并兼容流行的 NASM 语法。在 1.0.0 发布之前,本文档部分将更新,对 AT&T 与 Intel/NASM 语法的状态给出决定性的声明。
输出约束 §
输出约束在 Zig 中仍被视为不稳定,因此必须使用 LLVM 文档 和 GCC 文档 来理解语义。
请注意,计划通过 issue #215 对输出约束进行一些破坏性更改。
输入约束 §
输入约束在 Zig 中仍被视为不稳定,因此必须使用 LLVM 文档 和 GCC 文档 来理解语义。
请注意,计划通过 issue #215 对输入约束进行一些破坏性更改。
破坏 §
破坏是执行汇编代码后其值将不会保留的寄存器集。这些不包括输出或输入寄存器。特殊破坏值
"memory"
表示汇编导致写入任意未声明的内存位置 -
不仅是声明的间接输出指向的内存。
对于给定的内联汇编表达式,未能声明完整的破坏集是未检查的非法行为。
全局汇编 §
当汇编表达式出现在容器级别的 comptime 块中时,这是全局汇编。
这种汇编与内联汇编有不同的规则。首先,volatile
无效,因为所有全局汇编都无条件包含。
其次,没有输入、输出或破坏。所有全局汇编都逐字连接成一个长字符串并一起汇编。内联汇编表达式中关于
% 的模板替换规则不适用。
const std = @import("std");
const expect = std.testing.expect;
comptime {
asm (
\\.global my_func;
\\.type my_func, @function;
\\my_func:
\\ lea (%rdi,%rsi,1),%eax
\\ retq
);
}
extern fn my_func(a: i32, b: i32) i32;
test "global assembly" {
try expect(my_func(12, 34) == 46);
}
$ zig test test_global_assembly.zig -target x86_64-linux -fllvm 1/1 test_global_assembly.test.global assembly...OK All 1 tests passed.
原子操作 §
TODO: @atomic rmw
TODO: 内置原子内存排序枚举
另请参阅:
异步函数 §
异步函数在 0.11.0 版本发布时出现了回归。目前的计划是将它们作为支持 I/O 实现的更低级别原语重新引入。
跟踪问题: 提案:无栈协程作为低级原语
内置函数 §
内置函数由编译器提供,并以 @ 为前缀。
参数上的
comptime
关键字表示该参数必须在编译时已知。
@addrSpaceCast §
@addrSpaceCast(ptr: anytype) anytype
将指针从一个地址空间转换到另一个地址空间。新的地址空间根据结果类型推断。根据当前目标和地址空间,此转换可能是无操作、复杂操作或非法的。如果转换合法,则生成的指针指向与指针操作数相同的内存位置。在相同地址空间之间转换指针始终有效。
@addWithOverflow §
@addWithOverflow(a: anytype, b: anytype) struct { @TypeOf(a, b), u1 }
执行
a + b 并返回包含结果和可能的溢出位的元组。
@alignCast §
@alignCast(ptr: anytype) anytype
ptr 可以是 *T、?*T
或 []T。
改变指针的对齐方式。要使用的对齐方式根据结果类型推断。
向生成的代码添加指针对齐安全检查,以确保指针如承诺的那样对齐。
@alignOf §
@alignOf(comptime T: type) comptime_int
此函数返回此类型在当前目标上应对齐到的字节数,以匹配 C ABI。当指针的子类型具有此对齐方式时,可以从类型中省略对齐方式。
const assert = @import("std").debug.assert;
comptime {
assert(*u32 == *align(@alignOf(u32)) u32);
}
结果是特定于目标的编译时常量。保证小于或等于 @sizeOf(T)。
另请参阅:
@as §
@as(comptime T: type, expression) T
执行类型强制转换。当转换明确且安全时允许此转换,并且是在可能的情况下在类型之间转换的首选方式。
@atomicLoad §
@atomicLoad(comptime T: type, ptr: *const T, comptime ordering: AtomicOrder) T
此内置函数原子地解引用指向
T 的指针并返回值。
T 必须是指针、bool、浮点数、整数、枚举或打包结构。
AtomicOrder 可以通过
@import("std").builtin.AtomicOrder
找到。
另请参阅:
@atomicRmw §
@atomicRmw(comptime T: type, ptr: *T, comptime op: AtomicRmwOp, operand: T, comptime ordering: AtomicOrder) T
此内置函数解引用指向
T 的指针,原子地修改值并返回先前的值。
T 必须是指针、bool、浮点数、整数、枚举或打包结构。
AtomicOrder 可以通过
@import("std").builtin.AtomicOrder
找到。
AtomicRmwOp 可以通过
@import("std").builtin.AtomicRmwOp
找到。
另请参阅:
@atomicStore §
@atomicStore(comptime T: type, ptr: *T, value: T, comptime ordering: AtomicOrder) void
此内置函数解引用指向
T 的指针并原子地存储给定的值。
T 必须是指针、bool、浮点数、整数、枚举或打包结构。
AtomicOrder 可以通过
@import("std").builtin.AtomicOrder
找到。
另请参阅:
@bitCast §
@bitCast(value: anytype) anytype
将一种类型的值转换为另一种类型。返回类型是推断的结果类型。
断言
@sizeOf(@TypeOf(value)) ==
@sizeOf(DestType)。
断言
@typeInfo(DestType) != .pointer。如果需要这个,请使用
@ptrCast
或
@ptrFromInt。
例如可用于这些事情:
-
将
f32转换为u32位 -
将
i32转换为u32保留二进制补码
如果
value
在编译时已知,则在编译时工作。对未定义布局的值进行位转换是编译错误;这意味着,除了具有专用转换内置函数的类型(枚举、指针、错误集)的限制之外,裸结构、错误联合、切片、可选类型以及任何其他没有明确定义内存布局的类型也不能在此操作中使用。
@bitOffsetOf §
@bitOffsetOf(comptime T: type, comptime field_name: []const u8) comptime_int
返回字段相对于其包含结构的位偏移量。
对于非打包结构,这将始终能被
8 整除。
对于打包结构,非字节对齐的字段将共享一个字节偏移量,但它们将有不同的位偏移量。
另请参阅:
@bitSizeOf §
@bitSizeOf(comptime T: type) comptime_int
此函数返回在内存中存储
T
所需的位数,如果该类型是打包结构/联合中的字段。
结果是特定于目标的编译时常量。
此函数在运行时测量大小。对于在运行时不允许的类型,例如
comptime_int
和 type,结果为 0。
另请参阅:
@branchHint §
@branchHint(hint: BranchHint) void
向优化器提示给定控制流分支被到达的可能性。
BranchHint 可以通过
@import("std").builtin.BranchHint
找到。
此函数仅在控制流分支的第一条语句或函数的第一条语句中有效。
@breakpoint §
@breakpoint() void
此函数插入特定于平台的调试陷阱指令,导致调试器在此处中断。
与
@trap()
不同,如果程序恢复,执行可能会在此点之后继续。
此函数仅在函数作用域内有效。
另请参阅:
@mulAdd §
@mulAdd(comptime T: type, a: T, b: T, c: T) T
融合乘加,类似于
(a * b) + c,但只舍入一次,因此更准确。
@byteSwap §
@byteSwap(operand: anytype) T
@TypeOf(operand)
必须是整数类型或位计数能被 8 整除的整数向量类型。
交换整数的字节序。这会将大端整数转换为小端整数, 并将小端整数转换为大端整数。
注意,对于与字节序相关的内存布局,整数类型应该 与
@sizeOf 报告的字节数相关。这在
u24
中得到了演示。@sizeOf(u24) == 4,这意味着 存储在内存中的
u24 占用 4
个字节,这 4 个字节是在
小端与大端系统上被交换的。另一方面,如果
T 被指定为
u24,则只反转 3 个字节。
@bitReverse §
@bitReverse(integer: anytype) T
@TypeOf(anytype)
接受任何整数类型或整数向量类型。
反转整数值的位模式,包括符号位(如果适用)。
例如 0b10110110 (u8 =
182,
i8 = -74) 变为 0b01101101 (u8 =
109,
i8 =
109)。
@offsetOf §
@offsetOf(comptime T: type, comptime field_name: []const u8) comptime_int
返回字段相对于其包含结构的字节偏移量。
另请参阅:
@call §
@call(modifier: std.builtin.CallModifier, function: anytype, args: anytype) anytype
调用函数,方式与使用括号调用表达式相同:
const expect = @import("std").testing.expect;
test "noinline function call" {
try expect(@call(.auto, add, .{ 3, 9 }) == 12);
}
fn add(a: i32, b: i32) i32 {
return a + b;
}
$ zig test test_call_builtin.zig 1/1 test_call_builtin.test.noinline function call...OK All 1 tests passed.
@call
允许比普通函数调用语法更大的灵活性。
CallModifier 枚举在此重现:
pub const CallModifier = enum {
/// 等同于函数调用语法。
auto,
/// 等同于与函数调用语法一起使用的 async 关键字。
async_kw,
/// 阻止尾调用优化。这保证返回
/// 地址将指向调用点,而不是调用点的
/// 调用点。如果调用在其他情况下需要尾调用
/// 或内联,则会发出编译错误。
never_tail,
/// 保证调用不会被内联。如果调用
/// 在其他情况下需要内联,则会发出编译错误。
never_inline,
/// 断言函数调用不会挂起。这允许
/// 非异步函数调用异步函数。
no_async,
/// 保证调用将通过尾调用优化生成。
/// 如果这不可能,则会发出编译错误。
always_tail,
/// 保证调用将在调用点内联。
/// 如果这不可能,则会发出编译错误。
always_inline,
/// 在编译时评估调用。如果调用无法在
/// 编译时完成,则会发出编译错误。
compile_time,
};
@cDefine §
@cDefine(comptime name: []const u8, value) void
此函数只能在
@cImport
内部出现。
这会将 #define $name $value 追加到
@cImport
临时缓冲区。
要定义没有值的宏,像这样:
#define _GNU_SOURCE
使用 void 值,像这样:
@cDefine("_GNU_SOURCE", {})
另请参阅:
@cImport §
@cImport(expression) type
此函数解析 C 代码并将函数、类型、变量 和兼容的宏定义导入到新的空结构类型中,然后 返回该类型。
expression 在编译时被解释。内建函数
@cInclude、@cDefine
和
@cUndef 在
此表达式中工作,追加到临时缓冲区,然后将其解析为 C
代码。
通常您应该在整个应用程序中只有一个
@cImport,因为它可以避免编译器 多次调用
clang,并防止内联函数重复。
拥有多个
@cImport
表达式的原因是:
-
为了避免符号冲突,例如如果 foo.h 和 bar.h 都
#define CONNECTION_COUNT - 使用不同的预处理器定义分析 C 代码
另请参阅:
@cInclude §
@cInclude(comptime path: []const u8) void
此函数只能在
@cImport
内部出现。
这会将 #include <$path>\n 追加到
c_import
临时缓冲区。
另请参阅:
@clz §
@clz(operand: anytype) anytype
@TypeOf(operand)
必须是整数类型或整数向量类型。
计算整数中最高有效位(大端意义上的前导)零的数量 - "计数前导零"。
返回类型是无符号整数或无符号整数向量,具有可以表示整数类型位数的 最少位数。
如果 operand 为零,@clz
返回 整数类型 T 的位宽。
另请参阅:
@cmpxchgStrong §
@cmpxchgStrong(comptime T: type, ptr: *T, expected_value: T, new_value: T, success_order: AtomicOrder, fail_order: AtomicOrder) ?T
此函数执行强原子比较交换操作,如果当前值是给定的期望值,
则返回 null。它等同于以下代码, 除了是原子的:
fn cmpxchgStrongButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_value: T) ?T {
const old_value = ptr.*;
if (old_value == expected_value) {
ptr.* = new_value;
return null;
} else {
return old_value;
}
}
如果您在重试循环中使用 cmpxchg,@cmpxchgWeak 是更好的选择,因为它可以 在机器指令中更有效地实现。
T 必须是指针、bool、 整数、枚举或紧缩结构。
@typeInfo(@TypeOf(ptr)).pointer.alignment
必须
>=
@sizeOf(T).
AtomicOrder 可以通过
@import("std").builtin.AtomicOrder
找到。
另请参阅:
@cmpxchgWeak §
@cmpxchgWeak(comptime T: type, ptr: *T, expected_value: T, new_value: T, success_order: AtomicOrder, fail_order: AtomicOrder) ?T
此函数执行弱原子比较交换操作,如果当前值是给定的期望值,
则返回 null。它等同于以下代码, 除了是原子的:
fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_value: T) ?T {
const old_value = ptr.*;
if (old_value == expected_value and usuallyTrueButSometimesFalse()) {
ptr.* = new_value;
return null;
} else {
return old_value;
}
}
如果您在重试循环中使用
cmpxchg,偶发的失败不会有问题,并且
cmpxchgWeak
是更好的选择,因为它可以在机器指令中更有效地实现。
但是,如果您需要更强的保证,请使用
@cmpxchgStrong。
T 必须是指针、bool、 整数、枚举或紧缩结构。
@typeInfo(@TypeOf(ptr)).pointer.alignment
必须
>=
@sizeOf(T).
AtomicOrder 可以通过
@import("std").builtin.AtomicOrder
找到。
另请参阅:
@compileError §
@compileError(comptime msg: []const u8) noreturn
此函数在语义分析时会导致编译错误, 错误消息为
msg。
有几种方法可以避免代码被语义检查,例如
使用带有编译时常量的
if 或
switch, 以及
comptime 函数。
@compileLog §
@compileLog(...) void
此函数在编译时打印传递给它的参数。
为了防止意外地在代码库中留下编译日志语句, 会向构建中添加编译错误,指向编译 日志语句。此错误阻止代码生成,但 不会干扰分析。
此函数可用于对 编译时执行的代码进行"printf 调试"。
const print = @import("std").debug.print;
const num1 = blk: {
var val1: i32 = 99;
@compileLog("comptime val1 = ", val1);
val1 = val1 + 1;
break :blk val1;
};
test "main" {
@compileLog("comptime in main");
print("Runtime in main, num1 = {}.\n", .{num1});
}
$ zig test test_compileLog_builtin.zig /home/andy/dev/zig/doc/langref/test_compileLog_builtin.zig:5:5: error: found compile log statement @compileLog("comptime val1 = ", val1); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /home/andy/dev/zig/doc/langref/test_compileLog_builtin.zig:11:5: note: also here @compileLog("comptime in main"); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ referenced by: test.main: /home/andy/dev/zig/doc/langref/test_compileLog_builtin.zig:13:46 Compile Log Output: @as(*const [16:0]u8, "comptime val1 = "), @as(i32, 99) @as(*const [16:0]u8, "comptime in main")
@constCast §
@constCast(value: anytype) DestType
从指针中移除
const 限定符。
@ctz §
@ctz(operand: anytype) anytype
@TypeOf(operand)
必须是整数类型或整数向量类型。
计算整数中最低有效位(大端意义上的尾随)零的数量 - "计数尾随零"。
返回类型是无符号整数或无符号整数向量,具有可以表示整数类型位数的 最少位数。
如果 operand 为零,@ctz
返回 整数类型 T 的位宽。
另请参阅:
@cUndef §
@cUndef(comptime name: []const u8) void
此函数只能在
@cImport
内部出现。
这会将 #undef $name 追加到
@cImport
临时缓冲区。
另请参阅:
@cVaArg §
@cVaArg(operand: *std.builtin.VaList, comptime T: type) T
实现 C 宏 va_arg。
另请参阅:
@cVaCopy §
@cVaCopy(src: *std.builtin.VaList) std.builtin.VaList
实现 C 宏 va_copy。
另请参阅:
@cVaEnd §
@cVaEnd(src: *std.builtin.VaList) void
实现 C 宏 va_end。
另请参阅:
@cVaStart §
@cVaStart() std.builtin.VaList
实现 C 宏
va_start。仅在可变参数函数内有效。
另请参阅:
@divExact §
@divExact(numerator: T, denominator: T) T
精确除法。调用者保证
denominator !=
0
并且
@divTrunc(numerator, denominator) * denominator ==
numerator。
-
@divExact(6, 3) == 2 -
@divExact(a, b) * b == a
对于返回可能错误代码的函数,请使用
@import("std").math.divExact。
另请参阅:
@divFloor §
@divFloor(numerator: T, denominator: T) T
向下取整除法。向负无穷方向舍入。对于无符号整数,它与
numerator / denominator 相同。调用者保证
denominator !=
0
并且
!(@typeInfo(T) ==
.int and T.is_signed
and numerator ==
std.math.minInt(T)
and denominator ==
-1)。
-
@divFloor(-5, 3) == -2 -
(@divFloor(a, b) * b) + @mod(a, b) == a
对于返回可能错误代码的函数,请使用
@import("std").math.divFloor。
另请参阅:
@divTrunc §
@divTrunc(numerator: T, denominator: T) T
截断除法。向零方向舍入。对于无符号整数,它与
numerator / denominator 相同。调用者保证
denominator !=
0
并且
!(@typeInfo(T) ==
.int and T.is_signed
and numerator ==
std.math.minInt(T)
and denominator ==
-1)。
-
@divTrunc(-5, 3) == -1 -
(@divTrunc(a, b) * b) + @rem(a, b) == a
对于返回可能错误代码的函数,请使用
@import("std").math.divTrunc。
另请参阅:
@embedFile §
@embedFile(comptime path: []const u8) *const [N:0]u8
此函数返回一个编译时常量指针,指向以 null
结尾的固定大小数组, 其长度等于
path
给定文件的字节数。数组的内容是文件的内容。
这等同于具有文件内容的字符串字面量。
path 是绝对路径或相对于当前文件的路径,就像
@import
一样。
另请参阅:
@enumFromInt §
@enumFromInt(integer: anytype) anytype
将整数转换为 枚举 值。返回类型是推断的结果类型。
尝试转换在枚举中没有相应值的整数会调用 安全检查的非法行为。 注意,非穷尽枚举对于枚举整数标签类型中的所有 整数都有相应的值:_
值表示枚举标签类型中所有剩余的未命名整数。
另请参阅:
@errorFromInt §
@errorFromInt(value: std.meta.Int(.unsigned, @bitSizeOf(anyerror))) anyerror
从错误的整数表示形式转换为全局错误集类型。
通常建议避免此 转换,因为错误的整数表示形式在源代码更改时不稳定。
尝试转换不对应于任何错误的整数会导致 安全检查的非法行为。
另请参阅:
@errorName §
@errorName(err: anyerror) [:0]const u8
此函数返回错误的字符串表示形式。
error.OutOfMem
的字符串表示形式是
"OutOfMem"。
如果在整个应用程序中没有对
@errorName
的调用, 或者所有调用对
err 都有编译时已知的值,那么不会
生成错误名称表。
@errorReturnTrace §
@errorReturnTrace() ?*builtin.StackTrace
如果二进制文件构建时启用了错误返回跟踪,并且在 调用具有错误或错误联合返回类型的函数的函数中调用此函数,则返回 堆栈跟踪对象。否则返回 null。
@errorCast §
@errorCast(value: anytype) anytype
将错误集或错误联合值从一个错误集转换为另一个错误集。返回类型是 推断的结果类型。尝试转换不在目标错误 集中的错误会导致安全检查的非法行为。
@export §
@export(comptime ptr: *const anyopaque, comptime options: std.builtin.ExportOptions) void
在输出目标文件中创建一个符号,该符号指向
ptr 的目标。
ptr 必须指向全局变量或编译时已知的常量。
可以从
comptime
块调用此内建函数以有条件地导出符号。 当
ptr 指向具有 C 调用约定的函数并且
options.linkage 为
.strong 时,这等同于 在函数上使用的
export 关键字:
comptime {
@export(&internalName, .{ .name = "foo", .linkage = .strong });
}
fn internalName() callconv(.c) void {}
$ zig build-obj export_builtin.zig
这等同于:
export fn foo() void {}
$ zig build-obj export_builtin_equivalent_code.zig
注意,即使使用
export,也可以使用 @"foo" 语法作为
标识符来为符号名称选择任何字符串:
export fn @"A function name that is a complete sentence."() void {}
$ zig build-obj export_any_symbol_name.zig
查看生成的目标文件时,您可以看到符号是逐字使用的:
00000000000001f0 T A function name that is a complete sentence.
另请参阅:
@extern §
@extern(T: type, comptime options: std.builtin.ExternOptions) T
在输出目标文件中创建对外部符号的引用。 T 必须是指针类型。
另请参阅:
@field §
@field(lhs: anytype, comptime field_name: []const u8) (field)
通过编译时字符串执行字段访问。对字段和声明都有效。
const std = @import("std");
const Point = struct {
x: u32,
y: u32,
pub var z: u32 = 1;
};
test "field access by string" {
const expect = std.testing.expect;
var p = Point{ .x = 0, .y = 0 };
@field(p, "x") = 4;
@field(p, "y") = @field(p, "x") + 1;
try expect(@field(p, "x") == 4);
try expect(@field(p, "y") == 5);
}
test "decl access by string" {
const expect = std.testing.expect;
try expect(@field(Point, "z") == 1);
@field(Point, "z") = 2;
try expect(@field(Point, "z") == 2);
}
$ zig test test_field_builtin.zig 1/2 test_field_builtin.test.field access by string...OK 2/2 test_field_builtin.test.decl access by string...OK All 2 tests passed.
@fieldParentPtr §
@fieldParentPtr(comptime field_name: []const u8, field_ptr: *T) anytype
给定指向结构或联合字段的指针,返回指向包含该字段的结构或联合的指针。 返回类型(指向相关父结构或联合的指针)是推断的结果类型。
如果 field_ptr 不指向结果类型实例的
field_name 字段,
并且结果类型具有未定义的布局,则调用未检查的非法行为。
@FieldType §
@FieldType(comptime Type: type, comptime field_name: []const u8) type
给定类型和其字段之一的名称,返回该字段的类型。
@floatCast §
@floatCast(value: anytype) anytype
从一种浮点类型转换为另一种浮点类型。此转换是安全的,但可能导致 数值失去精度。返回类型是推断的结果类型。
@floatFromInt §
@floatFromInt(int: anytype) anytype
将整数转换为最接近的浮点表示形式。返回类型是推断的结果类型。 要进行相反的转换,请使用 @intFromFloat。此操作对于 所有整数类型的所有值都是合法的。
@frameAddress §
@frameAddress() usize
此函数返回当前堆栈帧的基指针。
其含义是特定于目标的,并且在所有 平台上都不一致。由于积极的优化, 在发布模式下帧地址可能不可用。
此函数仅在函数作用域内有效。
@hasDecl §
@hasDecl(comptime Container: type, comptime name: []const u8) bool
返回容器是否具有与
name 匹配的声明。
const std = @import("std");
const expect = std.testing.expect;
const Foo = struct {
nope: i32,
pub var blah = "xxx";
const hi = 1;
};
test "@hasDecl" {
try expect(@hasDecl(Foo, "blah"));
// 即使 `hi` 是私有的,@hasDecl 也会返回 true,因为此测试
// 与 Foo 在同一个文件作用域中。如果 Foo 在
// 不同的文件中声明,它将返回 false。
try expect(@hasDecl(Foo, "hi"));
// @hasDecl 用于声明;不用于字段。
try expect(!@hasDecl(Foo, "nope"));
try expect(!@hasDecl(Foo, "nope1234"));
}
$ zig test test_hasDecl_builtin.zig 1/1 test_hasDecl_builtin.test.@hasDecl...OK All 1 tests passed.
另请参阅:
@hasField §
@hasField(comptime Container: type, comptime name: []const u8) bool
返回结构、联合或枚举的字段名是否存在。
结果是编译时常量。
它不包括函数、变量或常量。
另请参阅:
@import §
@import(comptime target: []const u8) anytype
导入
target
处的文件,如果尚未添加,则将其添加到编译中。target
可以是从包含
@import
调用的文件到另一个文件的相对路径, 也可以是模块的名称,导入指向该模块的根源文件。
无论哪种方式,文件路径都必须以 .zig(对于
Zig 源文件)或 .zon(对于 ZON
数据文件)结尾。
如果 target 指向 Zig 源文件,则
@import
返回 该文件的对应结构类型,本质上就好像内建调用被
struct { FILE_CONTENTS
}
替换了一样。返回类型是
type。
如果 target 指向 ZON 文件,则
@import
返回 文件中字面量的值。如果有推断的结果类型, 则返回类型是该类型,并且 ZON
字面量被解释为该类型(结果类型通过 ZON 表达式传播)。否则,返回类型是等效 Zig
表达式的类型,本质上就好像 内建调用被 ZON
文件内容替换了一样。
以下模块始终可用于导入:
-
@import("std")- Zig 标准库 -
@import("builtin")- 特定于目标的信息。命令zig build-exe --show-builtin将源代码输出到 stdout 以供参考。 -
@import("root")- 根模块的别名。在典型的项目结构中,这意味着它指向src/main.zig。
另请参阅:
@inComptime §
@inComptime() bool
返回内建函数是否在
comptime
上下文中运行。结果是编译时常量。
这可用于提供函数的替代编译时友好实现。例如,它不应该用于从编译时评估中排除某些函数。
另请参阅:
@intCast §
@intCast(int: anytype) anytype
将整数转换为另一个整数,同时保持相同的数值。 返回类型是推断的结果类型。 尝试转换超出目标类型范围的数字会导致 安全检查的非法行为。
test "integer cast panic" {
var a: u16 = 0xabcd; // runtime-known
_ = &a;
const b: u8 = @intCast(a);
_ = b;
}
$ zig test test_intCast_builtin.zig 1/1 test_intCast_builtin.test.integer cast panic...thread 2898212 panic: integer does not fit in destination type /home/andy/dev/zig/doc/langref/test_intCast_builtin.zig:4:19: 0x102c020 in test.integer cast panic (test_intCast_builtin.zig) const b: u8 = @intCast(a); ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:218:25: 0x115cb50 in mainTerminal (test_runner.zig) if (test_fn.func()) |_| { ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:66:28: 0x1155d71 in main (test_runner.zig) return mainTerminal(); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x114fb0d in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x114f3a1 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) error: the following test command crashed: /home/andy/dev/zig/.zig-cache/o/056fc3b607934a9389a99437800346de/test --seed=0x9fcd81fa
要截断超出目标类型范围的数字的有效位,请使用 @truncate。
如果 T 是
comptime_int, 那么这在语义上等同于类型强制转换。
@intFromBool §
@intFromBool(value: bool) u1
将
true 转换为
@as(u1, 1),将
false 转换为
@as(u1, 0)。
@intFromEnum §
@intFromEnum(enum_or_tagged_union: anytype) anytype
将枚举值转换为其整数标签类型。当传递标记联合时, 标签值用作枚举值。
如果只有一个可能的枚举值,结果是在
comptime 时已知的
comptime_int。
另请参阅:
@intFromError §
@intFromError(err: anytype) std.meta.Int(.unsigned, @bitSizeOf(anyerror))
支持以下类型:
将错误转换为错误的整数表示形式。
通常建议避免此 转换,因为错误的整数表示形式在源代码更改时不稳定。
另请参阅:
@intFromFloat §
@intFromFloat(float: anytype) anytype
将浮点数的整数部分转换为推断的结果类型。
如果浮点数的整数部分无法适应目标类型, 它会调用安全检查的非法行为。
另请参阅:
@intFromPtr §
@intFromPtr(value: anytype) usize
将 value 转换为指针地址的
usize。 value 可以是 *T 或
?*T。
要进行相反的转换,请使用 @ptrFromInt
@max §
@max(...) T
接受两个或更多参数并返回包含的最大值(最大值)。此内建函数接受整数、浮点数以及两者的向量。在后一种情况下,操作按元素执行。
NaN 的处理如下:返回包含的最大非 NaN 值。如果所有操作数都是 NaN,则返回 NaN。
另请参阅:
@memcpy §
@memcpy(noalias dest, noalias source) void
此函数将字节从一个内存区域复制到另一个内存区域。
dest 必须是可变切片、指向数组的可变指针或
可变多项指针。它可以具有任何
对齐方式,并且可以具有任何元素类型。
source 必须是切片、指向 数组的指针或多项指针。它可以 具有任何对齐方式,并且可以具有任何元素类型。
source 元素类型必须与
dest 元素类型具有相同的 内存表示形式。
与 for 循环类似,source
和
dest
中至少有一个必须提供长度,如果提供两个长度,
它们必须相等。
最后,两个内存区域不能重叠。
@memset §
@memset(dest, elem) void
此函数将内存区域的所有元素设置为 elem。
dest 必须是可变切片或指向数组的可变指针。
它可以具有任何对齐方式,并且可以具有任何元素类型。
elem 强制转换为
dest 的元素类型。
对于安全地从内存中清除敏感内容,您应该使用
std.crypto.secureZero
@memmove §
@memmove(dest, source) void
此函数将字节从一个内存区域复制到另一个内存区域,但与 @memcpy 不同,区域可以重叠。
dest 必须是可变切片、指向数组的可变指针或
可变多项指针。它可以具有任何
对齐方式,并且可以具有任何元素类型。
source 必须是切片、指向 数组的指针或多项指针。它可以 具有任何对齐方式,并且可以具有任何元素类型。
source 元素类型必须与
dest 元素类型具有相同的 内存表示形式。
与 for 循环类似,source
和
dest
中至少有一个必须提供长度,如果提供两个长度,
它们必须相等。
@min §
@min(...) T
接受两个或更多参数并返回包含的最小值(最小值)。此内建函数接受整数、浮点数以及两者的向量。在后一种情况下,操作按元素执行。
NaN 的处理如下:返回包含的最小非 NaN 值。如果所有操作数都是 NaN,则返回 NaN。
另请参阅:
@wasmMemorySize §
@wasmMemorySize(index: u32) usize
此函数以 Wasm 页面为单位返回由 index 标识的
Wasm 内存的大小, 作为无符号值。注意每个 Wasm 页面为
64KB。
此函数是低级内在函数,没有安全机制,通常对针对 Wasm
的分配器
设计者有用。因此,除非您从头开始编写新的分配器,否则应使用
类似
@import("std").heap.WasmPageAllocator
的东西。
另请参阅:
@wasmMemoryGrow §
@wasmMemoryGrow(index: u32, delta: usize) isize
此函数将由 index 标识的 Wasm 内存的大小增加
delta,以无符号 Wasm 页数为单位。注意每个
Wasm 页面 为
64KB。成功时,返回之前的内存大小;失败时,如果分配失败,
返回 -1。
此函数是低级内在函数,没有安全机制,通常对针对 Wasm
的分配器
设计者有用。因此,除非您从头开始编写新的分配器,否则应使用
类似
@import("std").heap.WasmPageAllocator
的东西。
const std = @import("std");
const native_arch = @import("builtin").target.cpu.arch;
const expect = std.testing.expect;
test "@wasmMemoryGrow" {
if (native_arch != .wasm32) return error.SkipZigTest;
const prev = @wasmMemorySize(0);
try expect(prev == @wasmMemoryGrow(0, 1));
try expect(prev + 1 == @wasmMemorySize(0));
}
$ zig test test_wasmMemoryGrow_builtin.zig 1/1 test_wasmMemoryGrow_builtin.test.@wasmMemoryGrow...SKIP 0 passed; 1 skipped; 0 failed.
另请参阅:
@mod §
@mod(numerator: T, denominator: T) T
取模除法。对于无符号整数,这与
numerator % denominator 相同。调用者保证
denominator !=
0,否则 当启用运行时安全检查时,操作将导致余数除零。
-
@mod(-5, 3) == 1 -
(@divFloor(a, b) * b) + @mod(a, b) == a
对于返回错误代码的函数,请参阅
@import("std").math.mod。
另请参阅:
@mulWithOverflow §
@mulWithOverflow(a: anytype, b: anytype) struct { @TypeOf(a, b), u1 }
执行
a * b
并返回一个包含结果和可能的溢出位的元组。
@panic §
@panic(message: []const u8) noreturn
调用 panic 处理函数。默认情况下,panic 处理函数
调用在根源文件中公开的 panic 函数,或者
如果没有指定,则调用 std/builtin.zig 中的
std.builtin.default_panic
函数。
通常使用
@import("std").debug.panic
更好。 但是,@panic
在以下 2 种场景中很有用:
- 从库代码中,如果程序员在根源文件中公开了 panic 函数,则调用该函数。
- 在混合 C 和 Zig 代码时,跨多个 .o 文件调用规范的 panic 实现。
另请参阅:
@popCount §
@popCount(operand: anytype) anytype
@TypeOf(operand)
必须是一个整数类型。
计算整数中设置的位数 - "种群计数"。
返回类型是无符号整数或无符号整数向量,其位数是可以表示该整数类型的位计数的最小位数。
另请参阅:
@prefetch §
@prefetch(ptr: anytype, comptime options: PrefetchOptions) void
这个内建函数告诉编译器在目标 CPU 支持的情况下发出预取指令。如果目标 CPU 不支持请求的预取指令,这个内建函数是一个空操作。这个函数对程序的行为没有影响,只影响性能特性。
ptr
参数可以是任何指针类型,并确定要预取的内存地址。这个函数不会解引用指针,向这个函数传递一个指向无效内存的指针是完全合法的,不会导致非法行为。
PrefetchOptions 可以通过
@import("std").builtin.PrefetchOptions
找到。
@ptrCast §
@ptrCast(value: anytype) anytype
将一种类型的指针转换为另一种类型的指针。返回类型是推断的结果类型。
允许使用可选指针。将 null 的可选指针转换为非可选指针会调用安全检查的非法行为。
@ptrCast
不能用于:
-
移除
const限定符,使用 @constCast。 -
移除
volatile限定符,使用 @volatileCast。 - 更改指针地址空间,使用 @addrSpaceCast。
- 增加指针对齐,使用 @alignCast。
-
将非切片指针转换为切片,使用切片语法
ptr[start..end]。
@ptrFromInt §
@ptrFromInt(address: usize) anytype
将整数转换为指针。返回类型是推断的结果类型。 要进行反向转换,使用
@intFromPtr。将地址 0
转换为非可选且没有
allowzero
属性的目标类型时,在启用运行时安全检查时将导致指针转换无效空值恐慌。
如果目标指针类型不允许地址零且
address 为零,这会调用安全检查的非法行为。
@rem §
@rem(numerator: T, denominator: T) T
取余除法。对于无符号整数,这与
numerator % denominator 相同。调用者保证
denominator !=
0,否则在启用运行时安全检查时,该操作将导致取余除以零。
-
@rem(-5, 3) == -2 -
(@divTrunc(a, b) * b) + @rem(a, b) == a
对于返回错误代码的函数,参见
@import("std").math.rem。
另请参阅:
@returnAddress §
@returnAddress() usize
此函数返回当前函数返回时将执行的下一条机器代码指令的地址。
这样做的含义是特定于目标的,并且在所有平台上并不一致。
此函数仅在函数作用域内有效。如果函数被内联到调用函数中,返回的地址将应用于调用函数。
@select §
@select(comptime T: type, pred: @Vector(len, bool), a: @Vector(len, T), b: @Vector(len, T)) @Vector(len, T)
根据 pred 从 a 或
b 按元素选择值。如果
pred[i] 为
true,则结果中的相应元素将是 a[i],否则为
b[i]。
另请参阅:
@setEvalBranchQuota §
@setEvalBranchQuota(comptime new_quota: u32) void
增加编译时代码执行在放弃并产生编译错误之前可以使用的最大向后分支数。
如果 new_quota 小于默认配额(1000)或先前明确设置的配额,它将被忽略。
示例:
test "foo" {
comptime {
var i = 0;
while (i < 1001) : (i += 1) {}
}
}
$ zig test test_without_setEvalBranchQuota_builtin.zig /home/andy/dev/zig/doc/langref/test_without_setEvalBranchQuota_builtin.zig:4:9: error: evaluation exceeded 1000 backwards branches while (i < 1001) : (i += 1) {} ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /home/andy/dev/zig/doc/langref/test_without_setEvalBranchQuota_builtin.zig:4:9: note: use @setEvalBranchQuota() to raise the branch limit from 1000
现在我们使用
@setEvalBranchQuota:
test "foo" {
comptime {
@setEvalBranchQuota(1001);
var i = 0;
while (i < 1001) : (i += 1) {}
}
}
$ zig test test_setEvalBranchQuota_builtin.zig 1/1 test_setEvalBranchQuota_builtin.test.foo...OK All 1 tests passed.
另请参阅:
@setFloatMode §
@setFloatMode(comptime mode: FloatMode) void
更改当前作用域关于如何定义浮点运算的规则。
-
Strict(默认) - 浮点运算遵循严格的 IEEE 合规性。 -
Optimized- 浮点运算可能执行以下所有操作:- 假设参数和结果不是 NaN。优化需要保留对 NaN 的合法行为,但结果的值是未定义的。
- 假设参数和结果不是 +/-Inf。优化需要保留对 +/-Inf 的合法行为,但结果的值是未定义的。
- 将零参数或结果的符号视为无关紧要。
- 使用参数的倒数而不是执行除法。
- 执行浮点收缩(例如,将乘法后跟加法融合为融合乘加)。
- 执行代数等效变换,这些变换可能会改变浮点结果(例如,重新关联)。
-ffast-math。
浮点模式由子作用域继承,并且可以在任何作用域中覆盖。您可以通过使用 comptime 块在结构体或模块作用域中设置浮点模式。
FloatMode 可以通过
@import("std").builtin.FloatMode
找到。
另请参阅:
@setRuntimeSafety §
@setRuntimeSafety(comptime safety_on: bool) void
设置包含函数调用的作用域是否启用运行时安全检查。
test "@setRuntimeSafety" {
// 内建函数应用于调用它的作用域。所以在这里,在 ReleaseFast 和 ReleaseSmall 模式下不会捕获整数溢出:
// var x: u8 = 255;
// x += 1; // ReleaseFast/ReleaseSmall 模式下的未检查非法行为。
{
// 然而这个块启用了安全,所以即使在 ReleaseFast 和 ReleaseSmall 模式下,这里也会进行安全检查。
@setRuntimeSafety(true);
var x: u8 = 255;
x += 1;
{
// 该值可以在任何作用域中覆盖。所以在这里,在任何构建模式下都不会捕获整数溢出。
@setRuntimeSafety(false);
// var x: u8 = 255;
// x += 1; // 所有构建模式下的未检查非法行为。
}
}
}
$ zig test test_setRuntimeSafety_builtin.zig -OReleaseFast 1/1 test_setRuntimeSafety_builtin.test.@setRuntimeSafety...thread 2902624 panic: integer overflow /home/andy/dev/zig/doc/langref/test_setRuntimeSafety_builtin.zig:11:11: 0x103dc78 in test.@setRuntimeSafety (test) x += 1; ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:218:25: 0x10312bf in main (test) if (test_fn.func()) |_| { ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x102ee5d in posixCallMainAndExit (test) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x102e95d in _start (test) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) error: the following test command crashed: /home/andy/dev/zig/.zig-cache/o/7c580cf55e0b1cb6bb40fde0c61723ab/test --seed=0x2879e8a6
注意:计划用 @optimizeFor 替换
@setRuntimeSafety
@shlExact §
@shlExact(value: T, shift_amt: Log2T) T
执行左移操作(<<)。
对于无符号整数,如果任何 1 位被移出,则结果是未定义的。对于有符号整数,如果任何与结果符号位不一致的位被移出,则结果是未定义的。
shift_amt 的类型是具有
log2(@typeInfo(T).int.bits)
位的无符号整数。 这是因为
shift_amt >=
@typeInfo(T).int.bits
会触发安全检查的非法行为。
comptime_int
被建模为具有无限位数的整数, 这意味着在这种情况下,@shlExact
总是产生结果,不会产生编译错误。
另请参阅:
@shlWithOverflow §
@shlWithOverflow(a: anytype, shift_amt: Log2T) struct { @TypeOf(a), u1 }
执行
a << b
并返回一个包含结果和可能的溢出位的元组。
shift_amt 的类型是具有
log2(@typeInfo(@TypeOf(a)).int.bits)
位的无符号整数。 这是因为
shift_amt >=
@typeInfo(@TypeOf(a)).int.bits
会触发安全检查的非法行为。
另请参阅:
@shrExact §
@shrExact(value: T, shift_amt: Log2T) T
执行右移操作(>>)。调用者保证移位不会移出任何
1 位。
shift_amt 的类型是具有
log2(@typeInfo(T).int.bits)
位的无符号整数。 这是因为
shift_amt >=
@typeInfo(T).int.bits
会触发安全检查的非法行为。
另请参阅:
@shuffle §
@shuffle(comptime E: type, a: @Vector(a_len, E), b: @Vector(b_len, E), comptime mask: @Vector(mask_len, i32)) @Vector(mask_len, E)
通过根据 mask 从 a 和
b 选择元素来构造一个新的向量。
mask 中的每个元素从 a 或
b 中选择一个元素。正数从
a 开始选择,从 0 开始。负值从
b 选择,从
-1
开始并递减。建议对来自 b 的索引使用
~ 运算符,以便两个索引都可以从
0 开始(即
~@as(i32, 0)
是 -1)。
对于 mask 的每个元素,如果它或从
a 或 b 选择的值是
undefined,则结果元素是
undefined。
a_len 和
b_len 的长度可能不同。mask
中的越界元素索引会导致编译错误。
如果 a 或 b 是
undefined,它相当于一个所有元素都是
undefined
且长度与另一个向量相同的向量。如果两个向量都是
undefined,@shuffle
返回一个所有元素都是
undefined
的向量。
E 必须是整数、浮点数、指针或
bool。掩码可以是任何向量长度,其长度决定结果长度。
const std = @import("std");
const expect = std.testing.expect;
test "vector @shuffle" {
const a = @Vector(7, u8){ 'o', 'l', 'h', 'e', 'r', 'z', 'w' };
const b = @Vector(4, u8){ 'w', 'd', '!', 'x' };
// 要在单个向量内进行混洗,将 undefined 作为第二个参数传递。
// 注意,我们可以重新排序、复制或省略输入向量的元素
const mask1 = @Vector(5, i32){ 2, 3, 1, 1, 0 };
const res1: @Vector(5, u8) = @shuffle(u8, a, undefined, mask1);
try expect(std.mem.eql(u8, &@as([5]u8, res1), "hello"));
// 组合两个向量
const mask2 = @Vector(6, i32){ -1, 0, 4, 1, -2, -3 };
const res2: @Vector(6, u8) = @shuffle(u8, a, b, mask2);
try expect(std.mem.eql(u8, &@as([6]u8, res2), "world!"));
}
$ zig test test_shuffle_builtin.zig 1/1 test_shuffle_builtin.test.vector @shuffle...OK All 1 tests passed.
另请参阅:
@sizeOf §
@sizeOf(comptime T: type) comptime_int
此函数返回在内存中存储 T 所需的字节数。
结果是特定于目标的编译时常量。
这个大小可能包含填充字节。如果内存中有两个连续的
T,填充将是索引 0 处的元素和索引 1
处的元素之间的字节偏移量。对于整数,请考虑您是想使用
@sizeOf(T)
还是
@typeInfo(T).int.bits。
此函数测量运行时的大小。对于运行时不允许的类型,例如
comptime_int
和 type,结果是 0。
另请参阅:
@splat §
@splat(scalar: anytype) anytype
生成一个数组或向量,其中每个元素都是值
scalar。返回类型以及向量的长度是推断的。
const std = @import("std");
const expect = std.testing.expect;
test "vector @splat" {
const scalar: u32 = 5;
const result: @Vector(4, u32) = @splat(scalar);
try expect(std.mem.eql(u32, &@as([4]u32, result), &[_]u32{ 5, 5, 5, 5 }));
}
test "array @splat" {
const scalar: u32 = 5;
const result: [4]u32 = @splat(scalar);
try expect(std.mem.eql(u32, &@as([4]u32, result), &[_]u32{ 5, 5, 5, 5 }));
}
$ zig test test_splat_builtin.zig 1/2 test_splat_builtin.test.vector @splat...OK 2/2 test_splat_builtin.test.array @splat...OK All 2 tests passed.
另请参阅:
@reduce §
@reduce(comptime op: std.builtin.ReduceOp, value: anytype) E
通过使用指定的运算符
op 对其元素执行顺序水平归约,将向量转换为标量值(类型为 E)。
并非每个运算符都可用于每种向量元素类型:
请注意,整数类型上的 .Add 和
.Mul
归约是回绕的;当应用于浮点类型时,除非浮点模式设置为
Optimized,否则保留操作结合性。
const std = @import("std");
const expect = std.testing.expect;
test "vector @reduce" {
const V = @Vector(4, i32);
const value = V{ 1, -1, 1, -1 };
const result = value > @as(V, @splat(0));
// result 是 { true, false, true, false };
try comptime expect(@TypeOf(result) == @Vector(4, bool));
const is_all_true = @reduce(.And, result);
try comptime expect(@TypeOf(is_all_true) == bool);
try expect(is_all_true == false);
}
$ zig test test_reduce_builtin.zig 1/1 test_reduce_builtin.test.vector @reduce...OK All 1 tests passed.
另请参阅:
@src §
@src() std.builtin.SourceLocation
返回一个
SourceLocation
结构体,表示函数的名称及其在源代码中的位置。必须在函数中调用此函数。
const std = @import("std");
const expect = std.testing.expect;
test "@src" {
try doTheTest();
}
fn doTheTest() !void {
const src = @src();
try expect(src.line == 9);
try expect(src.column == 17);
try expect(std.mem.endsWith(u8, src.fn_name, "doTheTest"));
try expect(std.mem.endsWith(u8, src.file, "test_src_builtin.zig"));
}
$ zig test test_src_builtin.zig 1/1 test_src_builtin.test.@src...OK All 1 tests passed.
@sqrt §
@sqrt(value: anytype) @TypeOf(value)
执行浮点数的平方根。在可用时使用专用硬件指令。
@sin §
@sin(value: anytype) @TypeOf(value)
对弧度制浮点数进行正弦三角函数运算。在可用时使用专用硬件指令。
@cos §
@cos(value: anytype) @TypeOf(value)
对弧度制浮点数进行余弦三角函数运算。在可用时使用专用硬件指令。
@tan §
@tan(value: anytype) @TypeOf(value)
对弧度制浮点数进行正切三角函数运算。 在可用时使用专用硬件指令。
@exp §
@exp(value: anytype) @TypeOf(value)
对浮点数进行以 e 为底的指数函数运算。在可用时使用专用硬件指令。
@exp2 §
@exp2(value: anytype) @TypeOf(value)
对浮点数进行以 2 为底的指数函数运算。在可用时使用专用硬件指令。
@log §
@log(value: anytype) @TypeOf(value)
返回浮点数的自然对数。在可用时使用专用硬件指令。
@log2 §
@log2(value: anytype) @TypeOf(value)
返回浮点数以 2 为底的对数。在可用时使用专用硬件指令。
@log10 §
@log10(value: anytype) @TypeOf(value)
返回浮点数以 10 为底的对数。在可用时使用专用硬件指令。
@abs §
@abs(value: anytype) anytype
返回整数或浮点数的绝对值。在可用时使用专用硬件指令。 如果操作数是整数,返回类型始终是与操作数位宽相同的无符号整数。 支持无符号整数操作数。对于有符号整数操作数,内建函数不会溢出。
@floor §
@floor(value: anytype) @TypeOf(value)
返回不大于给定浮点数的最大整数值。 在可用时使用专用硬件指令。
@ceil §
@ceil(value: anytype) @TypeOf(value)
返回不小于给定浮点数的最小整数值。 在可用时使用专用硬件指令。
@trunc §
@trunc(value: anytype) @TypeOf(value)
将给定的浮点数向零舍入为整数。 在可用时使用专用硬件指令。
@round §
@round(value: anytype) @TypeOf(value)
将给定的浮点数舍入到最接近的整数。如果两个整数同样接近,则远离零舍入。 在可用时使用专用硬件指令。
const expect = @import("std").testing.expect;
test "@round" {
try expect(@round(1.4) == 1);
try expect(@round(1.5) == 2);
try expect(@round(-1.4) == -1);
try expect(@round(-2.5) == -3);
}
$ zig test test_round_builtin.zig 1/1 test_round_builtin.test.@round...OK All 1 tests passed.
@subWithOverflow §
@subWithOverflow(a: anytype, b: anytype) struct { @TypeOf(a, b), u1 }
执行
a - b
并返回一个包含结果和可能的溢出位的元组。
@tagName §
@tagName(value: anytype) [:0]const u8
将枚举值或联合值转换为表示名称的字符串字面量。
如果枚举是非穷尽的并且标签值未映射到名称,它会调用安全检查的非法行为。
@This §
@This() type
返回此函数调用所在的最内层结构体、枚举或联合。 这对于需要引用自身的匿名结构体很有用:
const std = @import("std");
const expect = std.testing.expect;
test "@This()" {
var items = [_]i32{ 1, 2, 3, 4 };
const list = List(i32){ .items = items[0..] };
try expect(list.length() == 4);
}
fn List(comptime T: type) type {
return struct {
const Self = @This();
items: []T,
fn length(self: Self) usize {
return self.items.len;
}
};
}
$ zig test test_this_builtin.zig 1/1 test_this_builtin.test.@This()...OK All 1 tests passed.
当
@This()
在文件作用域使用时,它返回对应于当前文件的结构体的引用。
@trap §
@trap() noreturn
此函数插入一个特定于平台的陷阱/卡住指令,可用于异常退出程序。
这可能通过显式发出无效指令来实现,这可能会导致某种非法指令异常。
与
@breakpoint()
不同,执行不会在此点之后继续。
在函数作用域之外,此内建函数会导致编译错误。
另请参阅:
@truncate §
@truncate(integer: anytype) anytype
此函数从整数类型截断位,生成一个更小或相同大小的整数类型。返回类型是推断的结果类型。
此函数始终截断整数的有效位,无论目标平台上的字节序如何。
对超出目标类型范围的数字调用
@truncate
是定义明确的工作代码:
const std = @import("std");
const expect = std.testing.expect;
test "integer truncation" {
const a: u16 = 0xabcd;
const b: u8 = @truncate(a);
try expect(b == 0xcd);
}
$ zig test test_truncate_builtin.zig 1/1 test_truncate_builtin.test.integer truncation...OK All 1 tests passed.
使用 @intCast 转换保证适合目标类型的数字。
@Type §
@Type(comptime info: std.builtin.Type) type
此函数是
@typeInfo
的逆函数。它将类型信息具体化为
type。
它适用于以下类型:
-
type -
noreturn -
void -
bool -
整数 - 整数类型的最大位数是
65535。 - 浮点数
- 指针
-
comptime_int -
comptime_float -
@TypeOf(undefined) -
@TypeOf(null) - 数组
- 可选类型
- 错误集类型
- 错误联合类型
- 向量
- opaque
-
anyframe - struct
- enum
- 枚举字面量
- union
- 函数
@typeInfo §
@typeInfo(comptime T: type) std.builtin.Type
提供类型反射。
结构体、联合、枚举和错误集的类型信息具有字段,这些字段保证与源文件中出现的顺序相同。
结构体、联合、枚举和不透明类型的类型信息具有声明,这些声明也保证与源文件中出现的顺序相同。
@typeName §
@typeName(T: type) *const [N:0]u8
此函数以数组形式返回类型的字符串表示形式。它相当于类型名称的字符串字面量。 返回的类型名称是完全限定的,父命名空间作为类型名称的一部分包含在内,使用一系列点。
@TypeOf §
@TypeOf(...) type
@TypeOf
是一个特殊的内建函数,它接受任意数量(非零)的表达式作为参数,并使用对等类型解析返回结果的类型。
表达式会被求值,但保证它们没有运行时副作用:
const std = @import("std");
const expect = std.testing.expect;
test "no runtime side effects" {
var data: i32 = 0;
const T = @TypeOf(foo(i32, &data));
try comptime expect(T == i32);
try expect(data == 0);
}
fn foo(comptime T: type, ptr: *T) T {
ptr.* += 1;
return ptr.*;
}
$ zig test test_TypeOf_builtin.zig 1/1 test_TypeOf_builtin.test.no runtime side effects...OK All 1 tests passed.
@unionInit §
@unionInit(comptime Union: type, comptime active_field_name: []const u8, init_expr) Union
这与联合初始化语法相同,只是字段名称是编译期已知的值,而不是标识符标记。
@unionInit
将其结果位置转发给 init_expr。
@Vector §
@Vector(len: comptime_int, Element: type) type
创建向量。
@volatileCast §
@volatileCast(value: anytype) DestType
从指针中移除
volatile
限定符。
@workGroupId §
@workGroupId(comptime dimension: u32) u32
返回当前内核调用中维度
dimension 的工作组索引。
@workGroupSize §
@workGroupSize(comptime dimension: u32) u32
返回工作组在维度
dimension 中具有的工作项数。
@workItemId §
@workItemId(comptime dimension: u32) u32
返回工作项在工作组中维度
dimension 的索引。此函数返回介于
0(包含)和
@workGroupSize(dimension)(不包含)之间的值。
构建模式 §
Zig 有四种构建模式:
要向
build.zig
文件添加标准构建选项:
const std = @import("std");
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "example",
.root_module = b.createModule(.{
.root_source_file = b.path("example.zig"),
.optimize = optimize,
}),
});
b.default_step.dependOn(&exe.step);
}
这会使这些选项可用:
- -Doptimize=Debug
- 优化关闭,安全检查开启(默认)
- -Doptimize=ReleaseSafe
- 优化开启,安全检查开启
- -Doptimize=ReleaseFast
- 优化开启,安全检查关闭
- -Doptimize=ReleaseSmall
- 大小优化开启,安全检查关闭
Debug §
$ zig build-exe example.zig
- 快速编译速度
- 启用安全检查
- 慢速运行时性能
- 大的二进制大小
- 无可重现构建要求
ReleaseFast §
$ zig build-exe example.zig -O ReleaseFast
- 快速运行时性能
- 禁用安全检查
- 慢速编译速度
- 大的二进制大小
- 可重现构建
ReleaseSafe §
$ zig build-exe example.zig -O ReleaseSafe
- 中等运行时性能
- 启用安全检查
- 慢速编译速度
- 大的二进制大小
- 可重现构建
ReleaseSmall §
$ zig build-exe example.zig -O ReleaseSmall
- 中等运行时性能
- 禁用安全检查
- 慢速编译速度
- 小的二进制大小
- 可重现构建
另请参阅:
单线程构建 §
Zig 有一个编译选项 -fsingle-threaded,它有以下效果:
- 所有线程局部变量都被视为常规的容器级变量。
- 异步函数的开销变得等同于函数调用开销。
-
@import("builtin").single_threaded变为true,因此读取此变量的各种用户态 API 变得更加高效。例如std.Mutex变为空数据结构,其所有函数都变为空操作。
非法行为 §
Zig 中的许多操作都会触发所谓的"非法行为"(IB)。如果在编译时检测到非法行为,Zig 会发出编译错误并拒绝继续。否则,当非法行为在编译时未被捕获时,它会属于两类中的一类。
一些非法行为是安全检查的:这意味着编译器会在可能在运行时发生非法行为的任何地方插入"安全检查",以确定它是否即将发生。如果是,安全检查"失败",从而触发恐慌。
所有其他非法行为都是未检查的,这意味着编译器无法为其插入安全检查。如果在运行时调用未检查的非法行为,可能会发生任何事情:通常是某种形式的崩溃,但优化器可以自由地使未检查的非法行为执行任何操作,例如调用任意函数或破坏任意数据。这类似于某些其他语言中"未定义行为"的概念。请注意,未检查的非法行为如果在 comptime 时求值,仍然总是导致编译错误,因为 Zig 编译器能够在编译时执行比在运行时更复杂的检查。
大多数非法行为都是安全检查的。但是,为了便于优化,默认情况下在 ReleaseFast 和 ReleaseSmall 优化模式下禁用安全检查。也可以使用 @setRuntimeSafety 在每个块的基础上启用或禁用安全检查,从而覆盖当前优化模式的默认值。当安全检查被禁用时,安全检查的非法行为表现得像未检查的非法行为;也就是说,调用它可能导致任何行为。
当安全检查失败时,Zig 的默认恐慌处理程序会崩溃并显示堆栈跟踪,如下所示:
test "安全检查" {
unreachable;
}
$ zig test test_illegal_behavior.zig 1/1 test_illegal_behavior.test.安全检查...thread 2892891 panic: 到达了不可达代码 /home/andy/dev/zig/doc/langref/test_illegal_behavior.zig:2:5: 0x102c00c in test.安全检查 (test_illegal_behavior.zig) unreachable; ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:218:25: 0x115cb20 in mainTerminal (test_runner.zig) if (test_fn.func()) |_| { ^ /home/andy/dev/zig/lib/compiler/test_runner.zig:66:28: 0x1155d41 in main (test_runner.zig) return mainTerminal(); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x114fadd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x114f371 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) error: 以下测试命令崩溃: /home/andy/dev/zig/.zig-cache/o/e72b27fd3a681a218f2215fb6e7fd433/test --seed=0xeebe2201
到达不可达代码 §
在编译时:
comptime {
assert(false);
}
fn assert(ok: bool) void {
if (!ok) unreachable; // 断言失败
}
$ zig test test_comptime_reaching_unreachable.zig /home/andy/dev/zig/doc/langref/test_comptime_reaching_unreachable.zig:5:14: error: 到达了不可达代码 if (!ok) unreachable; // 断言失败 ^~~~~~~~~~~ /home/andy/dev/zig/doc/langref/test_comptime_reaching_unreachable.zig:2:11: note: 在编译时调用 assert(false); ~~~~~~^~~~~~~
在运行时:
const std = @import("std");
pub fn main() void {
std.debug.assert(false);
}
$ zig build-exe runtime_reaching_unreachable.zig $ ./runtime_reaching_unreachable thread 2897013 panic: 到达了不可达代码 /home/andy/dev/zig/lib/std/debug.zig:559:14: 0x1044179 in assert (std.zig) if (!ok) unreachable; // 断言失败 ^ /home/andy/dev/zig/doc/langref/runtime_reaching_unreachable.zig:4:21: 0x113e86e in main (runtime_reaching_unreachable.zig) std.debug.assert(false); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
索引越界 §
在编译时:
comptime {
const array: [5]u8 = "hello".*;
const garbage = array[5];
_ = garbage;
}
$ zig test test_comptime_index_out_of_bounds.zig /home/andy/dev/zig/doc/langref/test_comptime_index_out_of_bounds.zig:3:27: error: 索引 5 超出长度为 5 的数组范围 const garbage = array[5]; ^
在运行时:
pub fn main() void {
const x = foo("hello");
_ = x;
}
fn foo(x: []const u8) u8 {
return x[5];
}
$ zig build-exe runtime_index_out_of_bounds.zig $ ./runtime_index_out_of_bounds thread 2893998 panic: 索引越界: 索引 5, 长度 5 /home/andy/dev/zig/doc/langref/runtime_index_out_of_bounds.zig:7:13: 0x113fae6 in foo (runtime_index_out_of_bounds.zig) return x[5]; ^ /home/andy/dev/zig/doc/langref/runtime_index_out_of_bounds.zig:2:18: 0x113e87a in main (runtime_index_out_of_bounds.zig) const x = foo("hello"); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
将负数转换为无符号整数 §
在编译时:
comptime {
const value: i32 = -1;
const unsigned: u32 = @intCast(value);
_ = unsigned;
}
$ zig test test_comptime_invalid_cast.zig /home/andy/dev/zig/doc/langref/test_comptime_invalid_cast.zig:3:36: error: 类型 'u32' 无法表示整数值 '-1' const unsigned: u32 = @intCast(value); ^~~~~
在运行时:
const std = @import("std");
pub fn main() void {
var value: i32 = -1; // 运行时已知
_ = &value;
const unsigned: u32 = @intCast(value);
std.debug.print("value: {}\n", .{unsigned});
}
$ zig build-exe runtime_invalid_cast.zig $ ./runtime_invalid_cast thread 2899906 panic: 整数无法适配目标类型 /home/andy/dev/zig/doc/langref/runtime_invalid_cast.zig:6:27: 0x113e87f in main (runtime_invalid_cast.zig) const unsigned: u32 = @intCast(value); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
要获取无符号整数的最大值,请使用
std.math.maxInt。
转换截断数据 §
在编译时:
comptime {
const spartan_count: u16 = 300;
const byte: u8 = @intCast(spartan_count);
_ = byte;
}
$ zig test test_comptime_invalid_cast_truncate.zig /home/andy/dev/zig/doc/langref/test_comptime_invalid_cast_truncate.zig:3:31: error: 类型 'u8' 无法表示整数值 '300' const byte: u8 = @intCast(spartan_count); ^~~~~~~~~~~~~
在运行时:
const std = @import("std");
pub fn main() void {
var spartan_count: u16 = 300; // 运行时已知
_ = &spartan_count;
const byte: u8 = @intCast(spartan_count);
std.debug.print("value: {}\n", .{byte});
}
$ zig build-exe runtime_invalid_cast_truncate.zig $ ./runtime_invalid_cast_truncate thread 2899317 panic: 整数无法适配目标类型 /home/andy/dev/zig/doc/langref/runtime_invalid_cast_truncate.zig:6:22: 0x113e880 in main (runtime_invalid_cast_truncate.zig) const byte: u8 = @intCast(spartan_count); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
要截断位,请使用 @truncate。
整数溢出 §
默认操作 §
以下操作可能导致整数溢出:
编译时加法溢出示例:
comptime {
var byte: u8 = 255;
byte += 1;
}
$ zig test test_comptime_overflow.zig /home/andy/dev/zig/doc/langref/test_comptime_overflow.zig:3:10: error: 类型 'u8' 的整数溢出,值为 '256' byte += 1; ~~~~~^~~~
在运行时:
const std = @import("std");
pub fn main() void {
var byte: u8 = 255;
byte += 1;
std.debug.print("value: {}\n", .{byte});
}
$ zig build-exe runtime_overflow.zig $ ./runtime_overflow thread 2892886 panic: 整数溢出 /home/andy/dev/zig/doc/langref/runtime_overflow.zig:5:10: 0x113e895 in main (runtime_overflow.zig) byte += 1; ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
标准库数学函数 §
标准库提供的这些函数返回可能的错误。
-
@import("std").math.add -
@import("std").math.sub -
@import("std").math.mul -
@import("std").math.divTrunc -
@import("std").math.divFloor -
@import("std").math.divExact -
@import("std").math.shl
捕获加法溢出的示例:
const math = @import("std").math;
const print = @import("std").debug.print;
pub fn main() !void {
var byte: u8 = 255;
byte = if (math.add(u8, byte, 1)) |result| result else |err| {
print("无法加一: {s}\n", .{@errorName(err)});
return err;
};
print("结果: {}\n", .{byte});
}
$ zig build-exe math_add.zig $ ./math_add 无法加一: Overflow error: Overflow /home/andy/dev/zig/lib/std/math.zig:570:21: 0x113ebae in add__anon_22552 (std.zig) if (ov[1] != 0) return error.Overflow; ^ /home/andy/dev/zig/doc/langref/math_add.zig:8:9: 0x113d422 in main (math_add.zig) return err; ^
内建溢出函数 §
这些内建函数返回一个元组,包含是否发生溢出(作为
u1)以及操作可能溢出的位:
@addWithOverflow 示例:
const print = @import("std").debug.print;
pub fn main() void {
const byte: u8 = 255;
const ov = @addWithOverflow(byte, 10);
if (ov[1] != 0) {
print("溢出结果: {}\n", .{ov[0]});
} else {
print("结果: {}\n", .{ov[0]});
}
}
$ zig build-exe addWithOverflow_builtin.zig $ ./addWithOverflow_builtin 溢出结果: 9
环绕操作 §
这些操作具有保证的环绕语义。
+%(环绕加法)-%(环绕减法)-%(环绕取负)*%(环绕乘法)
const std = @import("std");
const expect = std.testing.expect;
const minInt = std.math.minInt;
const maxInt = std.math.maxInt;
test "环绕加法和减法" {
const x: i32 = maxInt(i32);
const min_val = x +% 1;
try expect(min_val == minInt(i32));
const max_val = min_val -% 1;
try expect(max_val == maxInt(i32));
}
$ zig test test_wraparound_semantics.zig 1/1 test_wraparound_semantics.test.环绕加法和减法...OK 所有 1 个测试通过。
精确左移溢出 §
在编译时:
comptime {
const x = @shlExact(@as(u8, 0b01010101), 2);
_ = x;
}
$ zig test test_comptime_shlExact_overflow.zig /home/andy/dev/zig/doc/langref/test_comptime_shlExact_overflow.zig:2:15: error: 类型 'u8' 的整数溢出,值为 '340' const x = @shlExact(@as(u8, 0b01010101), 2); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
在运行时:
const std = @import("std");
pub fn main() void {
var x: u8 = 0b01010101; // 运行时已知
_ = &x;
const y = @shlExact(x, 2);
std.debug.print("value: {}\n", .{y});
}
$ zig build-exe runtime_shlExact_overflow.zig $ ./runtime_shlExact_overflow thread 2896313 panic: 左移溢出位 /home/andy/dev/zig/doc/langref/runtime_shlExact_overflow.zig:6:5: 0x113e8a1 in main (runtime_shlExact_overflow.zig) const y = @shlExact(x, 2); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
精确右移溢出 §
在编译时:
comptime {
const x = @shrExact(@as(u8, 0b10101010), 2);
_ = x;
}
$ zig test test_comptime_shrExact_overflow.zig /home/andy/dev/zig/doc/langref/test_comptime_shrExact_overflow.zig:2:15: error: 精确移位移出了 1 位 const x = @shrExact(@as(u8, 0b10101010), 2); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
在运行时:
const builtin = @import("builtin");
const std = @import("std");
pub fn main() void {
var x: u8 = 0b10101010; // 运行时已知
_ = &x;
const y = @shrExact(x, 2);
std.debug.print("value: {}\n", .{y});
if (builtin.cpu.arch.isRISCV() and builtin.zig_backend == .stage2_llvm) @panic("https://github.com/ziglang/zig/issues/24304");
}
$ zig build-exe runtime_shrExact_overflow.zig $ ./runtime_shrExact_overflow thread 2897712 panic: 右移溢出位 /home/andy/dev/zig/doc/langref/runtime_shrExact_overflow.zig:7:5: 0x113e88a in main (runtime_shrExact_overflow.zig) const y = @shrExact(x, 2); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
除以零 §
在编译时:
comptime {
const a: i32 = 1;
const b: i32 = 0;
const c = a / b;
_ = c;
}
$ zig test test_comptime_division_by_zero.zig /home/andy/dev/zig/doc/langref/test_comptime_division_by_zero.zig:4:19: error: 此处除以零导致非法行为 const c = a / b; ^
在运行时:
const std = @import("std");
pub fn main() void {
var a: u32 = 1;
var b: u32 = 0;
_ = .{ &a, &b };
const c = a / b;
std.debug.print("value: {}\n", .{c});
}
$ zig build-exe runtime_division_by_zero.zig $ ./runtime_division_by_zero thread 2902461 panic: 除以零 /home/andy/dev/zig/doc/langref/runtime_division_by_zero.zig:7:17: 0x113e890 in main (runtime_division_by_zero.zig) const c = a / b; ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
取余除以零 §
在编译时:
comptime {
const a: i32 = 10;
const b: i32 = 0;
const c = a % b;
_ = c;
}
$ zig test test_comptime_remainder_division_by_zero.zig /home/andy/dev/zig/doc/langref/test_comptime_remainder_division_by_zero.zig:4:19: error: 此处除以零导致非法行为 const c = a % b; ^
在运行时:
const std = @import("std");
pub fn main() void {
var a: u32 = 10;
var b: u32 = 0;
_ = .{ &a, &b };
const c = a % b;
std.debug.print("value: {}\n", .{c});
}
$ zig build-exe runtime_remainder_division_by_zero.zig $ ./runtime_remainder_division_by_zero thread 2899727 panic: 除以零 /home/andy/dev/zig/doc/langref/runtime_remainder_division_by_zero.zig:7:17: 0x113e890 in main (runtime_remainder_division_by_zero.zig) const c = a % b; ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
精确除法余数 §
在编译时:
comptime {
const a: u32 = 10;
const b: u32 = 3;
const c = @divExact(a, b);
_ = c;
}
$ zig test test_comptime_divExact_remainder.zig /home/andy/dev/zig/doc/langref/test_comptime_divExact_remainder.zig:4:15: error: 精确除法产生了余数 const c = @divExact(a, b); ^~~~~~~~~~~~~~~
在运行时:
const std = @import("std");
pub fn main() void {
var a: u32 = 10;
var b: u32 = 3;
_ = .{ &a, &b };
const c = @divExact(a, b);
std.debug.print("value: {}\n", .{c});
}
$ zig build-exe runtime_divExact_remainder.zig $ ./runtime_divExact_remainder thread 2901529 panic: 精确除法产生了余数 /home/andy/dev/zig/doc/langref/runtime_divExact_remainder.zig:7:15: 0x113e8c7 in main (runtime_divExact_remainder.zig) const c = @divExact(a, b); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
尝试解包空值 §
在编译时:
comptime {
const optional_number: ?i32 = null;
const number = optional_number.?;
_ = number;
}
$ zig test test_comptime_unwrap_null.zig /home/andy/dev/zig/doc/langref/test_comptime_unwrap_null.zig:3:35: error: 无法解包空值 const number = optional_number.?; ~~~~~~~~~~~~~~~^~
在运行时:
const std = @import("std");
pub fn main() void {
var optional_number: ?i32 = null;
_ = &optional_number;
const number = optional_number.?;
std.debug.print("value: {}\n", .{number});
}
$ zig build-exe runtime_unwrap_null.zig $ ./runtime_unwrap_null thread 2892887 panic: 尝试使用空值 /home/andy/dev/zig/doc/langref/runtime_unwrap_null.zig:6:35: 0x113e8b4 in main (runtime_unwrap_null.zig) const number = optional_number.?; ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
避免这种崩溃的一种方法是测试空值而不是假设非空,使用
if 表达式:
const print = @import("std").debug.print;
pub fn main() void {
const optional_number: ?i32 = null;
if (optional_number) |number| {
print("得到数字: {}\n", .{number});
} else {
print("是空值\n", .{});
}
}
$ zig build-exe testing_null_with_if.zig $ ./testing_null_with_if 是空值
另请参阅:
尝试解包错误 §
在编译时:
comptime {
const number = getNumberOrFail() catch unreachable;
_ = number;
}
fn getNumberOrFail() !i32 {
return error.UnableToReturnNumber;
}
$ zig test test_comptime_unwrap_error.zig /home/andy/dev/zig/doc/langref/test_comptime_unwrap_error.zig:2:44: error: 捕获到意外错误 'UnableToReturnNumber' const number = getNumberOrFail() catch unreachable; ^~~~~~~~~~~ /home/andy/dev/zig/doc/langref/test_comptime_unwrap_error.zig:7:18: note: 错误在此返回 return error.UnableToReturnNumber; ^~~~~~~~~~~~~~~~~~~~
在运行时:
const std = @import("std");
pub fn main() void {
const number = getNumberOrFail() catch unreachable;
std.debug.print("value: {}\n", .{number});
}
fn getNumberOrFail() !i32 {
return error.UnableToReturnNumber;
}
$ zig build-exe runtime_unwrap_error.zig $ ./runtime_unwrap_error thread 2895126 panic: 尝试解包错误: UnableToReturnNumber /home/andy/dev/zig/doc/langref/runtime_unwrap_error.zig:9:5: 0x113e86c in getNumberOrFail (runtime_unwrap_error.zig) return error.UnableToReturnNumber; ^ /home/andy/dev/zig/doc/langref/runtime_unwrap_error.zig:4:44: 0x113e8d3 in main (runtime_unwrap_error.zig) const number = getNumberOrFail() catch unreachable; ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
避免这种崩溃的一种方法是测试错误而不是假设成功结果,使用
if 表达式:
const print = @import("std").debug.print;
pub fn main() void {
const result = getNumberOrFail();
if (result) |number| {
print("得到数字: {}\n", .{number});
} else |err| {
print("得到错误: {s}\n", .{@errorName(err)});
}
}
fn getNumberOrFail() !i32 {
return error.UnableToReturnNumber;
}
$ zig build-exe testing_error_with_if.zig $ ./testing_error_with_if 得到错误: UnableToReturnNumber
另请参阅:
无效的错误代码 §
在编译时:
comptime {
const err = error.AnError;
const number = @intFromError(err) + 10;
const invalid_err = @errorFromInt(number);
_ = invalid_err;
}
$ zig test test_comptime_invalid_error_code.zig /home/andy/dev/zig/doc/langref/test_comptime_invalid_error_code.zig:4:39: error: 整数值 '11' 不代表任何错误 const invalid_err = @errorFromInt(number); ^~~~~~
在运行时:
const std = @import("std");
pub fn main() void {
const err = error.AnError;
var number = @intFromError(err) + 500;
_ = &number;
const invalid_err = @errorFromInt(number);
std.debug.print("value: {}\n", .{invalid_err});
}
$ zig build-exe runtime_invalid_error_code.zig $ ./runtime_invalid_error_code thread 2900570 panic: 无效的错误代码 /home/andy/dev/zig/doc/langref/runtime_invalid_error_code.zig:7:5: 0x113e8a7 in main (runtime_invalid_error_code.zig) const invalid_err = @errorFromInt(number); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
无效的枚举转换 §
在编译时:
const Foo = enum {
a,
b,
c,
};
comptime {
const a: u2 = 3;
const b: Foo = @enumFromInt(a);
_ = b;
}
$ zig test test_comptime_invalid_enum_cast.zig /home/andy/dev/zig/doc/langref/test_comptime_invalid_enum_cast.zig:8:20: error: 枚举 'test_comptime_invalid_enum_cast.Foo' 没有值为 '3' 的标签 const b: Foo = @enumFromInt(a); ^~~~~~~~~~~~~~~ /home/andy/dev/zig/doc/langref/test_comptime_invalid_enum_cast.zig:1:13: note: 枚举在此声明 const Foo = enum { ^~~~
在运行时:
const std = @import("std");
const Foo = enum {
a,
b,
c,
};
pub fn main() void {
var a: u2 = 3;
_ = &a;
const b: Foo = @enumFromInt(a);
std.debug.print("value: {s}\n", .{@tagName(b)});
}
$ zig build-exe runtime_invalid_enum_cast.zig $ ./runtime_invalid_enum_cast thread 2902395 panic: 无效的枚举值 /home/andy/dev/zig/doc/langref/runtime_invalid_enum_cast.zig:12:20: 0x113e8f0 in main (runtime_invalid_enum_cast.zig) const b: Foo = @enumFromInt(a); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
无效的错误集转换 §
在编译时:
const Set1 = error{
A,
B,
};
const Set2 = error{
A,
C,
};
comptime {
_ = @as(Set2, @errorCast(Set1.B));
}
$ zig test test_comptime_invalid_error_set_cast.zig /home/andy/dev/zig/doc/langref/test_comptime_invalid_error_set_cast.zig:10:19: error: 'error.B' 不是错误集 'error{A,C}' 的成员 _ = @as(Set2, @errorCast(Set1.B)); ^~~~~~~~~~~~~~~~~~
在运行时:
const std = @import("std");
const Set1 = error{
A,
B,
};
const Set2 = error{
A,
C,
};
pub fn main() void {
foo(Set1.B);
}
fn foo(set1: Set1) void {
const x: Set2 = @errorCast(set1);
std.debug.print("value: {}\n", .{x});
}
$ zig build-exe runtime_invalid_error_set_cast.zig $ ./runtime_invalid_error_set_cast thread 2900078 panic: 无效的错误代码 /home/andy/dev/zig/doc/langref/runtime_invalid_error_set_cast.zig:15:21: 0x113fb3c in foo (runtime_invalid_error_set_cast.zig) const x: Set2 = @errorCast(set1); ^ /home/andy/dev/zig/doc/langref/runtime_invalid_error_set_cast.zig:12:8: 0x113e877 in main (runtime_invalid_error_set_cast.zig) foo(Set1.B); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
不正确的指针对齐 §
在编译时:
comptime {
const ptr: *align(1) i32 = @ptrFromInt(0x1);
const aligned: *align(4) i32 = @alignCast(ptr);
_ = aligned;
}
$ zig test test_comptime_incorrect_pointer_alignment.zig /home/andy/dev/zig/doc/langref/test_comptime_incorrect_pointer_alignment.zig:3:47: error: 指针地址 0x1 未对齐到 4 字节 const aligned: *align(4) i32 = @alignCast(ptr); ^~~
在运行时:
const mem = @import("std").mem;
pub fn main() !void {
var array align(4) = [_]u32{ 0x11111111, 0x11111111 };
const bytes = mem.sliceAsBytes(array[0..]);
if (foo(bytes) != 0x11111111) return error.Wrong;
}
fn foo(bytes: []u8) u32 {
const slice4 = bytes[1..5];
const int_slice = mem.bytesAsSlice(u32, @as([]align(4) u8, @alignCast(slice4)));
return int_slice[0];
}
$ zig build-exe runtime_incorrect_pointer_alignment.zig $ ./runtime_incorrect_pointer_alignment thread 2897041 panic: 不正确的对齐 /home/andy/dev/zig/doc/langref/runtime_incorrect_pointer_alignment.zig:9:64: 0x113ec08 in foo (runtime_incorrect_pointer_alignment.zig) const int_slice = mem.bytesAsSlice(u32, @as([]align(4) u8, @alignCast(slice4))); ^ /home/andy/dev/zig/doc/langref/runtime_incorrect_pointer_alignment.zig:5:12: 0x113d3f2 in main (runtime_incorrect_pointer_alignment.zig) if (foo(bytes) != 0x11111111) return error.Wrong; ^ /home/andy/dev/zig/lib/std/start.zig:627:37: 0x113dbc9 in posixCallMainAndExit (std.zig) const result = root.main() catch |err| { ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
错误的联合体字段访问 §
在编译时:
comptime {
var f = Foo{ .int = 42 };
f.float = 12.34;
}
const Foo = union {
float: f32,
int: u32,
};
$ zig test test_comptime_wrong_union_field_access.zig /home/andy/dev/zig/doc/langref/test_comptime_wrong_union_field_access.zig:3:6: error: 字段 'int' 处于活动状态时访问联合体字段 'float' f.float = 12.34; ~^~~~~~ /home/andy/dev/zig/doc/langref/test_comptime_wrong_union_field_access.zig:6:13: note: 联合体在此声明 const Foo = union { ^~~~~
在运行时:
const std = @import("std");
const Foo = union {
float: f32,
int: u32,
};
pub fn main() void {
var f = Foo{ .int = 42 };
bar(&f);
}
fn bar(f: *Foo) void {
f.float = 12.34;
std.debug.print("value: {}\n", .{f.float});
}
$ zig build-exe runtime_wrong_union_field_access.zig $ ./runtime_wrong_union_field_access thread 2901950 panic: 字段 'int' 处于活动状态时访问联合体字段 'float' /home/andy/dev/zig/doc/langref/runtime_wrong_union_field_access.zig:14:6: 0x113fb1e in bar (runtime_wrong_union_field_access.zig) f.float = 12.34; ^ /home/andy/dev/zig/doc/langref/runtime_wrong_union_field_access.zig:10:8: 0x113e89f in main (runtime_wrong_union_field_access.zig) bar(&f); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
此安全性不适用于
extern 或
packed 联合体。
要更改联合体的活动字段,请像这样赋值整个联合体:
const std = @import("std");
const Foo = union {
float: f32,
int: u32,
};
pub fn main() void {
var f = Foo{ .int = 42 };
bar(&f);
}
fn bar(f: *Foo) void {
f.* = Foo{ .float = 12.34 };
std.debug.print("value: {}\n", .{f.float});
}
$ zig build-exe change_active_union_field.zig $ ./change_active_union_field value: 12.34
当字段的有意义的值未知时,要更改联合体的活动字段,请使用 undefined,像这样:
const std = @import("std");
const Foo = union {
float: f32,
int: u32,
};
pub fn main() void {
var f = Foo{ .int = 42 };
f = Foo{ .float = undefined };
bar(&f);
std.debug.print("value: {}\n", .{f.float});
}
fn bar(f: *Foo) void {
f.float = 12.34;
}
$ zig build-exe undefined_active_union_field.zig $ ./undefined_active_union_field value: 12.34
另请参阅:
浮点数转整数越界 §
当浮点数转换为整数时,如果浮点数的值超出整数类型的范围,就会发生这种情况。
在编译时:
comptime {
const float: f32 = 4294967296;
const int: i32 = @intFromFloat(float);
_ = int;
}
$ zig test test_comptime_out_of_bounds_float_to_integer_cast.zig /home/andy/dev/zig/doc/langref/test_comptime_out_of_bounds_float_to_integer_cast.zig:3:36: error: 浮点值 '4294967296' 无法存储在整数类型 'i32' 中 const int: i32 = @intFromFloat(float); ^~~~~
在运行时:
pub fn main() void {
var float: f32 = 4294967296; // 运行时已知
_ = &float;
const int: i32 = @intFromFloat(float);
_ = int;
}
$ zig build-exe runtime_out_of_bounds_float_to_integer_cast.zig $ ./runtime_out_of_bounds_float_to_integer_cast thread 2898584 panic: 浮点值的整数部分超出范围 /home/andy/dev/zig/doc/langref/runtime_out_of_bounds_float_to_integer_cast.zig:4:22: 0x113e8d2 in main (runtime_out_of_bounds_float_to_integer_cast.zig) const int: i32 = @intFromFloat(float); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (进程被信号终止)
指针转换无效空值 §
当将地址为 0 的指针转换为不允许地址为 0 的指针时会发生这种情况。 例如,C 指针、可选指针和 allowzero 指针 允许地址为零,但普通的指针不允许。
在编译期:
comptime {
const opt_ptr: ?*i32 = null;
const ptr: *i32 = @ptrCast(opt_ptr);
_ = ptr;
}
$ zig test test_comptime_invalid_null_pointer_cast.zig /home/andy/dev/zig/doc/langref/test_comptime_invalid_null_pointer_cast.zig:3:32: error: null pointer casted to type '*i32' const ptr: *i32 = @ptrCast(opt_ptr); ^~~~~~~
在运行时:
pub fn main() void {
var opt_ptr: ?*i32 = null;
_ = &opt_ptr;
const ptr: *i32 = @ptrCast(opt_ptr);
_ = ptr;
}
$ zig build-exe runtime_invalid_null_pointer_cast.zig $ ./runtime_invalid_null_pointer_cast thread 2892939 panic: cast causes pointer to be null /home/andy/dev/zig/doc/langref/runtime_invalid_null_pointer_cast.zig:4:23: 0x113e88a in main (runtime_invalid_null_pointer_cast.zig) const ptr: *i32 = @ptrCast(opt_ptr); ^ /home/andy/dev/zig/lib/std/start.zig:618:22: 0x113dabd in posixCallMainAndExit (std.zig) root.main(); ^ /home/andy/dev/zig/lib/std/start.zig:232:5: 0x113d351 in _start (std.zig) asm volatile (switch (native_arch) { ^ ???:?:?: 0x0 in ??? (???) (process terminated by signal)
内存 §
Zig 语言不代表程序员执行任何内存管理。这就是为什么 Zig 没有运行时,以及为什么 Zig 代码可以在如此多的环境中无缝工作,包括实时软件、操作系统内核、嵌入式设备和低延迟服务器。因此,Zig 程序员必须始终能够回答这个问题:
与 Zig 一样,C 编程语言具有手动内存管理。但是,与 Zig
不同,C 有一个默认分配器 - malloc、realloc
和 free。 当链接 libc 时,Zig 通过
std.heap.c_allocator 公开此分配器。
但是,按照惯例,Zig
中没有默认分配器。相反,需要分配内存的函数接受一个
Allocator
参数。同样,一些数据结构在其初始化函数中接受
Allocator 参数:
const std = @import("std");
const Allocator = std.mem.Allocator;
const expect = std.testing.expect;
test "using an allocator" {
var buffer: [100]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
const result = try concat(allocator, "foo", "bar");
try expect(std.mem.eql(u8, "foobar", result));
}
fn concat(allocator: Allocator, a: []const u8, b: []const u8) ![]u8 {
const result = try allocator.alloc(u8, a.len + b.len);
@memcpy(result[0..a.len], a);
@memcpy(result[a.len..], b);
return result;
}
$ zig test test_allocator.zig 1/1 test_allocator.test.using an allocator...OK All 1 tests passed.
在上面的示例中,100 字节的栈内存用于初始化
FixedBufferAllocator,然后将其传递给函数。
为了方便起见,在
std.testing.allocator 有一个全局
FixedBufferAllocator
可用于快速测试,它还将执行基本的泄漏检测。
Zig 有一个通用的分配器可以通过
std.heap.GeneralPurposeAllocator
导入。但是,仍然建议遵循选择分配器指南。
选择分配器 §
使用什么分配器取决于许多因素。这里有一个流程图可以帮助你决定:
-
你在创建一个库吗?在这种情况下,最好接受一个
Allocator作为参数,并允许库的用户决定使用什么分配器。 -
你在链接 libc 吗?在这种情况下,
std.heap.c_allocator可能是正确的选择,至少对于你的主分配器来说是这样。 -
你需要的最大字节数是否由在编译期已知的数字限定?在这种情况下,使用
std.heap.FixedBufferAllocator。 -
你的程序是一个命令行应用程序,它从头到尾运行,没有任何基本的循环模式(例如视频游戏主循环或
Web
服务器请求处理程序),以至于在最后一次性释放所有内容是有意义的吗?
在这种情况下,建议遵循此模式:
cli_allocation.zig const std = @import("std"); pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const ptr = try allocator.create(i32); std.debug.print("ptr={*}\n", .{ptr}); } 当使用这种分配器时,不需要手动释放任何东西。一切都通过调用Shell $ zig build-exe cli_allocation.zig $ ./cli_allocation ptr=i32@7f1a3ed8e010
arena.deinit()一次性释放。 -
分配是否是循环模式的一部分,例如视频游戏主循环或 Web
服务器请求处理程序?如果可以在循环结束时一次性释放所有分配,例如一旦视频游戏帧已完全渲染,或者
Web 服务器请求已得到服务,那么
std.heap.ArenaAllocator是一个很好的候选者。如前面的项目符号所示,这允许你一次释放整个 arena。 还要注意,如果可以建立内存的上限,那么std.heap.FixedBufferAllocator可以用作进一步的优化。 -
你在编写测试,并且想要确保正确处理
error.OutOfMemory?在这种情况下,使用std.testing.FailingAllocator。 -
你在编写测试吗?在这种情况下,使用
std.testing.allocator。 -
最后,如果上述情况都不适用,你需要一个通用的分配器。
如果你处于 Debug 模式,
std.heap.DebugAllocator可用作一个函数,该函数接受一个编译期 结构体的配置选项并返回一个类型。 通常,你将在主函数中恰好设置一个,然后将其或子分配器传递给应用程序的各个部分。 -
如果你在 ReleaseFast 模式下编译,
std.heap.smp_allocator是通用分配器的可靠选择。 - 你也可以考虑实现一个分配器。
字节在哪里? §
诸如
"hello"
之类的字符串字面量位于全局常量数据段中。
这就是为什么将字符串字面量传递给可变切片会出错,如下所示:
fn foo(s: []u8) void {
_ = s;
}
test "string literal to mutable slice" {
foo("hello");
}
$ zig test test_string_literal_to_slice.zig /home/andy/dev/zig/doc/langref/test_string_literal_to_slice.zig:6:9: error: expected type '[]u8', found '*const [5:0]u8' foo("hello"); ^~~~~~~ /home/andy/dev/zig/doc/langref/test_string_literal_to_slice.zig:6:9: note: cast discards const qualifier /home/andy/dev/zig/doc/langref/test_string_literal_to_slice.zig:1:11: note: parameter type declared here fn foo(s: []u8) void { ^~~~
但是,如果你将切片设为常量,那么它就可以工作:
fn foo(s: []const u8) void {
_ = s;
}
test "string literal to constant slice" {
foo("hello");
}
$ zig test test_string_literal_to_const_slice.zig 1/1 test_string_literal_to_const_slice.test.string literal to constant slice...OK All 1 tests passed.
就像字符串字面量一样,当值在编译期已知时,const
声明存储在全局常量数据段中。此外,编译期变量也存储在全局常量数据段中。
函数内的
var
声明存储在函数的栈帧中。一旦函数返回,任何指向函数栈帧中变量的指针都会成为无效引用,取消引用它们会成为未经检查的非法行为。
顶层或结构体声明中的
var
声明存储在全局数据段中。
使用 allocator.alloc 或
allocator.create
分配的内存的位置由分配器的实现确定。
TODO: 线程局部变量
堆分配失败 §
许多编程语言选择通过无条件崩溃来处理堆分配失败的可能性。按照惯例,Zig
程序员不认为这是一个令人满意的解决方案。相反,error.OutOfMemory
表示堆分配失败,并且当堆分配失败阻止操作成功完成时,Zig
库会返回此错误代码。
有些人认为,由于某些操作系统(如 Linux)默认启用了内存过度提交,因此处理堆分配失败是毫无意义的。这种推理有许多问题:
-
只有某些操作系统具有过度提交功能。
- Linux 默认启用它,但它是可配置的。
- Windows 不会过度提交。
- 嵌入式系统没有过度提交。
- 业余操作系统可能有或没有过度提交。
- 对于实时系统,不仅没有过度提交,而且通常每个应用程序的最大内存量是提前确定的。
- 在编写库时,主要目标之一是代码重用。通过使代码正确处理分配失败,库就有资格在更多环境中重用。
- 尽管某些软件已经开始依赖启用过度提交,但它的存在是无数用户体验灾难的根源。当启用过度提交的系统(如默认设置的 Linux)接近内存耗尽时,系统会锁定并变得无法使用。此时,OOM Killer 根据启发式算法选择要杀死的应用程序。这种非确定性决定通常会导致重要进程被杀死,并且通常无法使系统恢复到工作状态。
递归 §
递归是建模软件的基本工具。但是,它有一个经常被忽视的问题:无限内存分配。
递归是 Zig 中活跃实验的领域,因此这里的文档并不是最终的。 你可以在 0.3.0 发布说明中阅读递归状态摘要。
简短的摘要是,目前递归可以像你期望的那样正常工作。尽管 Zig 代码尚未受到栈溢出的保护,但计划在 Zig 的未来版本中提供此类保护,需要 Zig 代码进行一定程度的配合。
生命周期和所有权 §
确保在指向的内存不再可用时不访问指针是 Zig 程序员的责任。注意,切片是指针的一种形式,因为它引用其他内存。
为了防止错误,在处理指针时遵循一些有用的约定。通常,当函数返回指针时,函数的文档应该解释谁"拥有"该指针。这个概念帮助程序员决定何时释放指针(如果有的话)是合适的。
例如,函数的文档可能会说"调用者拥有返回的内存",在这种情况下,调用函数的代码必须有一个计划来决定何时释放该内存。在这种情况下,函数可能会接受一个
Allocator 参数。
有时指针的生命周期可能更复杂。例如,std.ArrayList(T).items
切片的生命周期在列表下次调整大小之前保持有效,例如通过追加新元素。
函数和数据结构的 API 文档应该非常注意解释指针的所有权和生命周期语义。所有权决定谁有责任释放指针引用的内存,生命周期决定内存变得不可访问的时间点(以免发生非法行为)。
编译变量 §
通过导入
"builtin"
包可以访问编译变量,编译器使该包对每个 Zig
源文件都可用。它包含编译时常量,例如当前目标、字节序和发布模式。
const builtin = @import("builtin");
const separator = if (builtin.os.tag == .windows) '\\' else '/';
使用
@import("builtin")
导入的内容示例:
const std = @import("std");
/// Zig 版本。在编写支持多个 Zig 版本的代码时,更倾向于
/// 功能检测(即使用 `@hasDecl` 或 `@hasField`)而不是版本检查。
pub const zig_version = std.SemanticVersion.parse(zig_version_string) catch unreachable;
pub const zig_version_string = "0.15.2";
pub const zig_backend = std.builtin.CompilerBackend.stage2_x86_64;
pub const output_mode: std.builtin.OutputMode = .Exe;
pub const link_mode: std.builtin.LinkMode = .static;
pub const unwind_tables: std.builtin.UnwindTables = .async;
pub const is_test = false;
pub const single_threaded = false;
pub const abi: std.Target.Abi = .gnu;
pub const cpu: std.Target.Cpu = .{
.arch = .x86_64,
.model = &std.Target.x86.cpu.znver4,
.features = std.Target.x86.featureSet(&.{
.@"64bit",
.adx,
.aes,
.allow_light_256_bit,
.avx,
.avx2,
.avx512bf16,
.avx512bitalg,
.avx512bw,
.avx512cd,
.avx512dq,
.avx512f,
.avx512ifma,
.avx512vbmi,
.avx512vbmi2,
.avx512vl,
.avx512vnni,
.avx512vpopcntdq,
.bmi,
.bmi2,
.branchfusion,
.clflushopt,
.clwb,
.clzero,
.cmov,
.crc32,
.cx16,
.cx8,
.evex512,
.f16c,
.fast_15bytenop,
.fast_bextr,
.fast_dpwssd,
.fast_imm16,
.fast_lzcnt,
.fast_movbe,
.fast_scalar_fsqrt,
.fast_scalar_shift_masks,
.fast_variable_perlane_shuffle,
.fast_vector_fsqrt,
.fma,
.fsgsbase,
.fsrm,
.fxsr,
.gfni,
.idivq_to_divl,
.invpcid,
.lzcnt,
.macrofusion,
.mmx,
.movbe,
.mwaitx,
.nopl,
.pclmul,
.pku,
.popcnt,
.prfchw,
.rdpid,
.rdpru,
.rdrnd,
.rdseed,
.sahf,
.sbb_dep_breaking,
.sha,
.shstk,
.slow_shld,
.smap,
.smep,
.sse,
.sse2,
.sse3,
.sse4_1,
.sse4_2,
.sse4a,
.ssse3,
.vaes,
.vpclmulqdq,
.vzeroupper,
.wbnoinvd,
.x87,
.xsave,
.xsavec,
.xsaveopt,
.xsaves,
}),
};
pub const os: std.Target.Os = .{
.tag = .linux,
.version_range = .{ .linux = .{
.range = .{
.min = .{
.major = 6,
.minor = 16,
.patch = 0,
},
.max = .{
.major = 6,
.minor = 16,
.patch = 0,
},
},
.glibc = .{
.major = 2,
.minor = 39,
.patch = 0,
},
.android = 29,
}},
};
pub const target: std.Target = .{
.cpu = cpu,
.os = os,
.abi = abi,
.ofmt = object_format,
.dynamic_linker = .init("/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib/ld-linux-x86-64.so.2"),
};
pub const object_format: std.Target.ObjectFormat = .elf;
pub const mode: std.builtin.OptimizeMode = .Debug;
pub const link_libc = false;
pub const link_libcpp = false;
pub const have_error_return_tracing = true;
pub const valgrind_support = true;
pub const sanitize_thread = false;
pub const fuzz = false;
pub const position_independent_code = false;
pub const position_independent_executable = false;
pub const strip_debug_info = false;
pub const code_model: std.builtin.CodeModel = .default;
pub const omit_frame_pointer = false;
另请参阅:
编译模型 §
Zig 编译被分为模块。每个模块是 Zig
源文件的集合,其中之一是模块的根源文件。每个模块可以依赖任意数量的其他模块,形成一个有向图(模块之间允许依赖循环)。如果模块
A 依赖于模块 B,那么模块 A 中的任何 Zig 源文件都可以使用
@import
和模块名称导入模块 B
的根源文件。本质上,模块充当导入 Zig
源文件(可能存在于文件系统的完全不同部分)的别名。
使用 zig build-exe 编译的简单 Zig
程序有两个关键模块:包含你代码的模块,称为"main"或"root"模块,以及标准库。你的模块在名称"std"下依赖于标准库模块,这就是允许你编写
@import("std")
的原因!事实上,Zig 编译中的每个模块 — 包括标准库本身 —
都隐式依赖于名称"std"下的标准库模块。
"根模块"(在
zig build-exe
示例中由你提供的模块)具有一个特殊属性。与标准库一样,它被隐式地提供给所有模块(包括它自己),这次使用名称"root"。因此,@import("root")
将始终等同于
@import
你的"main"源文件(通常但不一定命名为
main.zig)。
源文件结构体 §
每个 Zig 源文件都隐式地是一个
struct
声明;你可以想象文件的内容实际上被
struct { ... }
包围。这意味着除了声明之外,文件的顶层还允许包含字段:
//! 因为此文件包含字段,它是一个旨在被实例化的类型,因此
//! 按照惯例使用 TitleCase 命名,而不是 snake_case。
foo: u32,
bar: u64,
/// `@This()` 可用于引用此结构体类型。在有字段的文件中,通常会
/// 在这里命名类型,以便此文件中的其他声明可以轻松引用它。
const TopLevelFields = @This();
pub fn init(val: u32) TopLevelFields {
return .{
.foo = val,
.bar = val * 10,
};
}
这样的文件可以像任何其他
struct
类型一样被实例化。文件的"根结构体类型"可以在该文件中使用
@This 引用。
文件和声明发现 §
Zig 重视任何代码片段是否被语义分析的概念;本质上,编译器是否"查看"它。哪些代码被分析是基于从某个点"发现"了哪些文件和声明。这个"发现"过程基于一组简单的递归规则:
-
如果分析了对
@import的调用,则会分析被导入的文件。 -
如果分析了一个类型(包括文件),则会分析其中的所有
comptime和export声明。 -
如果分析了一个类型(包括文件),并且编译是用于测试,并且该类型所在的模块是编译的根模块,则还会分析其中的所有
test声明。 - 如果分析了对命名声明的引用(即使用它),则会分析被引用的声明。声明是顺序无关的,因此此引用可能在被引用声明的上方或下方,甚至在完全不同的文件中。
就是这样!这些规则定义了如何发现 Zig 文件和声明。剩下的就是理解这个过程从哪里开始。
答案是标准库的根:每次 Zig 编译都从分析文件
lib/std/std.zig 开始。此文件包含一个
comptime
声明,该声明导入
lib/std/start.zig,该文件又使用
@import("root")
引用"根模块";因此,你作为主模块的根源文件提供的文件实际上也是一个根,因为标准库总是会引用它。
通常需要确保某些声明 — 特别是
test 或
export 声明 —
被发现。根据上述规则,一个常见的策略是在
comptime 或
test 块中使用
@import:
comptime {
// 这将确保文件 'api.zig' 始终被发现(只要此文件被发现)。
// 如果 'api.zig' 包含重要的导出声明,这很有用。
_ = @import("api.zig");
// 我们也可以有一个文件,其中包含我们只想根据 comptime 条件导出的声明。
// 在这种情况下,我们可以在这里使用 `if` 语句:
if (builtin.os.tag == .windows) {
_ = @import("windows_api.zig");
}
}
test {
// 这将确保文件 'tests.zig' 始终被发现(只要此文件被发现),
// 如果此编译是测试。如果 'tests.zig' 包含我们想要确保运行的测试,这很有用。
_ = @import("tests.zig");
// 我们也可以有一个文件,其中包含我们只想根据 comptime 条件运行的测试。
// 在这种情况下,我们可以在这里使用 `if` 语句:
if (builtin.os.tag == .windows) {
_ = @import("windows_tests.zig");
}
}
const builtin = @import("builtin");
特殊根声明 §
因为根模块的根源文件始终可以使用
@import("root")
访问,所以它有时被库 — 包括 Zig 标准库 —
用作程序向该库公开某些"全局"信息的地方。Zig
标准库将在此文件中查找几个声明。
入口点 §
在构建可执行文件时,在此文件中查找的最重要的内容是程序的入口点。最常见的是一个名为
main 的函数,std.start
将在执行重要的初始化工作后调用它。
或者,名为 _start 的声明的存在(例如,pub
const _start = {};)将禁用默认的
std.start
逻辑,允许你的根源文件根据需要导出低级入口点。
/// `std.start` 使用 `@import("root")` 导入此文件,并将此声明用作程序的
/// 用户提供的入口点。它可以返回以下任何类型:
/// * `void`
/// * `E!void`,对于任何错误集 `E`
/// * `u8`
/// * `E!u8`,对于任何错误集 `E`
/// 从此函数返回 `void` 值将以代码 0 退出。
/// 从此函数返回 `u8` 值将以给定的状态代码退出。
/// 从此函数返回错误值将打印错误返回跟踪并以代码 1 退出。
pub fn main() void {
std.debug.print("Hello, World!\n", .{});
}
// 如果取消注释,此声明将抑制通常的 std.start 逻辑,导致
// 上面的 `main` 声明被忽略。
//pub const _start = {};
const std = @import("std");
$ zig build-exe entry_point.zig $ ./entry_point Hello, World!
如果 Zig 编译链接 libc,main
函数可以选择是一个
export
fn,它匹配 C main 函数的签名:
pub export fn main(argc: c_int, argv: [*]const [*:0]const u8) c_int {
const args = argv[0..@intCast(argc)];
std.debug.print("Hello! argv[0] is '{s}'\n", .{args[0]});
return 0;
}
const std = @import("std");
$ zig build-exe libc_export_entry_point.zig -lc $ ./libc_export_entry_point Hello! argv[0] is './libc_export_entry_point'
在某些情况下,std.start
还可能使用其他入口点声明,例如 wWinMain 或
EfiMain。有关这些声明的详细信息,请参阅
lib/std/start.zig 逻辑。
标准库选项 §
标准库还在根模块的根源文件中查找名为
std_options
的声明。如果存在,此声明应该是类型为
std.Options
的结构体,并允许程序自定义某些标准库功能,例如
std.log 实现。
/// 此声明的存在允许程序覆盖标准库的某些行为。
/// 有关可用选项的完整列表,请参阅 `std.Options` 的文档。
pub const std_options: std.Options = .{
// 默认情况下,在安全构建模式下,标准库将为程序附加一个段错误处理程序,以
// 在发生段错误时打印有用的堆栈跟踪。在这里,我们可以禁用它,甚至在不安全的构建模式下启用它。
.enable_segfault_handler = true,
// 这是 `std.log` 使用的日志函数。
.logFn = myLogFn,
};
fn myLogFn(
comptime level: std.log.Level,
comptime scope: @Type(.enum_literal),
comptime format: []const u8,
args: anytype,
) void {
// 我们可以在这里做任何我们想做的事情!
// ...但实际上,让我们只调用默认实现。
std.log.defaultLog(level, scope, format, args);
}
const std = @import("std");
恐慌处理程序 §
Zig 标准库在根模块的根源文件中查找名为
panic
的声明。如果存在,它应该是一个命名空间(容器类型),其中包含提供不同恐慌处理程序的声明。
有关此命名空间的基本实现,请参阅
std.debug.simple_panic。
覆盖恐慌处理程序实际输出消息的方式,但保留默认启用的格式化安全恐慌,可以通过
std.debug.FullPanic 轻松实现:
pub fn main() void {
@setRuntimeSafety(true);
var x: u8 = 255;
// 让我们溢出这个整数!
x += 1;
}
pub const panic = std.debug.FullPanic(myPanic);
fn myPanic(msg: []const u8, first_trace_addr: ?usize) noreturn {
_ = first_trace_addr;
std.debug.print("Panic! {s}\n", .{msg});
std.process.exit(1);
}
const std = @import("std");
$ zig build-exe panic_handler.zig $ ./panic_handler Panic! integer overflow
Zig 构建系统 §
Zig 构建系统提供了一种跨平台、无依赖的方式来声明构建项目所需的逻辑。使用此系统,构建项目的逻辑在 build.zig 文件中编写,使用 Zig 构建系统 API 来声明和配置构建工件和其他任务。
构建系统可以帮助完成的一些任务示例:
- 并行执行任务并缓存结果。
- 依赖于其他项目。
- 为其他项目提供一个包以供依赖。
- 通过执行 Zig 编译器创建构建工件。这包括构建 Zig 源代码以及 C 和 C++ 源代码。
- 捕获用户配置的选项并使用这些选项来配置构建。
- 通过提供一个可以被 Zig 代码导入的文件,将构建配置作为编译期值公开。
- 缓存构建工件以避免不必要地重复步骤。
- 执行构建工件或系统安装的工具。
- 运行测试并验证执行构建工件的输出与预期值匹配。
- 在代码库或其子集上运行
zig fmt。 - 自定义任务。
要使用构建系统,请运行 zig build --help 以查看命令行使用帮助菜单。这将包括在 build.zig 脚本中声明的项目特定选项。
目前,构建系统文档托管在外部: 构建系统文档
C §
尽管 Zig 独立于 C,并且与大多数其他语言不同,不依赖于 libc,但 Zig 承认与现有 C 代码交互的重要性。
Zig 通过几种方式促进 C 互操作。
C 类型原语 §
这些具有保证的 C ABI 兼容性,并且可以像任何其他类型一样使用。
-
c_char -
c_short -
c_ushort -
c_int -
c_uint -
c_long -
c_ulong -
c_longlong -
c_ulonglong -
c_longdouble
要与 C
void
类型互操作,请使用
anyopaque。
另请参阅:
从 C 头文件导入 §
@cImport
内置函数可用于直接从
.h 文件导入符号:
const c = @cImport({
// 参见 https://github.com/ziglang/zig/issues/515
@cDefine("_NO_CRT_STDIO_INLINE", "1");
@cInclude("stdio.h");
});
pub fn main() void {
_ = c.printf("hello\n");
}
$ zig build-exe cImport_builtin.zig -lc $ ./cImport_builtin hello
@cImport
函数将表达式作为参数。此表达式在编译时求值,用于控制预处理器指令并包含多个
.h 文件:
const builtin = @import("builtin");
const c = @cImport({
@cDefine("NDEBUG", builtin.mode == .ReleaseFast);
if (something) {
@cDefine("_GNU_SOURCE", {});
}
@cInclude("stdlib.h");
if (something) {
@cUndef("_GNU_SOURCE");
}
@cInclude("soundio.h");
});
另请参阅:
C 翻译 CLI §
Zig 的 C 翻译功能可通过 zig translate-c 作为 CLI 工具使用。它需要一个文件名作为参数。它还可能接受一组可选的标志,这些标志被转发到 clang。它将翻译后的文件写入 stdout。
命令行标志 §
- -I: 指定包含文件的搜索目录。可以多次使用。等同于 clang 的 -I 标志。默认情况下不包括当前目录;使用 -I. 包含它。
- -D: 定义预处理器宏。等同于 clang 的 -D 标志。
- -cflags [flags] --: 将任意附加的命令行标志传递给 clang。注意:标志列表必须以 -- 结束
- -target: 翻译后的 Zig 代码的目标三元组。如果未指定目标,将使用当前主机目标。
使用 -target 和 -cflags §
重要! 使用 zig translate-c 翻译 C 代码时,你必须使用与编译翻译后的代码时将使用的相同的 -target 三元组。此外,你必须确保使用的 -cflags(如果有)与目标系统上代码使用的 cflags 匹配。使用不正确的 -target 或 -cflags 可能导致 clang 或 Zig 解析失败,或在与 C 代码链接时出现细微的 ABI 不兼容性。
long FOO = __LONG_MAX__;
$ zig translate-c -target thumb-freestanding-gnueabihf varytarget.h|grep FOO pub export var FOO: c_long = 2147483647; $ zig translate-c -target x86_64-macos-gnu varytarget.h|grep FOO pub export var FOO: c_long = 9223372036854775807;
enum FOO { BAR };
int do_something(enum FOO foo);
$ zig translate-c varycflags.h|grep -B1 do_something pub const enum_FOO = c_uint; pub extern fn do_something(foo: enum_FOO) c_int; $ zig translate-c -cflags -fshort-enums -- varycflags.h|grep -B1 do_something pub const enum_FOO = u8; pub extern fn do_something(foo: enum_FOO) c_int;
@cImport vs translate-c §
@cImport
和 zig translate-c 使用相同的底层 C
翻译功能,因此在技术层面上它们是等效的。在实践中,@cImport
作为一种快速轻松地访问数字常量、typedef
和记录类型的方式很有用,无需任何额外设置。如果你需要将
cflags 传递给
clang,或者如果你想编辑翻译后的代码,建议使用
zig translate-c
并将结果保存到文件中。编辑生成的代码的常见原因包括:将函数类宏中的
anytype
参数更改为更具体的类型;将 [*c]T 指针更改为
[*]T 或
*T 指针以提高类型安全性;以及在特定函数中启用或禁用运行时安全。
另请参阅:
C 翻译缓存 §
C 翻译功能(无论是通过 zig translate-c 还是
@cImport
使用)都与 Zig 缓存系统集成。使用相同的源文件、目标和
cflags 的后续运行将使用缓存,而不是重复翻译相同的代码。
要查看在编译使用
@cImport
的代码时缓存文件的存储位置,请使用
--verbose-cimport 标志:
const c = @cImport({
@cDefine("_NO_CRT_STDIO_INLINE", "1");
@cInclude("stdio.h");
});
pub fn main() void {
_ = c;
}
$ zig build-exe verbose_cimport_flag.zig -lc --verbose-cimport info(compilation): C import source: /home/andy/dev/zig/.zig-cache/o/f9216ef6681abef94b056af4b875b0bd/cimport.h info(compilation): C import .d file: /home/andy/dev/zig/.zig-cache/o/f9216ef6681abef94b056af4b875b0bd/cimport.h.d $ ./verbose_cimport_flag
cimport.h
包含要翻译的文件(由对
@cInclude、@cDefine
和
@cUndef
的调用构造),cimport.h.d
是文件依赖项列表,cimport.zig
包含翻译后的输出。
另请参阅:
翻译失败 §
某些 C 构造无法翻译为 Zig - 例如,goto、带位域的结构体和标记粘贴宏。Zig 使用降级来允许在遇到不可翻译的实体时继续翻译。
降级有三种变体 - opaque、extern
和
@compileError。 无法正确翻译的 C 结构体和联合体将被翻译为
opaque{}。
包含不透明类型或无法翻译的代码构造的函数将被降级为
extern 声明。
因此,不可翻译的类型仍然可以用作指针,并且只要链接器知道编译后的函数,就可以调用不可翻译的函数。
当顶级定义(全局变量、函数原型、宏)无法翻译或降级时,使用
@compileError。由于 Zig
对顶级声明使用惰性分析,因此不可翻译的实体不会导致代码中的编译错误,除非你实际使用它们。
另请参阅:
C 宏 §
C 翻译尽最大努力尝试将类函数宏翻译为等效的 Zig
函数。由于 C 宏在词法标记级别运行,因此并非所有 C
宏都可以翻译为 Zig。无法翻译的宏将被降级为
@compileError。注意,使用宏的 C
代码将被翻译而不会出现任何额外的问题(因为 Zig
对宏展开后的预处理源进行操作)。仅仅是宏本身可能无法翻译为
Zig。
考虑以下示例:
#define MAKELOCAL(NAME, INIT) int NAME = INIT
int foo(void) {
MAKELOCAL(a, 1);
MAKELOCAL(b, 2);
return a + b;
}
$ zig translate-c macro.c > macro.zig
pub export fn foo() c_int {
var a: c_int = 1;
_ = &a;
var b: c_int = 2;
_ = &b;
return a + b;
}
pub const MAKELOCAL = @compileError("unable to translate C expr: unexpected token .Equal"); // macro.c:1:9
注意,尽管使用了不可翻译的宏,foo
仍被正确翻译。MAKELOCAL 被降级为
@compileError,因为它不能表示为 Zig 函数;这仅仅意味着你不能直接从 Zig
使用 MAKELOCAL。
另请参阅:
C 指针 §
应尽可能避免使用此类型。使用 C 指针的唯一有效理由是在从翻译 C 代码生成的自动生成代码中。
在导入 C
头文件时,指针应该翻译为单项指针(*T)还是多项指针([*]T)是不明确的。
C 指针是一种折衷方案,以便 Zig
代码可以直接利用翻译后的头文件。
[*c]T - C 指针。
-
支持其他两种指针类型(
*T)和([*]T)的所有语法。 - 强制转换为其他指针类型,以及可选指针。 当 C 指针强制转换为非可选指针时,如果地址为 0,则会发生安全检查的非法行为。
-
允许地址 0。在非独立目标上,取消引用地址 0
是安全检查的非法行为。可选 C 指针引入另一个位来跟踪 null,就像
?usize一样。注意,创建可选 C 指针是不必要的,因为可以使用普通的可选指针。 - 支持与整数之间的类型强制转换。
- 支持与整数的比较。
- 不支持仅限 Zig 的指针属性,例如对齐。请使用普通指针!
当 C 指针指向单个结构体(不是数组)时,取消引用 C 指针以访问结构体的字段或成员数据。该语法如下所示:
ptr_to_struct.*.struct_member
这相当于在 C 中执行 ->。
当 C 指针指向结构体数组时,语法恢复为:
ptr_to_struct_array[index].struct_member
C 可变参数函数 §
Zig 支持 extern 可变参数函数。
const std = @import("std");
const testing = std.testing;
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;
test "variadic function" {
try testing.expect(printf("Hello, world!\n") == 14);
try testing.expect(@typeInfo(@TypeOf(printf)).@"fn".is_var_args);
}
$ zig test test_variadic_function.zig -lc 1/1 test_variadic_function.test.variadic function...OK All 1 tests passed. Hello, world!
可以使用 @cVaStart、@cVaEnd、@cVaArg 和 @cVaCopy 来实现可变参数函数。
const std = @import("std");
const testing = std.testing;
const builtin = @import("builtin");
fn add(count: c_int, ...) callconv(.c) c_int {
var ap = @cVaStart();
defer @cVaEnd(&ap);
var i: usize = 0;
var sum: c_int = 0;
while (i < count) : (i += 1) {
sum += @cVaArg(&ap, c_int);
}
return sum;
}
test "defining a variadic function" {
if (builtin.cpu.arch == .aarch64 and builtin.os.tag != .macos) {
// https://github.com/ziglang/zig/issues/14096
return error.SkipZigTest;
}
if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows) {
// https://github.com/ziglang/zig/issues/16961
return error.SkipZigTest;
}
try std.testing.expectEqual(@as(c_int, 0), add(0));
try std.testing.expectEqual(@as(c_int, 1), add(1, @as(c_int, 1)));
try std.testing.expectEqual(@as(c_int, 3), add(2, @as(c_int, 1), @as(c_int, 2)));
}
$ zig test test_defining_variadic_function.zig 1/1 test_defining_variadic_function.test.defining a variadic function...OK All 1 tests passed.
导出 C 库 §
Zig 的主要用例之一是导出一个具有 C ABI
的库供其他编程语言调用。在函数、变量和类型前面使用
export
关键字会使它们成为库 API 的一部分:
export fn add(a: i32, b: i32) i32 {
return a + b;
}
制作静态库:
$ zig build-lib mathtest.zig
制作动态库:
$ zig build-lib mathtest.zig -dynamic
这是一个使用 Zig 构建系统的示例:
// This header is generated by zig from mathtest.zig
#include "mathtest.h"
#include <stdio.h>
int main(int argc, char **argv) {
int32_t result = add(42, 1337);
printf("%d\n", result);
return 0;
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const lib = b.addLibrary(.{
.linkage = .dynamic,
.name = "mathtest",
.root_module = b.createModule(.{
.root_source_file = b.path("mathtest.zig"),
}),
.version = .{ .major = 1, .minor = 0, .patch = 0 },
});
const exe = b.addExecutable(.{
.name = "test",
.root_module = b.createModule(.{
.link_libc = true,
}),
});
exe.root_module.addCSourceFile(.{ .file = b.path("test.c"), .flags = &.{"-std=c99"} });
exe.root_module.linkLibrary(lib);
b.default_step.dependOn(&exe.step);
const run_cmd = exe.run();
const test_step = b.step("test", "Test the program");
test_step.dependOn(&run_cmd.step);
}
$ zig build test 1379
另见:
混合目标文件 §
您可以将 Zig 目标文件与任何其他遵循 C ABI 的目标文件混合。示例:
const base64 = @import("std").base64;
export fn decode_base_64(
dest_ptr: [*]u8,
dest_len: usize,
source_ptr: [*]const u8,
source_len: usize,
) usize {
const src = source_ptr[0..source_len];
const dest = dest_ptr[0..dest_len];
const base64_decoder = base64.standard.Decoder;
const decoded_size = base64_decoder.calcSizeForSlice(src) catch unreachable;
base64_decoder.decode(dest[0..decoded_size], src) catch unreachable;
return decoded_size;
}
// This header is generated by zig from base64.zig
#include "base64.h"
#include <string.h>
#include <stdio.h>
int main(int argc, char **argv) {
const char *encoded = "YWxsIHlvdXIgYmFzZSBhcmUgYmVsb25nIHRvIHVz";
char buf[200];
size_t len = decode_base_64(buf, 200, encoded, strlen(encoded));
buf[len] = 0;
puts(buf);
return 0;
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const obj = b.addObject(.{
.name = "base64",
.root_module = b.createModule(.{
.root_source_file = b.path("base64.zig"),
}),
});
const exe = b.addExecutable(.{
.name = "test",
.root_module = b.createModule(.{
.link_libc = true,
}),
});
exe.root_module.addCSourceFile(.{ .file = b.path("test.c"), .flags = &.{"-std=c99"} });
exe.root_module.addObject(obj);
b.installArtifact(exe);
}
$ zig build $ ./zig-out/bin/test all your base are belong to us
另见:
WebAssembly §
Zig 开箱即支持构建 WebAssembly。
独立环境 §
对于像 web 浏览器和 nodejs 这样的宿主环境,使用独立操作系统目标构建可执行文件。这是一个使用 nodejs 运行编译为 WebAssembly 的 Zig 代码的示例。
extern fn print(i32) void;
export fn add(a: i32, b: i32) void {
print(a + b);
}
$ zig build-exe math.zig -target wasm32-freestanding -fno-entry --export=add
const fs = require('fs');
const source = fs.readFileSync("./math.wasm");
const typedArray = new Uint8Array(source);
WebAssembly.instantiate(typedArray, {
env: {
print: (result) => { console.log(`The result is ${result}`); }
}}).then(result => {
const add = result.instance.exports.add;
add(1, 2);
});
$ node test.js The result is 3
WASI §
Zig 对 WebAssembly 系统接口 (WASI) 的支持正在积极开发中。 使用标准库和读取命令行参数的示例:
const std = @import("std");
pub fn main() !void {
var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init;
const gpa = general_purpose_allocator.allocator();
const args = try std.process.argsAlloc(gpa);
defer std.process.argsFree(gpa, args);
for (args, 0..) |arg, i| {
std.debug.print("{}: {s}\n", .{ i, arg });
}
}
$ zig build-exe wasi_args.zig -target wasm32-wasi
$ wasmtime wasi_args.wasm 123 hello 0: wasi_args.wasm 1: 123 2: hello
一个更有趣的示例是从运行时提取预打开列表。 这现在通过
std.fs.wasi.Preopens 在标准库中得到支持:
const std = @import("std");
const fs = std.fs;
pub fn main() !void {
var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init;
const gpa = general_purpose_allocator.allocator();
var arena_instance = std.heap.ArenaAllocator.init(gpa);
defer arena_instance.deinit();
const arena = arena_instance.allocator();
const preopens = try fs.wasi.preopensAlloc(arena);
for (preopens.names, 0..) |preopen, i| {
std.debug.print("{}: {s}\n", .{ i, preopen });
}
}
$ zig build-exe wasi_preopens.zig -target wasm32-wasi
$ wasmtime --dir=. wasi_preopens.wasm 0: stdin 1: stdout 2: stderr 3: .
目标 §
目标指的是将用于运行可执行文件的计算机。它由 CPU 架构、启用的 CPU 特性集、操作系统、最小和最大操作系统版本、ABI 和 ABI 版本组成。
Zig
是一种通用编程语言,这意味着它被设计为针对大量目标生成最优代码。命令
zig targets
提供有关编译器所知的所有目标的信息。
当没有向编译器提供目标选项时,默认选择是以宿主计算机为目标,这意味着生成的可执行文件不适合复制到另一台计算机。为了将可执行文件复制到另一台计算机,编译器需要通过
-target 选项了解目标要求。
Zig 标准库(@import("std"))具有跨平台抽象,使相同的源代码可以在许多目标上运行。有些代码比其他代码更具可移植性。通常,Zig
代码与其他编程语言相比具有极高的可移植性。
每个平台都需要自己的实现以使 Zig 的跨平台抽象正常工作。这些实现处于不同的完成程度。编译器的每个标记发布都附带发布说明,其中提供每个目标的完整支持表。
风格指南 §
这些编码约定不由编译器强制执行,但它们与编译器一起在本文档中提供,以便在任何人希望引用公认的 Zig 编码风格权威时提供参考点。
避免名称中的冗余 §
避免在类型名称中使用这些词:
- Value
- Data
- Context
- Manager
- utils、misc 或某人的首字母缩写
一切都是值,所有类型都是数据,一切都是上下文,所有逻辑都管理状态。使用适用于所有类型的词不会传达任何信息。
使用"实用程序"、"杂项"或某人的首字母缩写的诱惑是分类失败,或者更常见的是过度分类。此类声明可以存在于需要它们的模块的根部,无需命名空间。
避免完全限定命名空间中的冗余名称 §
编译器为每个声明分配一个完全限定命名空间,创建一个树结构。根据完全限定的命名空间选择名称,并避免冗余的名称段。
const std = @import("std");
pub const json = struct {
pub const JsonValue = union(enum) {
number: f64,
boolean: bool,
// ...
};
};
pub fn main() void {
std.debug.print("{s}\n", .{@typeName(json.JsonValue)});
}
$ zig build-exe redundant_fqn.zig $ ./redundant_fqn redundant_fqn.json.JsonValue
在此示例中,"json"在完全限定的命名空间中重复。解决方案是从
JsonValue 中删除
Json。在这个示例中,我们有一个名为
json
的空结构,但请记住文件也充当完全限定命名空间的一部分。
此示例是避免名称中的冗余中指定规则的例外。类型的含义已缩减为其核心:它是一个 json 值。该名称不能在不正确的情况下更具体。
空白 §
- 4 空格缩进
- 在同一行上打开大括号,除非需要换行。
- 如果事物列表长度超过 2,则将每个项目放在自己的行上,并在末尾放置一个额外的逗号。
- 行长度:目标为 100;使用常识。
名称 §
大致来说:camelCaseFunctionName、TitleCaseTypeName、snake_case_variable_name。更准确地说:
-
如果
x是type,那么x应该是TitleCase,除非它是一个包含 0 个字段且永远不会被实例化的struct,在这种情况下它被视为"命名空间"并使用snake_case。 -
如果
x是可调用的,并且x的返回类型是type,那么x应该是TitleCase。 -
如果
x在其他情况下是可调用的,那么x应该是camelCase。 -
否则,
x应该是snake_case。
首字母缩略词、首字母缩写词、专有名词或书面英语中具有大写规则的任何其他单词都与任何其他单词一样受命名约定的约束。甚至只有 2 个字母长的首字母缩略词也受这些约定的约束。
文件名分为两类:类型和命名空间。如果文件(隐式为结构)具有顶级字段,则应使用
TitleCase
像任何其他具有字段的结构一样命名。否则,应使用
snake_case。目录名称应为
snake_case。
这些是一般经验法则;如果做不同的事情更有意义,那就做有意义的事情。例如,如果有一个既定的约定,如
ENOENT,遵循既定的约定。
示例 §
const namespace_name = @import("dir_name/file_name.zig");
const TypeName = @import("dir_name/TypeName.zig");
var global_var: i32 = undefined;
const const_name = 42;
const primitive_type_alias = f32;
const string_alias = []u8;
const StructName = struct {
field: i32,
};
const StructAlias = StructName;
fn functionName(param_name: TypeName) void {
var functionPointer = functionName;
functionPointer();
functionPointer = otherFunction;
functionPointer();
}
const functionAlias = functionName;
fn ListTemplateFunction(comptime ChildType: type, comptime fixed_size: usize) type {
return List(ChildType, fixed_size);
}
fn ShortList(comptime T: type, comptime n: usize) type {
return struct {
field_name: [n]T,
fn methodName() void {}
};
}
// The word XML loses its casing when used in Zig identifiers.
const xml_document =
\\<?xml version="1.0" encoding="UTF-8"?>
\\<document>
\\</document>
;
const XmlParser = struct {
field: i32,
};
// The initials BE (Big Endian) are just another word in Zig identifier names.
fn readU32Be() u32 {}
有关更多示例,请参阅 Zig 标准库。
文档注释指南 §
- 省略基于所记录事物的名称而冗余的任何信息。
- 鼓励在多个类似函数上复制信息,因为它有助于 IDE 和其他工具提供更好的帮助文本。
- 使用词假设来表示在违反时导致未检查非法行为的不变量。
- 使用词断言来表示在违反时导致安全检查非法行为的不变量。
源码编码 §
Zig 源代码以 UTF-8 编码。无效的 UTF-8 字节序列会导致编译错误。
在所有 zig 源代码中(包括在注释中),永远不允许某些码位:
- Ascii 控制字符,除了 U+000a (LF)、U+000d (CR) 和 U+0009 (HT):U+0000 - U+0008、U+000b - U+000c、U+000e - U+0001f、U+007f。
- 非 Ascii Unicode 行结束符:U+0085 (NEL)、U+2028 (LS)、U+2029 (PS)。
LF(字节值 0x0a,码位 U+000a,'\n')是 Zig 源代码中的行终止符。此字节值终止 zig
源代码的每一行,除了文件的最后一行。建议非空源文件以空行结尾,这意味着最后一个字节将是
0x0a (LF)。
每个 LF 可以紧接着一个 CR(字节值 0x0d,码位 U+000d,'\r')形成 Windows
样式的行尾,但这是不鼓励的。请注意,在多行字符串中,CRLF
序列在编译到 zig 程序中时将被编码为
LF。在任何其他上下文中都不允许 CR。
HT 硬制表符(字节值 0x09,码位 U+0009,'\t')作为标记分隔符可与 SP 空格(字节值 0x20,码位
U+0020,' ')互换使用,但不鼓励使用硬制表符。参见语法。
为了与其他工具兼容,如果 UTF-8 编码的字节顺序标记(U+FEFF)是源文本中的第一个 Unicode 码位,编译器会忽略它。源代码中的任何其他位置都不允许字节顺序标记。
请注意,在源文件上运行 zig fmt 将实现此处提到的所有建议。
请注意,读取 Zig 源代码的工具可以在假定源代码是正确的 Zig
代码的情况下做出假设。例如,在识别行尾时,工具可以使用简单的搜索,如
/\n/,或高级搜索,如
/\r\n?|[\n\u0085\u2028\u2029]/,在任何一种情况下,行尾都将被正确识别。再例如,在识别一行第一个标记之前的空白时,工具可以使用简单的搜索,如
/[ \t]/,或高级搜索,如
/\s/,在任何一种情况下,空白都将被正确识别。
关键字参考 §
| 关键字 | 描述 |
|---|---|
|
addrspace
关键字。
|
|
align
可用于指定指针的对齐。它也可以在变量或函数声明之后使用,以指定指向该变量或函数的指针的对齐。
|
|
指针属性
allowzero
允许指针具有地址零。
|
|
布尔运算符
and。
|
|
anyframe
可用作保存指向函数帧的指针的变量的类型。
|
|
函数参数可以用
anytype
代替类型进行声明。类型将在调用函数的位置推断。
|
|
asm
开始一个内联汇编表达式。这允许直接控制编译时生成的机器代码。
|
|
break
可以与块标签一起使用以从块返回值。它也可以用于在迭代自然完成之前退出循环。
|
|
callconv
可用于在函数类型中指定调用约定。
|
|
如果它之前的表达式计算为错误,则可以使用
catch
来计算表达式。catch
之后的表达式可以选择捕获错误值。
|
|
声明前的
comptime
可用于将变量或函数参数标记为在编译时已知。它也可以用于保证表达式在编译时运行。
|
|
const
声明一个无法修改的变量。用作指针属性时,它表示指针引用的值无法修改。
|
|
continue
可以在循环中使用以跳回到循环的开始。
|
|
当控制流离开当前块时,defer
将执行一个表达式。
|
|
else
可用于为
if、switch、while
和
for
表达式提供备用分支。
|
|
enum
定义一个枚举类型。
|
|
如果函数返回错误,当控制流离开当前块时,errdefer
将执行一个表达式,errdefer
表达式可以捕获解包的值。
|
|
error
定义一个错误类型。
|
|
export
使函数或变量在生成的目标文件中外部可见。导出的函数默认使用
C 调用约定。
|
|
extern
可用于声明将在静态链接时或在动态链接时在运行时解析的函数或变量。
|
|
fn
声明一个函数。
|
|
for
表达式可用于迭代切片、数组或元组的元素。
|
|
if
表达式可以测试布尔表达式、可选值或错误联合。对于可选值或错误联合,if
表达式可以捕获解包的值。
|
|
inline
可用于标记循环表达式,使其在编译时展开。它也可以用于强制函数在所有调用位置内联。
|
|
linksection
关键字可用于指定函数或全局变量将放入哪个节(例如
.text)。
|
|
noalias
关键字。
|
|
noinline
禁止函数在所有调用位置内联。
|
|
nosuspend
关键字可用于块、语句或表达式之前,以标记未到达挂起点的范围。特别是,在
nosuspend
范围内:
nosuspend
范围内的代码不会导致封闭函数成为异步函数。
|
|
opaque
定义一个不透明类型。
|
|
布尔运算符
or。
|
|
如果它之前的表达式计算为 null,则可以使用
orelse
来计算表达式。
|
|
struct 定义前的
packed
关键字会将 struct 的内存布局更改为保证的
packed
布局。
|
|
顶级声明前的
pub
使该声明可以从声明它的文件以外的文件中引用。
|
|
resume
将在函数挂起的点之后继续执行函数帧。
|
|
return
使用一个值退出函数。
|
|
struct
定义一个结构。
|
|
suspend
将使控制流返回到函数的调用位置或恢复者。suspend
也可以在函数内的块之前使用,以允许函数在控制流返回到调用位置之前访问其帧。
|
|
switch
表达式可用于测试通用类型的值。switch
case 可以捕获标记联合的字段值。
|
|
test
关键字可用于表示用于确保行为符合预期的顶级代码块。
|
|
threadlocal
可用于将变量指定为线程局部。
|
|
try
计算错误联合表达式。如果它是错误,则使用相同的错误从当前函数返回。否则,表达式结果为解包的值。
|
|
union
定义一个联合。
|
|
unreachable
可用于断言控制流永远不会到达特定位置。根据构建模式,unreachable
可能会触发 panic。
|
|
var
声明一个可以修改的变量。
|
|
volatile
可用于表示指针的加载或存储具有副作用。它也可以修改内联汇编表达式以表示它具有副作用。
|
|
while
表达式可用于重复测试布尔、可选或错误联合表达式,并在该表达式分别计算为
false、null 或错误时停止循环。
|
附录 §
容器 §
Zig 中的容器是充当命名空间以保存变量和函数声明的任何语法结构。容器也是可以实例化的类型定义。结构、枚举、联合、不透明,甚至 Zig 源文件本身都是容器。
尽管容器(除 Zig 源文件外)使用花括号包围其定义,但它们不应与块或函数混淆。容器不包含语句。
语法 §
Root <- skip container_doc_comment? ContainerMembers eof
# *** 顶层 ***
ContainerMembers <- ContainerDeclaration* (ContainerField COMMA)* (ContainerField / ContainerDeclaration*)
ContainerDeclaration <- TestDecl / ComptimeDecl / doc_comment? KEYWORD_pub? Decl
TestDecl <- KEYWORD_test (STRINGLITERALSINGLE / IDENTIFIER)? Block
ComptimeDecl <- KEYWORD_comptime Block
Decl
<- (KEYWORD_export / KEYWORD_extern STRINGLITERALSINGLE? / KEYWORD_inline / KEYWORD_noinline)? FnProto (SEMICOLON / Block)
/ (KEYWORD_export / KEYWORD_extern STRINGLITERALSINGLE?)? KEYWORD_threadlocal? GlobalVarDecl
FnProto <- KEYWORD_fn IDENTIFIER? LPAREN ParamDeclList RPAREN ByteAlign? AddrSpace? LinkSection? CallConv? EXCLAMATIONMARK? TypeExpr
VarDeclProto <- (KEYWORD_const / KEYWORD_var) IDENTIFIER (COLON TypeExpr)? ByteAlign? AddrSpace? LinkSection?
GlobalVarDecl <- VarDeclProto (EQUAL Expr)? SEMICOLON
ContainerField <- doc_comment? KEYWORD_comptime? !KEYWORD_fn (IDENTIFIER COLON)? TypeExpr ByteAlign? (EQUAL Expr)?
# *** 块级 ***
Statement
<- KEYWORD_comptime ComptimeStatement
/ KEYWORD_nosuspend BlockExprStatement
/ KEYWORD_suspend BlockExprStatement
/ KEYWORD_defer BlockExprStatement
/ KEYWORD_errdefer Payload? BlockExprStatement
/ IfStatement
/ LabeledStatement
/ SwitchExpr
/ VarDeclExprStatement
ComptimeStatement
<- BlockExpr
/ VarDeclExprStatement
IfStatement
<- IfPrefix BlockExpr ( KEYWORD_else Payload? Statement )?
/ IfPrefix AssignExpr ( SEMICOLON / KEYWORD_else Payload? Statement )
LabeledStatement <- BlockLabel? (Block / LoopStatement)
LoopStatement <- KEYWORD_inline? (ForStatement / WhileStatement)
ForStatement
<- ForPrefix BlockExpr ( KEYWORD_else Statement )?
/ ForPrefix AssignExpr ( SEMICOLON / KEYWORD_else Statement )
WhileStatement
<- WhilePrefix BlockExpr ( KEYWORD_else Payload? Statement )?
/ WhilePrefix AssignExpr ( SEMICOLON / KEYWORD_else Payload? Statement )
BlockExprStatement
<- BlockExpr
/ AssignExpr SEMICOLON
BlockExpr <- BlockLabel? Block
# 表达式、赋值或任何解构,作为语句。
VarDeclExprStatement
<- VarDeclProto (COMMA (VarDeclProto / Expr))* EQUAL Expr SEMICOLON
/ Expr (AssignOp Expr / (COMMA (VarDeclProto / Expr))+ EQUAL Expr)? SEMICOLON
# *** 表达式级 ***
# 一个赋值或解构,其左侧都是左值表达式。
AssignExpr <- Expr (AssignOp Expr / (COMMA Expr)+ EQUAL Expr)?
SingleAssignExpr <- Expr (AssignOp Expr)?
Expr <- BoolOrExpr
BoolOrExpr <- BoolAndExpr (KEYWORD_or BoolAndExpr)*
BoolAndExpr <- CompareExpr (KEYWORD_and CompareExpr)*
CompareExpr <- BitwiseExpr (CompareOp BitwiseExpr)?
BitwiseExpr <- BitShiftExpr (BitwiseOp BitShiftExpr)*
BitShiftExpr <- AdditionExpr (BitShiftOp AdditionExpr)*
AdditionExpr <- MultiplyExpr (AdditionOp MultiplyExpr)*
MultiplyExpr <- PrefixExpr (MultiplyOp PrefixExpr)*
PrefixExpr <- PrefixOp* PrimaryExpr
PrimaryExpr
<- AsmExpr
/ IfExpr
/ KEYWORD_break BreakLabel? Expr?
/ KEYWORD_comptime Expr
/ KEYWORD_nosuspend Expr
/ KEYWORD_continue BreakLabel?
/ KEYWORD_resume Expr
/ KEYWORD_return Expr?
/ BlockLabel? LoopExpr
/ Block
/ CurlySuffixExpr
IfExpr <- IfPrefix Expr (KEYWORD_else Payload? Expr)?
Block <- LBRACE Statement* RBRACE
LoopExpr <- KEYWORD_inline? (ForExpr / WhileExpr)
ForExpr <- ForPrefix Expr (KEYWORD_else Expr)?
WhileExpr <- WhilePrefix Expr (KEYWORD_else Payload? Expr)?
CurlySuffixExpr <- TypeExpr InitList?
InitList
<- LBRACE FieldInit (COMMA FieldInit)* COMMA? RBRACE
/ LBRACE Expr (COMMA Expr)* COMMA? RBRACE
/ LBRACE RBRACE
TypeExpr <- PrefixTypeOp* ErrorUnionExpr
ErrorUnionExpr <- SuffixExpr (EXCLAMATIONMARK TypeExpr)?
SuffixExpr
<- PrimaryTypeExpr (SuffixOp / FnCallArguments)*
PrimaryTypeExpr
<- BUILTINIDENTIFIER FnCallArguments
/ CHAR_LITERAL
/ ContainerDecl
/ DOT IDENTIFIER
/ DOT InitList
/ ErrorSetDecl
/ FLOAT
/ FnProto
/ GroupedExpr
/ LabeledTypeExpr
/ IDENTIFIER
/ IfTypeExpr
/ INTEGER
/ KEYWORD_comptime TypeExpr
/ KEYWORD_error DOT IDENTIFIER
/ KEYWORD_anyframe
/ KEYWORD_unreachable
/ STRINGLITERAL
/ SwitchExpr
ContainerDecl <- (KEYWORD_extern / KEYWORD_packed)? ContainerDeclAuto
ErrorSetDecl <- KEYWORD_error LBRACE IdentifierList RBRACE
GroupedExpr <- LPAREN Expr RPAREN
IfTypeExpr <- IfPrefix TypeExpr (KEYWORD_else Payload? TypeExpr)?
LabeledTypeExpr
<- BlockLabel Block
/ BlockLabel? LoopTypeExpr
LoopTypeExpr <- KEYWORD_inline? (ForTypeExpr / WhileTypeExpr)
ForTypeExpr <- ForPrefix TypeExpr (KEYWORD_else TypeExpr)?
WhileTypeExpr <- WhilePrefix TypeExpr (KEYWORD_else Payload? TypeExpr)?
SwitchExpr <- KEYWORD_switch LPAREN Expr RPAREN LBRACE SwitchProngList RBRACE
# *** 汇编 ***
AsmExpr <- KEYWORD_asm KEYWORD_volatile? LPAREN Expr AsmOutput? RPAREN
AsmOutput <- COLON AsmOutputList AsmInput?
AsmOutputItem <- LBRACKET IDENTIFIER RBRACKET STRINGLITERAL LPAREN (MINUSRARROW TypeExpr / IDENTIFIER) RPAREN
AsmInput <- COLON AsmInputList AsmClobbers?
AsmInputItem <- LBRACKET IDENTIFIER RBRACKET STRINGLITERAL LPAREN Expr RPAREN
AsmClobbers <- COLON Expr
# *** 辅助语法 ***
BreakLabel <- COLON IDENTIFIER
BlockLabel <- IDENTIFIER COLON
FieldInit <- DOT IDENTIFIER EQUAL Expr
WhileContinueExpr <- COLON LPAREN AssignExpr RPAREN
LinkSection <- KEYWORD_linksection LPAREN Expr RPAREN
AddrSpace <- KEYWORD_addrspace LPAREN Expr RPAREN
# 函数特定
CallConv <- KEYWORD_callconv LPAREN Expr RPAREN
ParamDecl
<- doc_comment? (KEYWORD_noalias / KEYWORD_comptime)? (IDENTIFIER COLON)? ParamType
/ DOT3
ParamType
<- KEYWORD_anytype
/ TypeExpr
# 控制流前缀
IfPrefix <- KEYWORD_if LPAREN Expr RPAREN PtrPayload?
WhilePrefix <- KEYWORD_while LPAREN Expr RPAREN PtrPayload? WhileContinueExpr?
ForPrefix <- KEYWORD_for LPAREN ForArgumentsList RPAREN PtrListPayload
# 载荷
Payload <- PIPE IDENTIFIER PIPE
PtrPayload <- PIPE ASTERISK? IDENTIFIER PIPE
PtrIndexPayload <- PIPE ASTERISK? IDENTIFIER (COMMA IDENTIFIER)? PIPE
PtrListPayload <- PIPE ASTERISK? IDENTIFIER (COMMA ASTERISK? IDENTIFIER)* COMMA? PIPE
# Switch 特定
SwitchProng <- KEYWORD_inline? SwitchCase EQUALRARROW PtrIndexPayload? SingleAssignExpr
SwitchCase
<- SwitchItem (COMMA SwitchItem)* COMMA?
/ KEYWORD_else
SwitchItem <- Expr (DOT3 Expr)?
# For 特定
ForArgumentsList <- ForItem (COMMA ForItem)* COMMA?
ForItem <- Expr (DOT2 Expr?)?
# 运算符
AssignOp
<- ASTERISKEQUAL
/ ASTERISKPIPEEQUAL
/ SLASHEQUAL
/ PERCENTEQUAL
/ PLUSEQUAL
/ PLUSPIPEEQUAL
/ MINUSEQUAL
/ MINUSPIPEEQUAL
/ LARROW2EQUAL
/ LARROW2PIPEEQUAL
/ RARROW2EQUAL
/ AMPERSANDEQUAL
/ CARETEQUAL
/ PIPEEQUAL
/ ASTERISKPERCENTEQUAL
/ PLUSPERCENTEQUAL
/ MINUSPERCENTEQUAL
/ EQUAL
CompareOp
<- EQUALEQUAL
/ EXCLAMATIONMARKEQUAL
/ LARROW
/ RARROW
/ LARROWEQUAL
/ RARROWEQUAL
BitwiseOp
<- AMPERSAND
/ CARET
/ PIPE
/ KEYWORD_orelse
/ KEYWORD_catch Payload?
BitShiftOp
<- LARROW2
/ RARROW2
/ LARROW2PIPE
AdditionOp
<- PLUS
/ MINUS
/ PLUS2
/ PLUSPERCENT
/ MINUSPERCENT
/ PLUSPIPE
/ MINUSPIPE
MultiplyOp
<- PIPE2
/ ASTERISK
/ SLASH
/ PERCENT
/ ASTERISK2
/ ASTERISKPERCENT
/ ASTERISKPIPE
PrefixOp
<- EXCLAMATIONMARK
/ MINUS
/ TILDE
/ MINUSPERCENT
/ AMPERSAND
/ KEYWORD_try
PrefixTypeOp
<- QUESTIONMARK
/ KEYWORD_anyframe MINUSRARROW
/ SliceTypeStart (ByteAlign / AddrSpace / KEYWORD_const / KEYWORD_volatile / KEYWORD_allowzero)*
/ PtrTypeStart (AddrSpace / KEYWORD_align LPAREN Expr (COLON Expr COLON Expr)? RPAREN / KEYWORD_const / KEYWORD_volatile / KEYWORD_allowzero)*
/ ArrayTypeStart
SuffixOp
<- LBRACKET Expr (DOT2 (Expr? (COLON Expr)?)?)? RBRACKET
/ DOT IDENTIFIER
/ DOTASTERISK
/ DOTQUESTIONMARK
FnCallArguments <- LPAREN ExprList RPAREN
# 指针特定
SliceTypeStart <- LBRACKET (COLON Expr)? RBRACKET
PtrTypeStart
<- ASTERISK
/ ASTERISK2
/ LBRACKET ASTERISK (LETTERC / COLON Expr)? RBRACKET
ArrayTypeStart <- LBRACKET Expr (COLON Expr)? RBRACKET
# 容器声明特定
ContainerDeclAuto <- ContainerDeclType LBRACE container_doc_comment? ContainerMembers RBRACE
ContainerDeclType
<- KEYWORD_struct (LPAREN Expr RPAREN)?
/ KEYWORD_opaque
/ KEYWORD_enum (LPAREN Expr RPAREN)?
/ KEYWORD_union (LPAREN (KEYWORD_enum (LPAREN Expr RPAREN)? / Expr) RPAREN)?
# 对齐
ByteAlign <- KEYWORD_align LPAREN Expr RPAREN
# 列表
IdentifierList <- (doc_comment? IDENTIFIER COMMA)* (doc_comment? IDENTIFIER)?
SwitchProngList <- (SwitchProng COMMA)* SwitchProng?
AsmOutputList <- (AsmOutputItem COMMA)* AsmOutputItem?
AsmInputList <- (AsmInputItem COMMA)* AsmInputItem?
StringList <- (STRINGLITERAL COMMA)* STRINGLITERAL?
ParamDeclList <- (ParamDecl COMMA)* ParamDecl?
ExprList <- (Expr COMMA)* Expr?
# *** 词法单元 ***
eof <- !.
bin <- [01]
bin_ <- '_'? bin
oct <- [0-7]
oct_ <- '_'? oct
hex <- [0-9a-fA-F]
hex_ <- '_'? hex
dec <- [0-9]
dec_ <- '_'? dec
bin_int <- bin bin_*
oct_int <- oct oct_*
dec_int <- dec dec_*
hex_int <- hex hex_*
ox80_oxBF <- [\200-\277]
oxF4 <- '\364'
ox80_ox8F <- [\200-\217]
oxF1_oxF3 <- [\361-\363]
oxF0 <- '\360'
ox90_0xBF <- [\220-\277]
oxEE_oxEF <- [\356-\357]
oxED <- '\355'
ox80_ox9F <- [\200-\237]
oxE1_oxEC <- [\341-\354]
oxE0 <- '\340'
oxA0_oxBF <- [\240-\277]
oxC2_oxDF <- [\302-\337]
# 来自 https://lemire.me/blog/2018/05/09/how-quickly-can-you-check-that-a-string-is-valid-unicode-utf-8/
# 第一字节 第二字节 第三字节 第四字节
# [0x00,0x7F]
# [0xC2,0xDF] [0x80,0xBF]
# 0xE0 [0xA0,0xBF] [0x80,0xBF]
# [0xE1,0xEC] [0x80,0xBF] [0x80,0xBF]
# 0xED [0x80,0x9F] [0x80,0xBF]
# [0xEE,0xEF] [0x80,0xBF] [0x80,0xBF]
# 0xF0 [0x90,0xBF] [0x80,0xBF] [0x80,0xBF]
# [0xF1,0xF3] [0x80,0xBF] [0x80,0xBF] [0x80,0xBF]
# 0xF4 [0x80,0x8F] [0x80,0xBF] [0x80,0xBF]
mb_utf8_literal <-
oxF4 ox80_ox8F ox80_oxBF ox80_oxBF
/ oxF1_oxF3 ox80_oxBF ox80_oxBF ox80_oxBF
/ oxF0 ox90_0xBF ox80_oxBF ox80_oxBF
/ oxEE_oxEF ox80_oxBF ox80_oxBF
/ oxED ox80_ox9F ox80_oxBF
/ oxE1_oxEC ox80_oxBF ox80_oxBF
/ oxE0 oxA0_oxBF ox80_oxBF
/ oxC2_oxDF ox80_oxBF
ascii_char_not_nl_slash_squote <- [\000-\011\013-\046\050-\133\135-\177]
char_escape
<- "\\x" hex hex
/ "\\u{" hex+ "}"
/ "\\" [nr\\t'"]
char_char
<- mb_utf8_literal
/ char_escape
/ ascii_char_not_nl_slash_squote
string_char
<- char_escape
/ [^\\"\n]
container_doc_comment <- ('//!' [^\n]* [ \n]* skip)+
doc_comment <- ('///' [^\n]* [ \n]* skip)+
line_comment <- '//' ![!/][^\n]* / '////' [^\n]*
line_string <- ("\\\\" [^\n]* [ \n]*)+
skip <- ([ \n] / line_comment)*
CHAR_LITERAL <- "'" char_char "'" skip
FLOAT
<- "0x" hex_int "." hex_int ([pP] [-+]? dec_int)? skip
/ dec_int "." dec_int ([eE] [-+]? dec_int)? skip
/ "0x" hex_int [pP] [-+]? dec_int skip
/ dec_int [eE] [-+]? dec_int skip
INTEGER
<- "0b" bin_int skip
/ "0o" oct_int skip
/ "0x" hex_int skip
/ dec_int skip
STRINGLITERALSINGLE <- "\"" string_char* "\"" skip
STRINGLITERAL
<- STRINGLITERALSINGLE
/ (line_string skip)+
IDENTIFIER
<- !keyword [A-Za-z_] [A-Za-z0-9_]* skip
/ "@" STRINGLITERALSINGLE
BUILTINIDENTIFIER <- "@"[A-Za-z_][A-Za-z0-9_]* skip
AMPERSAND <- '&' ![=] skip
AMPERSANDEQUAL <- '&=' skip
ASTERISK <- '*' ![*%=|] skip
ASTERISK2 <- '**' skip
ASTERISKEQUAL <- '*=' skip
ASTERISKPERCENT <- '*%' ![=] skip
ASTERISKPERCENTEQUAL <- '*%=' skip
ASTERISKPIPE <- '*|' ![=] skip
ASTERISKPIPEEQUAL <- '*|=' skip
CARET <- '^' ![=] skip
CARETEQUAL <- '^=' skip
COLON <- ':' skip
COMMA <- ',' skip
DOT <- '.' ![*.?] skip
DOT2 <- '..' ![.] skip
DOT3 <- '...' skip
DOTASTERISK <- '.*' skip
DOTQUESTIONMARK <- '.?' skip
EQUAL <- '=' ![>=] skip
EQUALEQUAL <- '==' skip
EQUALRARROW <- '=>' skip
EXCLAMATIONMARK <- '!' ![=] skip
EXCLAMATIONMARKEQUAL <- '!=' skip
LARROW <- '<' ![<=] skip
LARROW2 <- '<<' ![=|] skip
LARROW2EQUAL <- '<<=' skip
LARROW2PIPE <- '<<|' ![=] skip
LARROW2PIPEEQUAL <- '<<|=' skip
LARROWEQUAL <- '<=' skip
LBRACE <- '{' skip
LBRACKET <- '[' skip
LPAREN <- '(' skip
MINUS <- '-' ![%=>|] skip
MINUSEQUAL <- '-=' skip
MINUSPERCENT <- '-%' ![=] skip
MINUSPERCENTEQUAL <- '-%=' skip
MINUSPIPE <- '-|' ![=] skip
MINUSPIPEEQUAL <- '-|=' skip
MINUSRARROW <- '->' skip
PERCENT <- '%' ![=] skip
PERCENTEQUAL <- '%=' skip
PIPE <- '|' ![|=] skip
PIPE2 <- '||' skip
PIPEEQUAL <- '|=' skip
PLUS <- '+' ![%+=|] skip
PLUS2 <- '++' skip
PLUSEQUAL <- '+=' skip
PLUSPERCENT <- '+%' ![=] skip
PLUSPERCENTEQUAL <- '+%=' skip
PLUSPIPE <- '+|' ![=] skip
PLUSPIPEEQUAL <- '+|=' skip
LETTERC <- 'c' skip
QUESTIONMARK <- '?' skip
RARROW <- '>' ![>=] skip
RARROW2 <- '>>' ![=] skip
RARROW2EQUAL <- '>>=' skip
RARROWEQUAL <- '>=' skip
RBRACE <- '}' skip
RBRACKET <- ']' skip
RPAREN <- ')' skip
SEMICOLON <- ';' skip
SLASH <- '/' ![=] skip
SLASHEQUAL <- '/=' skip
TILDE <- '~' skip
end_of_word <- ![a-zA-Z0-9_] skip
KEYWORD_addrspace <- 'addrspace' end_of_word
KEYWORD_align <- 'align' end_of_word
KEYWORD_allowzero <- 'allowzero' end_of_word
KEYWORD_and <- 'and' end_of_word
KEYWORD_anyframe <- 'anyframe' end_of_word
KEYWORD_anytype <- 'anytype' end_of_word
KEYWORD_asm <- 'asm' end_of_word
KEYWORD_break <- 'break' end_of_word
KEYWORD_callconv <- 'callconv' end_of_word
KEYWORD_catch <- 'catch' end_of_word
KEYWORD_comptime <- 'comptime' end_of_word
KEYWORD_const <- 'const' end_of_word
KEYWORD_continue <- 'continue' end_of_word
KEYWORD_defer <- 'defer' end_of_word
KEYWORD_else <- 'else' end_of_word
KEYWORD_enum <- 'enum' end_of_word
KEYWORD_errdefer <- 'errdefer' end_of_word
KEYWORD_error <- 'error' end_of_word
KEYWORD_export <- 'export' end_of_word
KEYWORD_extern <- 'extern' end_of_word
KEYWORD_fn <- 'fn' end_of_word
KEYWORD_for <- 'for' end_of_word
KEYWORD_if <- 'if' end_of_word
KEYWORD_inline <- 'inline' end_of_word
KEYWORD_noalias <- 'noalias' end_of_word
KEYWORD_nosuspend <- 'nosuspend' end_of_word
KEYWORD_noinline <- 'noinline' end_of_word
KEYWORD_opaque <- 'opaque' end_of_word
KEYWORD_or <- 'or' end_of_word
KEYWORD_orelse <- 'orelse' end_of_word
KEYWORD_packed <- 'packed' end_of_word
KEYWORD_pub <- 'pub' end_of_word
KEYWORD_resume <- 'resume' end_of_word
KEYWORD_return <- 'return' end_of_word
KEYWORD_linksection <- 'linksection' end_of_word
KEYWORD_struct <- 'struct' end_of_word
KEYWORD_suspend <- 'suspend' end_of_word
KEYWORD_switch <- 'switch' end_of_word
KEYWORD_test <- 'test' end_of_word
KEYWORD_threadlocal <- 'threadlocal' end_of_word
KEYWORD_try <- 'try' end_of_word
KEYWORD_union <- 'union' end_of_word
KEYWORD_unreachable <- 'unreachable' end_of_word
KEYWORD_var <- 'var' end_of_word
KEYWORD_volatile <- 'volatile' end_of_word
KEYWORD_while <- 'while' end_of_word
keyword <- KEYWORD_addrspace / KEYWORD_align / KEYWORD_allowzero / KEYWORD_and
/ KEYWORD_anyframe / KEYWORD_anytype / KEYWORD_asm
/ KEYWORD_break / KEYWORD_callconv / KEYWORD_catch
/ KEYWORD_comptime / KEYWORD_const / KEYWORD_continue / KEYWORD_defer
/ KEYWORD_else / KEYWORD_enum / KEYWORD_errdefer / KEYWORD_error / KEYWORD_export
/ KEYWORD_extern / KEYWORD_fn / KEYWORD_for / KEYWORD_if
/ KEYWORD_inline / KEYWORD_noalias / KEYWORD_nosuspend / KEYWORD_noinline
/ KEYWORD_opaque / KEYWORD_or / KEYWORD_orelse / KEYWORD_packed
/ KEYWORD_pub / KEYWORD_resume / KEYWORD_return / KEYWORD_linksection
/ KEYWORD_struct / KEYWORD_suspend / KEYWORD_switch / KEYWORD_test
/ KEYWORD_threadlocal / KEYWORD_try / KEYWORD_union / KEYWORD_unreachable
/ KEYWORD_var / KEYWORD_volatile / KEYWORD_while
禅意 §
- 精确传达意图。
- 边界情况很重要。
- 优先阅读代码,而非编写代码。
- 只有一种明显的方式来做事情。
- 运行时崩溃优于错误。
- 编译错误优于运行时崩溃。
- 渐进式改进。
- 避免局部最优。
- 减少必须记住的内容。
- 专注于代码而非风格。
- 资源分配可能失败;资源释放必须成功。
- 内存是一种资源。
- 我们共同服务于用户。