Zig语言中文手册

1. 简介(introduction)

Zig语言是强类型的、无内存垃圾收集的静态语言,具有独一无二的强大的编译期计算和编译期类型反射功能,并且紧跟时代发展,有切片、可选类型、错误联合类型等现代特性。Zig语言编译工具链可直接编译C语言,跨平台的交叉编译强大且易用。编译期运算更易于性能优化,交叉编译更易于Zig源代码跨平台使用。

可以大致理解为:

如果有基本的C语言和计算机组成原理方面的基础,更易于学习使用Zig语言。

本文档参考下列文档和源代码编写:

名词解释:

内存垃圾收集(:gc garbage collection)

用alloc是在堆上申请的内存,堆内存由语言虚拟机自动管理、自动释放的特性称之为内存垃圾收集。

编译期(:compile time)

源代码须经过编译生成目标文件,经链接后生成可执行程序。编译期是指从源代码到生成目标文件期间。

运行期(:run time)

运行期是指可执行程序从程序加载到内存中,到程序运行结束期间。

交叉编译(:cross-compilation)

交叉编译是指在某平台上,源代码可以编译生成另一平台的可执行文件。例如:在Windows下生成linux可执行程序。

1.1. 编写原则(write rule)

本文档重点是理解和使用Zig语言本身。

由于阅读时大段文本理解费劲,另外为了便于在手机和PAD上阅读,所以:

1.2. 安装(installation)

安装包下载网址:

https://ziglang.org/download/

下载后解压,将Zig目录添加到PATH(或path)环境变量。

shell

$ zig version
0.11.0-dev.38+b40fc7018

表明安装成功。

1.3. 示例程序(example source code)

示例程序尽可能不超过半个PC屏幕的高和宽,用尽可能短的标识符,减少空格和空行。

注意:实际开发时应以正确的风格来源代码,本文档中的示例代码仅适用于阅读浏览。

所有示例程序均在win10环境下验证测试,Zig语言版本为:0.11.0-dev.38+b40fc7018

如果示例程序名是: run_xxx.zig ,则编译运行该示例用的命令是:

zig build-exe run_xxx.zig

run_xxx

zig run run_xxx.zig

在有些示例源代码的 print 函数后如果有注释,是程序运行时该行的输出显示内容。

要注意有些输出显示内容(如指针地址)每次运行可能都可能不一样。

如果示例程序名是: test_xxx.zig ,表示测试该示例用的命令是:

zig test test_xxx.zig

测试功能的详细使用说明参见20. 测试(test)

如果没有额外的编译测试选项,或者运行无错或测试符合预期,则文档中不再描述命令行情况。

1.4. 第一个示例(first example)

示例中, fn 是函数定义关键字, @ 开头的一般是Zig语言的内置函数, @import 函数引入标准库。

Zig 程序的运行起点是 main 函数。如果是程序库则不需要 main 函数。

print函数中的第2个参数 .{"world"} ,是匿名列表类型。

shell表示命令行交互情况。$ 后面是命令行输入,其它是信息输出行。在windows环境下, $ 可能是 c:\zig>

run_hello.zig

const print = @import("std").debug.print;
pub fn main() void {
    //this is comm
    print("Hello, world!\n", .{});
}
shell

$ zig run run_hello.zig
Hello, world!

1.4.1. windows命令行输出乱码(windows output is garbled)

在 windows 的 cmd 命令行窗口下,zig 程序使用非 ascii 字符(如汉字)可能会输出乱码。

一个简便的方法是用 chcp 命令。 chcp 不带参数显示当前代码页(即输入输出编码方式),带参数改变当前代码页。常用的参数有:

参数 编码方式 说明
936 GBK 简体中文
65001 UTF-8
437 IBM437 美国编码
1252 latin-1 0x80-0xff中间有些是西欧字母的编码

run_winutf8.zig

const print=@import("std").std.debug.print;
pub fn main() void{
    print("Hello 世界", .{});
}
shell

D:\ziglearn>zig build-exe run_winutf8.zig
D:\ziglearn>run_winutf8
Hello 涓栫晫
D:\ziglearn>chcp
活动代码页: 936
D:\ziglearn>chcp 65001
Active code page: 65001
D:\ziglearn>run_winutf8
Hello 世界

1.5. 注释(comment)

Zig源代码的注释以 // 开头到行尾。例如上例中的//this is comm

Zig没有多行注释。

1.5.1. 文档注释行(doc comment)

文档注释行主要用于自动生成API接口文档。

文档注释行以 /// 开始。

文档注释最好是单独的行,否则编译可能出错(比如在表达式中间时)。

1.5.2. 文件注释行(top-level doc comment)

整个文件的注释以 //! 开始。文件注释行只能放在文件的最前面。

2. 值(value)

Zig语言中的值必须要有确切的类型。

2.1. 基本类型(primitive type)

Zig语言中没有专门设立字符类型和字符串类型。字符字面值的类型是 comptime_int ,字符串字面值的类型是 *const [:0]u8。

名字解释:

字面值(:literal) 是指在源代码中用数字、字符或字符串直接写出的值,。比如: x=12; 12 就是数字字面值。

2.1.1. 整数(integer)

整数类型名中 i 表示有符号整数(含负数), u 表示无符号整数(不含负数),后面的数字表示该类型的比特位长。

用二进制补码表示有符号整数。

普通整数类型有:i8 u8 i16 u16 i32 u32 i64 u64 i128 u128

例如 u8 表示有符号整数,比特位长为8位,相当于C语言中的 unsigned char 类型。

目标平台相关整数类型有: isize usize

该类型在32位CPU或32位可执行程序上,比特位长是32;在64位CPU上的64位可执行程序时,比特位长是64。

该类型通常用于指针和整数间类型转换,和数组索引。

其它比特位整数类型有: iN uN

该类型表示整数的比特位长是 N ,N 的最大值是65535。

例如 u21 可表示 unicode 码点类型,因为 unicode 码点最大值为 0x10FFFF ,共21位。

名词解释:

比特(:bit) 比特是信息的最小单位,可以是有无、真假等。通常用 0 1 表示其值。在数字电路中通常用低电平表示0,高电平表示1。在计算机中1个比特占1位,是不能再细分的最小单位。

字节(:byte) 在绝大多数计算机中,1个字节由8个比特组成,字节是程序运行的基本单位。Zig语言中的 u8, i8 类型,C语言中的 char, unsigned char 类型,均是字节。

2.1.2. 浮点数(float)

浮点数类型有: f16 f32 f64 f80 f128

浮点数类型须符合IEEE-754标准,f后面的数字是比特位长。

f32可对应C语言的float类型,f64可对应C语言的double类型。

2.1.3. 编译期字面值(comptime literal)

comptime_int 编译期可知的整数字面值的类型。

comptime_float 编译期可知的浮点数字面值的类型。

2.1.4. 其它类型(other type)

2.1.5. C语言ABI兼容类型(c ABI compatible)

c_short c_ushort c_int c_uint c_long c_ulong c_longlong c_ulonglong c_longdouble

主要用于与C语言ABI接口用,如c_ulonglong等同于C语言中unsigned long long类型。

名词解释:

应用程序二进制接口(:ABI application binary interface) 描述了目标平台、应用程序之间的数据类型、调用约定、二进制格式等接口规范。

2.2. 字面值(literal)

字面值是指写在源代码中的,由数字、字母和其它字符组成符合语法的具体值。

2.2.1. 整数字面值(integer literal)

整数字面值的类型是 comptime_int。

整数类型字面值可转换为能容纳其值的任意整数类型。

不加前缀表示是十进制数,加前缀 0x 表示十六进制,前缀 0o 表示八进制,前缀 0b 表示二进制。

字面值数字字符间可以用 _ 隔开,以更易于查看。

test_integer_literal.zig

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=0b1111_1100;
const permissions=0o7_5_5;

2.2.2. 浮点数字面值(float literal)

浮点数字面值的类型是comptime_float,可以最大保证与f128有同样的精度。

浮点数字面值可强制转换为任意浮点数类型,如果没有小数则可强制转换为整数类型。

字面值中有 e 的是十进制的科学计数法格式,基数是10;有 p 的是二进制的科学计数法格式,基数是2。

test_float_literals.zig

const floating_point=123.0E+77;
const another_float=123.0;
const another_float1=1234.02;
const yet_another=123.0e+77;
const hex_floating_point=0x103.70p-5;
const another_hex_float=0x103.70;
const nanosecond=0.000_000_001;

2.2.3. 字符字面值(char literal)

字符字面值是指以单引号包围的单个字符,类型是 comptime_int ,值是 unicode 码点。

如下例中,字符 e 的值是 0x65 ,也是它的 unicode 码点,类型是 comptime_int。

汉字 语 的值是 0x8BED ,也是它的 unicode 码点,类型也是 comptime_int。

run_char_literal.zig

const print=@import("std").std.debug.print;
pub fn main() void{
    print("{} {}\n", .{'e' == '\x65',@TypeOf('e')});
    print("{X} {}\n",.{'语',@TypeOf('语')});
}
shell

$ zig run run_char_literal.zig
true comptime_int
8BED comptime_int

字符字面值不能为空(即''),如果是''则编译出错。如果想表示编码为0的字符,可以用'\x00'。

名词解释:

unicode 全球统一的,可容纳各种语言的,字符集、编码方案方面的国际标准。https://home.unicode.org/

unicode码点(:unicode code point) 字符对应的unicode 32位编码称之为码点,不同字符的码点也不同。编码范围是从 0 到 0x10FFFF 。

2.2.4. 字符串字面值(string literal)

字符串字面值是指以双引号包围的零到多个字符,类型是 *const [x:0]u8,是指向以 '\x00' (字节值为0)结尾的,长度为 x(不含结尾0)的,元素类型为 u8 的常量指针。

字符串字面值是UTF-8编码的。

如本例中,"hello"的类型是 *const [5:0]u8, "" 的类型是 *const [0:0]u8

把 "hello" 赋值给 bytes 常量,则 bytes 可以转换为切片使用,取得长度值,根据索引访问元素。

run_string_literal.zig

const print=@import("std").std.debug.print;
pub fn main() void{
    const bytes = "hello";
    print("{}\n", .{@TypeOf(bytes)});
    print("{} {c} {}\n", .{bytes.len,bytes[1],bytes[5]});
    print("{}   {}\n",.{""[0],@TypeOf("")});
}
shell

$ zig run run_string_literal.zig
*[5:0]u8
5 e 0
0   *const [0:0]u8

名词解释:

unicode转换格式(:UTF-8 unicode transformation format) 把32位 unicode 码点编码为以字节(8比特)为单位的序列,称为 UTF-8 编码。 1个UTF-8 编码的最大长度是4个字节长。

码点和UTF-8编码转换关系式为:

码点 UTF-8
0AAA_AAAA 0AAA_AAAA
BBB_BBAA_AAAA 110B_BBBB 10AA_AAAA
CCCC_BBBB_BBAA_AAAA 1110_CCCC 10BB_BBBB 10AA_AAAA
D_DDCC_CCCC_BBBB_BBAA_AAAA 1111_0DDD 10CC_CCCC 10BB_BBBB 10AA_AAAA

例如:

码点十六进制 码点二进制 UTF-8二进制 UTF-8十六进制
7E 1111110 01111110 7E
2BC 01010_111100 110_01010 0b10_111100 CA BC
B95B 1011_100101_011011 1110_1011 10_100101 10_011011 EB A5 9B
10BD6D 100_001011_110101_101101 1111_0100 10_001011 10_110101 10_101101 F4 8B B5 AD

2.2.4.1. 与字符字面值编码区别(difference from character literal encode)

字符字面值是Unicode码点,字符串字面值则是UTF-8编码。如本例和上例中,

'语'是字符,值是0x8BED,是字符的Unicode码点,类型是comptime_int;

"语"是字符串,值是E8 AF AD 0,是字符的UTF-8编码,类型是*const [3:0]u8

"语"实质上等同于"\xE8\xAF\xAD"

run_diff_char_string_literal.zig

const mem = @import("std").mem;
const print=@import("std").std.debug.print;
pub fn main() void{
    print("{s} {}\n",.{"\xE8\xAF\xAD",@TypeOf("\xE8\xAF\xAD")});
    print("{X} {X} {X} {X}\n",.{"语"[0],"语"[1],"语"[2],"语"[3]});
    print("{}\n",.{mem.eql(u8,"语","\xE8\xAF\xAD")});
    const i='语';
    print("{X}\n",.{i});
}
shell

$ zig run run_diff_char_string_literal.zig
语 *const [3:0]u8
E8 AF AD 0
true
8BED

以十六进制方式查看本例源代码文件,'语' 实际保存为27 E8 AF AD 27

' 的UTF-8编码为0x27, 语 的UTF-8编码为0xE8AFAD。而本例i的值是0x8BED,这是 语 的unicode码点。

这说明使用字符字面值时,Zig语言编译时把源代码中UTF-8编码自动变成 unicode 码点。

2.2.4.2. 字符不等于元素(single character not equal to element)

对于ASCII字符来说,字符等于元素,因为其unicode码点值和UTF-8编码值相同;

对于非ASCII字符来说,字符不等于元素,因为其unicode码点值和UTF-8编码值不同。

test_single_char_elem.zig

const expect=@import("std").testing.expect;
test "single char equal to element" {
    const str="Zig语言";
    try expect(str[0]=='Z');
    try expect(str[2]=='g');
    try expect(str[3]!='语');
}

名词解释:

ASCII码(american standard code for information interchange) 美国信息交换标准代码是最早最通用的字符编码,编码范围0到127(0x7F)。

2.2.4.3. 逃逸序列(escape sequence)

以 \ 开头的表示逃逸序列,具体如下:


\n 10 0x0A LF(new line) 回车
\r 13 0x0D CR(carriage return) 换行
\t 09 0x09 HT(horizontal tab) 水平制表符
\\ 92 0x5C bask slash 反斜杠
\' 39 0x27 single quote 单引号
\" 34 0x22 double quote 双引号
\xNN 8位长的字节值(2个十六进制数字)
\u{NNNNNN} Unicode码点值(1个或多个十六进制数字)
run_string_escape.zig

const eql=@import("std").mem.eql;
const expect=@import("std").testing.expect;
test "escape sequence" {
    try expect(eql(u8,"hello","h\x65llo"));
    try expect(eql(u8,"--\x65--\u{8bed}--","--e--语--"));
}

名词解释:

逃逸序列(escape sequence) 为了能在字符串中表示制表符、换行等不能直观输入和显示的字符,用 \ 和其后的字符组合,转义为对应的控制字符。

2.2.4.4. 多行字符串字面值(multiline string literal)

多行字符串字面值的每一行用 \\ 开头,且多行字符串内的逃逸序列无效。

run_multiline_string_literals.zig

const print=@import("std").std.debug.print;
pub fn main() void{
const c =
    \\#include <stdio.h>
    \\
    \\int main(int argc, char **argv) {
    \\    printf("hello world\n");
    \\    return 0;
    \\}
;
print("{}",.{c});
}

TODO: 如果按Windows文本规范的结尾,其源代码的行以\r\n结尾,则多行字符串字面值编译时失败,行必须以\n结尾才能正确编译,需要打补丁。

2.3. 数组字面值(array literal)

可以用数组字面值给数组或切片类型赋初始值。

语法有两种如下,T是元素的类型,如果数组的长度是N,大括号中元素的数量也必须是N个。

[_]T{ e1, e2, ... en}

[N]T{ e1, e2, ... en}

当变量定义了具体的数组类型时,数组字面值可省略类型前缀,语法为:

.{ e1, e2, ... en}

run_array_literals.zig

const mem=@import("std").mem;
const assert=@import("std").debug.assert;
pub fn main() !void {
    const m=[_]u8{'h','e'};
    const m1="he";
comptime{
    assert(m.len==2);
    assert(mem.eql(u8,&m,m1));
}
    var m2=[4]u16{15,290,30,400};
    assert(m2[1]==290);
    var m3:[3]u32=.{1,2,3};
    assert(m3[1]==2);
}

2.4. 结构字面值(struct literal)

可用结构字面值给结构类型的变量赋初始值。

结构字面值语法如下,其中sname是结构类型的名字,field 是属性的名字,v是初始值,

sname{.field1=v1, .field2=v2, ... .fieldn=vn}

当变量定义了具体的结构类型时,结构字面值可省略类型前缀,其语法为:

.{.field1=v1, .field2=v2, ... .fieldn=vn}

赋初始值时,如果有些属性值不立即赋具体值,可以设为undefined,稍后再赋值,如本例中的f3.y。

test_struct_literal.zig

const expect=@import("std").testing.expect;
const foo=struct{x:u8, y:u64, var z:u64=5};
test "struct literal" {
    const f1=foo{.x=1, .y=260,};
    try expect(f1.y==260);
    const f2:foo=.{.x=2, .y=470};
    try expect(f2.x==2);
    var f3:foo=.{.x=3, .y=undefined};
    f3.y=50;
    try expect(f3.y==50);
}

注意:结构体字面值中,须包括结构的所有没有默认值的属性(但是不能包含结构内定义的静态变量),否则编译错误。

2.5. 元组(tuple)

如果待赋值的变量没有定义类型,或者待输入的函数参数定义是 anytype, 那么不带结构类型的结构字面值称为匿名列表字面值(anonymous list literal),又称为元组。

元组中各元素必须设定类型,元素间的类型可以相同,也可以不同,这点与结构类似。

因没有定义属性名字,所以元组的属性名是从零开始的数字序号,这点与数组的索引类似。

因为元组是编译期可知的常量,所以可以用 inline for 遍历。

test_anonymous_list_literal.zig

const expect=@import("std").testing.expect;
fn dump(args:anytype) !void {
    try expect(args.@"0"==1234);
    try expect(args.@"1"==12.34);
    try expect(args.@"2");
    try expect(args.@"3"[1]=='i');
}
test "anonymous list literal" {
    try dump(.{@as(u32,1234),@as(f64,12.34),true,"hi"});
}
test "tuple" {
    const args=(.{@as(u32,1234),@as(f64,12.34),true,"hi"});
    try expect(args[0]==1234);
    try expect(args[1]==12.34);
    try expect(args[2]==true);
    try expect(args[3][1]=='i');
}
test "inline for tuple"{
    const t=(.{@as(u32,1234),@as(f64,12.34),true,"hi"});
    inline for(t) |v,i|{
        if(i!=2) continue;
        try expect(v);
    }
}

2.6. 枚举字面值(enum literal)

可用枚举字面值给枚举类型的变量赋初始值。

枚举字面值语法为 ename.fieldx ,其中 ename 是枚举类型的名字,fieldx是 ename 中某个属性的名字。

当变量定义了具体类型时,枚举字面值可省略类型前缀,其语法为 .fieldx

test_enum_literal.zig

const expect=@import("std").testing.expect;
test "enum literal" {
    const foo=enum{ok,not_ok,};
    const f1=foo.ok;
    const f2:foo=.ok;
    try expect(f1==f2);
}

2.7. 联合字面值(union literal)

可用联合字面值给联合类型的变量赋初始值。

联合字面值语法为 uname{.fieldx=init} ,其中uname是联合类型的名字,fieldx是 uname 中某个属性的名字,init 是初始值。

当变量定义了具体类型时,联合字面值可省略类型前缀,语法为 .{.fieldx=init}

test_unicon_literal.zig

const expect=@import("std").testing.expect;
test "union literal" {
    const foo=union{i:i32,f:f64};
    const f1=foo{.i=15};
    try expect(f1.i==15);
    const f2:foo=.{.f=3.3};
    try expect(f2.f==3.3);
}

3. 语法单元(syntax unit)

变量和运算符构成表达式,表达式和运算符构成语句,语句构成语句块,语句块加参数列表加返回值类型构成函数,语句、函数构成源代码文件。

3.1. 名字空间(namespace)

变量名、类型名、属性名、函数名等可统称为符号。

名字空间可分为:全局名字空间、文件名字空间、函数名字空间、语句块名字空间、聚合类型名字空间。

聚合类型名字空间是指在结构、枚举、联合类型内定义的符号。

其中,全局名字空间是最上级,文件名字空间是次一级,这两者是其余3种名字空间的上级。

3.1.1. 标识符(identifier)

标识符不能是Zig语言关键字。

普通标识符必须以ASCII英文字母或下划线开始,后面可以有任意数量的ASCII英文字母、ASCII数字、下划线字符。

标识符中不能直接使用非ASCII字符。如果要用不符合要求的变量名,例如与外部库链接或使用 Unicode 字符,用@""语法。

本例中,C语言error函数的名字是Zig语言的关键字,最大月份值不是ASCII英文字母,所以要用@""语法。

test_id.zig

const c = @import("std").c;
const expectEqual=@import("std").testing.expectEqual;
pub extern "c" fn @"error"() void;
test "ID"{
    const i:i8=10;
    const @"最大月份值"=i+2; // unicode汉字作为标识符。
    try expectEqual(12,@"最大月份值");
}

3.1.2. 全局名字空间(global namespace)

软件通常是由多个源代码文件构建而成,多个源代码文件的 pub 符号构成全局名字空间。

当前文件的符号定义时加 pub 修饰符,表示其它文件可以用 @import导入使用。

本例中,pubfile.zig中的foo.x, fn2可供test_pub.zig使用。

pubfile.zig

pub const foo=struct{pub const x:i32=15; const y:i32=17;};
fn fn1() void{}
pub fn fn2() void{}
const z:i64=18;
test_pub.zig

const expect=@import("std").testing.expect;
const testp=@import("pubfile.zig");
test "test pub" {
    try expect(testp.foo.x==15);
    testp.fn2();
    // try expect(testp.foo.y==17);
    // testp.fn1();
    // try expect(testp.foo.z==18);
}

把最后3行的注释去掉任意1行,编译出现如下类似错误并中止。

error: 'y' is not marked 'pub'

与C语言交互时, extern 引入外部符号, export 导出本文件符号供其它文件使用。

3.1.3. 文件名字空间(file namespace)

直接在源代码文件中定义,没有在函数、类型和语句块内部定义的所有符号,称为文件名字空间。本例中,s1, i, foo, 属于文件名字空间。 x, y, k, j不属于。

test_filens.zig

const s1=struct{x:i32,const y:i64=15;};
const i:i32=100;
fn foo() void{
    const k:i8=0; _=k;
}
test "filens" {
    var j:i32=16; _=j;
}

3.1.4. 其它名字空间(other namespace)

本函数内不属于全局名字空间和文件名字空间的符号,且不是聚合类型中的属性名的符号,称为函数名字空间。

函数名字空间包括其中所有下级聚合类型名字空间、块名字空间的符号。

本例中,i, j, s, s1, z 均是foo函数名字空间的符号, x, y是 s 的属性,不属于foo函数名字空间;

test_otherns.zig

fn foo() void{
    const i:i8=0;
    comptime {
        const j:i32=1; _=j;
    }
    const s=struct{x:i64,y:i64,const z:i32=1;};
    const s1=s{.x=1,.y=2}; _=s1;
}
fn foo1() void{
    var i:bool=true;
    if(i){
        const s=struct{};
        const j:i32=17;
        if(j>0){
            const k=j+5;
        }
    }
}
const temp:i32=10;

本语句块内不属于全局名字空间和文件名字空间的符号,且不是聚合类型中的属性名的符号,称为块名字空间。块名字空间包括所有下级块的符号。

本例中,if(j>0)语句块名字空间包含有 k, if(i)语句块名字空间包含有属于自己的符号 s, j, 和下级语句块的符号 k

在结构、枚举、联合等聚合类型内定义的变量名,和其自身属性名,属于聚合类型名字空间。

本例中,s 名字空间下有 x,y,z 符号; s1 名字空间下有 x,y 符号。

因s 和 s1 没有上下级关系,所以可以重复使用 x,y 符号。

3.1.5. 隐藏(shadow)

Zig语言不允许符号名隐藏,即同一个名字空间内的符号不能一样,不同名字空间的上下级符号名不能一样。

本例中,test语句块名字空间的 i 与 文件名字空间 i 属于上下级关系,所以编译出错中止。

test_shadow.zig

var i:i32=5;
test "inside blocks"{
    const j:i8=1;
    var i:i8=10+j; _=i;
}
shell

$ zig test test_shadow.zig
test_shadow.zig:4:6: error: local shadows declaration of 'i'
 var i:i8=10+j;
     ^
test_shadow.zig:1:1: note: declared here
var i:i32=5;
^~~~~~~~~~~

不允许符号隐藏,这样更易于理解和分析代码,减少bug产生。

如果名字空间没有上下级关系,则符号名可以重复。

test_shadow1.zig

test "can same ID" {
    {
        const pi=3.14; _=pi;
    }
    {
        var pi:bool=true; _=pi;
    }
}

3.1.6. 导入名字空间(usingnamespace)

usingnamespace 将其操作数所有公开的定义,汇集到当前名字空间中。操作数类型必须是结构、枚举、联合和opaque 。

test_usingnamespace.zig

test "using std namespace" {
    const S=struct {
        usingnamespace @import("std");
    };
    try S.testing.expect(true);
}

可以用 pub 修饰 usingnamespace 。

usingnamespace在组织文件或包公开的API有重要作用。例如,可以用下面代码来导入用到的C语言符号。

usingnsc.zig

pub usingnamespace @cImport({
    @cInclude("epoxy/gl.h");
    @cInclude("GLFW/glfw3.h");
    @cDefine("STBI_ONLY_PNG", "");
    @cDefine("STBI_NO_STDIO", "");
    @cInclude("stb_image.h");
});

3.2. 变量和赋值(variable and assignment)

变量实质上是1块符合定义类型的内存单元。

所有的变量有6个要素:名字、类型、值、地址、占用内存字节长度、对齐。

类型确定了变量的运算规则、占用内存字节长度、默认对齐方式,所以类型是程序设计语言的核心内容之一。

赋给变量的值必须能转换为该变量的类型。

变量的地址不可修改。

3.2.1. 变量名字(variable name)

变量名字须符合3.1.1. 标识符(identifier)要求,且同一名字空间、上下级名字空间的变量名字不能一样,即不能3.1.5. 隐藏(shadow)

3.2.2. 常量和变量(const and var)

变量定义赋初始值后,其值不能修改的称之为常量 const ,可以修改的称之为变量 var 。

定义变量时,尽可能用 const , 这样不容易有 bug ,且易于优化。

如果改变常量的值,则编译出错中止。

test_change_const.zig

const k:i32=1;
k+=1;

本例编译出错中止,并输出如下信息。

error: cannot assign to constant

3.2.3. 变量类型(variable type)

变量必须有类型,不存在没有类型的变量。通常在定义时须获知变量的类型,否则编译出错中止。

类型定义语法为:

const name:T=v; var name:T=v;

const name=vT; var name=vT;

定义变量时:

本例中,变量y的类型可以根据x推导出是i32,所以y的类型定义可以省略。

test_var_type.zig

const expect=@import("std").debug.expect;
test "variable type" {
    // var k=5;
    const x:i32=15;
    var y=x+3;
    try expect(@TypeOf(y)=i32);
}

本例中,去掉注释,则var k没有明确的类型,会编译出错中止,输出信息为:

error: variable of type 'comptime_int' must be const or comptime

3.2.4. undefined

test_undefined.zig

test "undefined" {
    // const k;
    var x:i32=undefined;
    x=3;
    _=x;
}

本例中,如果把注释去掉,则编译出错中止,输出信息为:

error: variables must be initialized

调试模式下,用字节值0xAA填充到初始值为 undefined 变量的对应内存,这样有助于调试器检测未定义内存。

3.2.5. 必须使用变量(must used variable)

变量定义后必须要使用,如果始终不使用则编译出错中止。

可以用 _ = expr; 忽略表达式 expr 的运算结果。

_ 字符可以理解为垃圾筒变量,类似于/dev/null

test "ignore result" {

    const i:i32=5;
    // _=i;
}

本例编译时出错中止,错误信息为:

error: unused local constant

把注释符删掉,则测试通过。

3.3. 运算符(operator)

运算符根据需要操作数的数量,可分为单目运算符、双目运算符。

例如:-a 对a取相反数运算,其中 - 是单目运算符; a-b 是减法运算,其中 - 是双目运算符。

Zig语言中运算符没有重载功能。

名词解释:

重载(:overload) 指多个函数名字一样(或是同一个运算符),但参数的数量或类型不一样,叫重载。

3.3.1. 优先级(precedence)

运算符有优先级,优先级高的先运算。例如,乘法优先级比加法高,所以:

1+5*3==1+15==16

而不是

1+5*3!=6*3!=18

() 包围的表达式被看作是一个表达式,所以可以改变优先级:

(1+5)*3==6*3==18

运算符的优先级见下表,第1行优先级最高,第12行优先级最低。同一行的优先级一样。


1  x() x[] x.y x.* x.?
2  a!b
3  x{}
4  !x -x -%x ~x &x ?x
5  * / % ** *% *| ||
6  + - ++ +% -% +| -|
7  << >> <<|
8  & ^ | orelse catch
9  == != < > <= >=
10 and
11 or
12 = *= *%= *|= /= %= += +%= +|= -= -%= -|= <<= <<|= >>= &= ^= |=

3.4. 表达式和语句(expression and statement)

表达式是符合语法规范的符号和运算符的组合式子,表达式有结果值。

语句是符合语法规范的运行单元,语句没有结果值。

定义语句、赋值语句等语句,行尾需要加 ; 。

} 结尾的流程控制语句和语句块行尾不加 ; 。

下例中,if(i>10) 是if表达式,其结果值赋给常量i;if(j)是语句,语句块内运行expect函数语句。

test_expr_stat.zig

const expect=@import("std").testing.expect;
test "expr and stat" {
    const i:i32=5;
    const j= if(i>10) true else false;
    if(j){
        try expect(j);
    }
}

3.5. 块(block)

内有0条或多条语句组成,用{ } 括起来的语法单元,称为语句块。

语句块可以嵌套,即内部还可以有语句块。

语句块也有值,普通语句块的值是 void,通常可以用break语句跳出带标签的语句块,给语句块赋值。

把语句块的值赋给其它变量,语句块的 } 后要加 ;

语句块内的语句,按书写的先后顺序运行,如果有break等跳转语句,按语句定义运行。

test_block.zig

const expect=@import("std").testing.expect;
test "block nesting and values" {
    const i={
        const j:i32=0;
        {
            const k=j+1;
            _=k;
        }
    };
    try expect(@TypeOf(i)==void);
}
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);
}

3.6. 函数(function)

函数由函数名name、参数列表varlist、返回值类型result、函数体body、修饰符specifier组成。

specifier fn name(varlist) result body

test_func.zig

const expect=@import("std").testing.expect;
fn nop() void{}
fn add(a:i8,b:i8) i8{
    if (a==0) {return b;}
    return a+b;
}
test "functions"{
    const i=add(0,9);
    try expect(i==9);
    nop();
    const x:i8=10; const y:i8=20;
    try expect(add(x,y)==30);
}

3.6.1. 参数按值传递(pass-by-value parameter)

整数和浮点数等基本类型做参数时,在函数体内使用的是值的副本。这种情况基本只涉及到cpu中的寄存器复制,代价极小。

const k:i32=1;

fn foo(i:i32) void {j=i+1; _=j;}

foo(k);

foo(k) 运行时,把输入变量k的值传递给参数i,相当于在函数体内用的是k值而不是变量k本身。运行逻辑相当于:

(i=k;) {j=i+1; _=j;}

结构、枚举、联合等聚合类型做参数时,因为聚合类型的字节长可能会很大,所以复制代价可能会高,这种情况下引用传递(传送地址)代价更小。Zig语言根据所花代价来选择按值传递还是按引用传递。

对于extern函数,Zig语言按照C语言ABI接口标准,按值传递结构和联合。

注意:不管按值传递还是按引用传递,在函数体内均不能改变参数的值。

test_change_para.zig

fn test1(i:i32)void{
    i+=1;
}
fn test2(p:*i32)void{
    p.*+=10;
    var i:i32=1;
    p=&i;
}
test "change parameter"{
    var i:i32=0;
    test1(i);
    test2(&i);
}
shell

$ zig test test_change_para.zig
zig test test_change_para.zig
test_change_para.zig:5:3: error: cannot assign to constant
 i+=1;
 ~^~~
test_change_para.zig:10:4: error: cannot assign to constant
 p=&i;
   ^~

3.6.2. 函数参数类型推导(function parameter type inference)

函数参数用anytype类型定义时,在函数被调用时根据输入变量的类型,来推导出参数的类型。

test_func_para_infer.zig

const expect=@import("std").testing.expect;
fn add2(x:anytype) @TypeOf(x){
    return x+42;
}
test "fn type inference" {
    try expect(add2(1)==43);
    try expect(@TypeOf(add2(1))==comptime_int);
    var y:i64=2;
    try expect(add2(y)==44);
    try expect(@TypeOf(add2(y))==i64);
}

3.6.3. 修饰符(specifier)

export修饰符使函数在生成的obj文件中外部可见,并且用C语言ABI接口,参看本例中sub函数;

extern修饰符定义该函数是外部定义,定义在链接时静态库里,或运行时动态库里,参看ExitProcess和atan2函数;

pub修饰符允许函数外部可见,其它文件能用@import函数导入并调用,参看sub2函数;

callconv修饰符改变函数的调用约定:

.Naked 使函数没有附加的前导和后续,通常用于与汇编集成,参看_start函数;

.Inline 强制在该函数的所有调用处内联,如果函数不能内联,则产生编译错误,参看shiftl1函数;

@setCold 告知优化器该函数很少被调用,参看abort函数。

test_func_specifier.zig

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==.i386) .Stdcall else .C;
extern "kernel32" fn ExitProcess(exit_code: u32) callconv(WINAPI) noreturn;
extern "c" fn atan2(a: f64, b: f64) f64;

export fn sub(a:i8, b:i8) i8{
    return a-b;
}
pub fn sub2(a:i8,b:i8) i8{
    return a - b;
}
fn _start() callconv(.Naked) noreturn {
    abort();
}
fn shiftl1(a:u32) callconv(.Inline) u32{
    return a<<1;
}
fn abort() noreturn {
    @setCold(true);
    while (true) {}
}
test "function specifier"{
    try expect(true);
}

3.6.4. @call

@call(options:std.builtin.CallOptions,function:anytype,args:anytype) anytype

调用函数,与使用括号调用表达式的方式相同。

test_call.zig

const expect = @import("std").testing.expect;
test "noinline function call" {
    try expect(@call(.{},add,.{3, 9})==12);
}
fn add(a: i32, b: i32) i32 {
    return a+b;
}

通过设置参数CallOptions,可以比普通函数调用有更多的灵活性。

stack: 仅当modifier==Modifier.async_kw时有效;

auto: 等效于普通函数调用

async_kw: 等效于用async关键字的函数调用

never_tail: 防止尾调用优化,这样保证返回地址指向调用点,而不是调用点的调用点。如果是尾调用优化或内联,则产生编译错误

never_inline: 保证调用不会内联。如果是内联,则产生编译错误

no_async: 断言函数调用不会挂起。这样允许非异步函数调用异步函数

always_tail: 保证调用是尾优化的,如果不能则产生编译错误

always_inline: 保证在调用点内联函数,如果不能则产生编译错误

compile-time: 在编译时计算调用。如果不能则产生编译错误

builtin.CallOptions

pub const CallOptions = struct {
    modifier: Modifier = .auto,
    stack: ?[]align(std.Target.stack_align) u8 = null;
    pub const Modifier = enum {
        auto,
        async_kw,
        never_tail,
        never_inline,
        no_async,
        always_tail,
        always_inline,
        compile-time,
    };
};

3.6.5. @setCold

@setCold(comptime is_cold:bool)

告知编译器本函数很少被调用。

4. 变量有效性(variable validity)

名字空间、作用域、生命周期、所有权这四者决定了变量是否有效。

4.1. 作用域(scope)

作用域是指符号(包括普通变量、类型定义、函数定义等)在程序运行期间的有效使用区域。

不能在某作用域之外,使用此作用域的符号。

注意:离开作用域不绝对等同于变量已失效,只是不能使用。

作用域与名字空间类似,可分为全局作用域、文件作用域、函数作用域、块作用域。

结构等聚合类型名字空间不是作用域。

全局作用域是指符号在整个应用程序范围内有效,类似于C语言中的全局变量。

文件作用域是指符号在本源代码文件内有效,类似于在C语言文件下直接用static 声明的变量和函数。

函数作用域是指符号在本函数内有效,块作用域是指符号在本块内有效。

与名字空间类似,有上下级关系的作用域,下级作用域可以使用上级作用域符号,上级作用域不可以使用下级作用域符号。

如下例中,x在下级(也就是y所在的)语句块有效。

x在上级语句块无效,即 最后一行语句x=5; 编译出错。

run_scope.zig

const print=@import("std").debug.print;
pub fn main() void{
    {
        var x:i32=1;
        {
            var y=x+2; _=y;
        }
        x+=5; print("{}\n",.{x});
    }
    x=5;
}
shell

$zig build-exe run_scope.zig
hello.zig:12:2: error: use of undeclared identifier 'x'
 x=5;
 ^

4.1.1. defer

当程序运行流是离开当前作用域(通常是代码块或函数)时,如果有defer语句,则运行该语句。

test_defer1.zig

const expect=@import("std").testing.expect;
fn defer1() !i32{
    var a:i32=1;
    {
        defer a=2;
        a=1;
    }
    try expect(a==2);
    a = 5;
    return a;
}
test "defer basics" {
    try expect(try defer1()==5);
}

同一个作用域有多个defer语句时,按定义的反向顺序运行,先定义的后运行,后定义的先运行。

不运行的语句块内的defer语句,不会运行。见例子中的defer3。

run_multiple_defer.zig

const print=@import("std").debug.print;
fn deferUnwind() void{
    defer {
        print("defer1 ", .{});
    }
    defer {
        print("defer2 ", .{});
    }
    if (false) {
        defer {
            print("defer3 ", .{});
        }
    }
}
pub fn main() void{
    deferUnwind(); // defer2 defer1
}

defer语句块中不能有return语句,否则编译出错。

test_defer_return.zig

fn defreturn(i:*i32) bool{
    i.* +=1;
    defer return true;
}
shell

error: cannot return from defer expression

4.1.2. errdefer

当离开当前作用域出错时,如果有errdefer语句,则运行该语句,主要用于清理错误现场,类似于C语言中的goto语言。

通常内存分配代码后,紧跟用含有内存释放代码的errdefer语句。这样错误处理更健壮,而不必为了确保覆盖每个退出路径编写冗长的代码。

errdefer还支持捕获操作,可以在清理错误现场过程中,记录错误信息。

run_errdefer.zig

const print=@import("std").debug.print;
fn deferError(i:bool) !void {
    print("{}: start of function\n", .{i});
    defer {
        print("{}: end of function\n", .{i});
    }
    errdefer {
        print("{}: encountered an error!\n", .{i});
    }
    if (i) {
        return error.DeferError;
    }
}
fn deferErrorCap() !void {
    errdefer |e| {
        print("the error is {s}\n", .{@errorName(e)});
    }
    return error.DeferError;
}
pub fn main() void{
    deferError(false) catch {};
    deferError(true) catch {};
    deferErrorCap() catch {};
}
shell

false: start of function
false: end of function
true: start of function
true: encountered an error!
true: end of function
the error is DeferError

4.1.2.1. 常见的errdefer错误用法(common errdefer slip-ups)

errdefer语句只持续在块末尾,如果在块外返回错误,则不会执行errdefer。

下面当if语句返回错误时,已不在errdefer定义的块范围内,所以errdefer不会执行,有泄漏风险。

test_errdefer_leak.zig

const std=@import("std");
const Alloc=std.mem.Allocator;
const talloc=std.testing.allocator;
const expectError=std.testing.expectError;
const foo=struct{i:i32};
fn creatfoo(a:Alloc,i:i32) !*foo{
    const f= getf: {
        var f=try a.create(foo);
        errdefer a.destroy(f);
        f.i=15;
        break :getf f;
    };
    //errdefer a.destroy(f);
    if(i>1000) return error.invalid;
    return f;
}
test "errdefer leap"{
    try expectError(error.invalid,creatfoo(talloc,1001));
}
shell

$ zig test test_errdefer_leak.zig
Test [1/1] test.errdefer leap... [gpa] (err): memory address 0x23b060c0000 leaked:
All 1 tests passed.
1 errors were logged.
1 tests leaked memory.

上例中,把if语句前的注释去掉,则测试通过。

4.2. 生命周期(lifetime)

可以保留变量值有效的时间称为变量的生命周期。

通常变量的生命周期全过程有定义、使用、失效:

生命周期可分为静态生命周期和动态生命周期。静态生命周期是指在程序运行期间一直有效的变量,动态生命周期是在程序运行期间不一定一直有效的变量。

4.2.1. 局部变量(local variable)

局部变量是指生命周期仅在本函数或本语句块或 @import 块内有效的变量。局部变量是动态生命周期。

局部变量的名字空间、作用域、生存周期这三者是一致的。

本例中,j离开语句块后,不可再使用,所以编译出错中止。

test_local_var.zig

const expect=@import("std").testing.expect;
test "local var" {
    var i:i32=5;
    {
        var j:i32=10;
        try expect(i!=j);
    }
    i=j;
    try expect(i==j);
}
shell

$ zig test test_local_var.zig
test_local_var.zig:8:4: error: use of undeclared identifier 'j'
 i=j;
   ^

4.2.1.1. 编译期局部变量(comptime local variable)

局部变量可以定义在comptime块内,或用comptime关键字来修饰。这样该变量的值是编译期可知的,并且该变量的所有读取和写入都发生在程序编译过程中,而不是在运行时。在comptime表达式中定义的所有变量都是comptime变量。

本例中,因为y是编译期变量,y!=2 也是1个编译期变量,其值为false,所以编译时解析if(y!=2)不会运行,就不会触发@compileError,则测试通过。

test_comptime_var.zig

const expect=@import("std").testing.expect;
test "comptime var" {
    comptime var y:i32=1;
    y+=1;
    try expect(y==2);
    if (y!=2) {
        @compileError("wrong y value");
    }
}

本例中,因x的值是运行期可知,编译期不可知,编译期不能求得 x!=2 的值,不能解析出 if(x!=2) 肯定不会运行,所以需要解析到if语句内,导致触发@compileError,编译中止。

test_not_comptime_var.zig

const expect=@import("std").testing.expect;
test "not comptime local var" {
    var x:i32=1;
    x+=1;
    try expect(x==2);
    if (x!=2) {
        @compileError("wrong x value");
    }
}

编译时出错,输出信息为:

error: wrong x value

4.2.2. 静态变量(static variable)

静态变量是指在程序运行期间一直有效的变量,静态变量编译时可以延迟解析,所以使用和定义前后顺序无关,即使用语句可以写在定义语句之前。在Zig语言中,静态变量又被称为容器级变量(container level variable)。

4.2.2.1. 全局变量(global variable)

全局名字空间的符号(即定义时带 pub 或export 的)是全局变量。

全局变量有静态生命周期。

本例中,i在j之后定义,但j仍可以使用i,因为i是静态变量。

global_var.zig

pub const j=i+3;
const i:i32=1;
pub fn foo() void {}
test_global_var.zig

const expect=@import("std").testing.expect;
const gvar=@import("global_var.zig");
test "global var"{
    try expect(gvar.j==4);
    gvar.foo();
}

4.2.2.2. 文件级变量(file level variable)

直接在源代码文件中定义的,没有在函数定义、类型定义、语句块内部的所有不带 pub 或 export 的变量,称之为文件级变量。

文件级变量有静态生命周期,属于文件名字空间和文件级作用域。

本例中,i, add1, add2的定义顺序不影响正常使用。且变量 x 在运行期间一直有效,值由1变为2再变为4。

test_file_level_var.zig

const expect=@import("std").testing.expect;
test "file level var" {
    try expect(add1()==2);
    try expect(x==2);
    try expect(add2()==4);
    try expect(x==4);
}
fn add1() i32 {x+=1; return x;}
fn add2() i32 {x+=2; return x;}
var x:i32=1;

4.2.2.3. 聚合类型内变量(variable within the aggregation type)

结构、枚举、联合内定义的变量是聚合类型变量。

聚合类型变量有静态生命周期,属于聚合类型名字空间。

其作用域归属于聚合类型定义语句所在位置,不在函数定义、语句块内的属于文件级作用域;否则属于函数或块作用域。

文件级作用域的聚合类型内变量见本例。

test_var_within_aggregation.zig

const expect = @import("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;
}

4.2.2.4. 静态局部变量(static local varable)

函数或块作用域的聚合类型变量又称之为静态局部变量。

静态局部变量有静态生命周期,其作用域属于函数作用域或块作用域。

test_static_local.zig

const expect = @import("std").testing.expect;
test "static local variable" {
    try expect(foo()==1235);
    try expect(foo()==1236);
    try expect(foo1()==1235);
    try expect(foo1()==1235);
}
fn foo() i32 {
    const S=struct {
        var x:i32=1234;
    };
    S.x+=1;
    return S.x;
}
fn foo1() i32 {
    var x:i32=1234;
    x+=1;
    return x;
}

请仔细比较本例中foo和foo1的不同,可以深刻理解静态生命周期。

foo调用后,x的修改后的值被保持,说明foo函数中的x是在程序运行期一直有效,是静态生命周期;

每次调用foo1,返回值总是x+1==1235,说明foo函数中的x是局部变量,退出函数作用域则x失效。

两次调用foo1,运行流程相当于:


{var x1:i32=1234; x1+=1; return x1};
{var x2:i32=1234; x1+=2; return x2};

两次调用foo,运行流程相当于:


{const S=struct{var x:i32=1234;};
    S.x+=1; return S.x};
{S.x+=1; return S.x};

4.2.3. 外部变量(extern variable)

extern 关键字或 @extern 内置函数,可以链接从其它文件或库导出的变量。

extern 变量的类型必须是C语言ABI兼容类型。

4.2.4. 线程局部变量(thread local variable)

用threadlocal关键字可定义线程局部变量。线程局部变量不能用const定义。

如果用单线程方式构建,所有线程局部变量视为普通静态变量。

test_tls.zig

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);
}

4.2.5. 变量保存位置(the variable is saved where)

字符串字面值存储在全局常量数据段,所以可转换为不可变切片,但不能转换为可变切片。

const定义的变量如果值是编译期可知,以及编译期可知的变量,也存储在全局常量数据段中。

函数中用var定义的变量存储在函数的栈帧中。当函数返回时,任何指向栈帧中变量的指针会变成无效引用,且此时解引用是未定义行为。

用var定义的静态变量,存储在全局数据段。

用allocator.alloc或allocator.create分配的内存位置,由分配器的实现确定。

4.3. 所有权(ownership)

变量定义是指把某块内存单元与变量的名字关联起来,也就是说,这块内存单元目前归该变量所有。

变量失效是指再也不能通过该变量名访问到其对应的内存单元,也就是说,这块内存单元此时已不归该变量所有。

在生命周期之外,该块内存单元的所有权回归程序后,或空闲或再次分配给其它变量。

如果没有指针的话(这从本质上说,如果变量没有地址要素),则在变量生命周期内,该块内存单元的所有权只有1个,就是这个变量。

是这样的话,那一切都是这么简单轻松和美好。

但这是白日做梦。

引入指针后,如果把变量的地址赋给指针变量,则该指针变量也可以修改该块内存单元的值,也可以通过free等调用释放该块内存单元所有权。此时,该块内存单元的所有权到底是属于原变量呢?还是属于指针变量呢?这是巨多烦恼和BUG的根源。

有3种应对方法如下,总而言之,鱼与熊掌不可兼得,这也是有巨多程序设计语言的主要原因之一:

如果有兴趣,或许可以通过读Zig语言源代码和标准库中的源代码,来深入理解所有权和生命周期。

4.3.1. API文档的所有权部分(ownership section of API document)

程序员的责任是确保不访问指向内存不可用时的指针,切片是指针因为引用其它内存。

为防止bug,在处理指针时需要遵循一些有用的约定。通常当一个函数返回指针时,文档应该说明谁“拥有”这个指针。这有助程序员决定什么时候释放指针是合适的。

例如,函数文档可能会注明“调用方拥有返回的内存”,这时调用函数的代码必须有释放内存的代码。在这种情况下,函数可能会有Allocator参数。

有时指针的生存期可能更复杂。例如,在下一次调整列表大小(如追加新元素)之前,std.ArrayList(T).items切片生命周期保持有效。

函数和数据结构的API文档应该非常注意解释指针的所有权和生命周期语义。所有权决定了谁负责释放指针引用的内存,生命周期决定了内存无法访问的时间点,以免出现未定义行为。

4.4. 对齐(alignment)

内存的基本单位是为字节,1个字节共8个比特位。

通常可以理解为,当变量的内存地址可以被该变量所属类型的字节长整除时,则该变量是对齐的。

基本类型变量如果对齐的话,可以在一个CPU的读周期就可以读出;不对齐的话,可能就得两个CPU的读周期,极端的CPU会异常退出。这就是对齐也是变量基本属性的原因之一。

4.4.1. 对齐值(alignment value)

对齐值与CPU体系结构有关。

对齐值始终是2的次方,且小于0x1000_0000(1<<29)。

64位平台的指针对齐值为8,32位平台的指针对齐值为4。

变量的对齐值等于对应类型的字节长。

test_align_cpu.zig

const expect=@import("std").testing.expect;
test "align value" {
    if (@import("builtin").target.cpu.arch == .x86_64) {
        try expect(4==@typeInfo(*i32).Pointer.alignment);
        try expect(8==@alignOf(*i32));
        try expect(8==@alignOf(*u8));
        try expect(4==@alignOf(i32));
        try expect(1==@alignOf(u8));
        var x:i64=1;
        try expect(8==@alignOf(@TypeOf(x)));
    }
}

4.4.2. 指定对齐值(set alignment value)

可以用 align(N) 指定变量和函数的对齐值,指向这些变量指针的对齐值属性与指定值相同。

run_set_align_value.zig

const expect=@import("std").testing.expect;
fn noop() align(@sizeOf(usize)*2) void {}
test "set align" {
    var i:u8 align(4)=100;
    const j=&i;
    var m:u8=200; const n=&m;
    try expect(@TypeOf(j)==*align(4) u8);
    try expect(@TypeOf(n)==*u8);
    try expect(1==@alignOf(u8));
    try expect(@TypeOf(j)!=@TypeOf(n));
    try expect(@alignOf(@TypeOf(noop))==@alignOf(usize)*2);
    noop();
}

4.4.2.1. 设置结构属性对齐(set alignment of struct field)

可设置结构属性的对齐值。

test_struct_fields_align.zig

const expectEqual=@import("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));
}

4.4.3. 指针对齐(pointer alignment)

4.4.3.1. 省略指针对齐值(Omit pointer alignment value)

当指针指向类型的对齐值等于该类型的对齐值时,则在指针类型声明里可以省略对齐值。

test_omit_ptr_align.zig

const expect=@import("std").testing.expect;
test "omit ptr align" {
    var i:u16=1; var j:*u16=&i;
    const a=@alignOf(@TypeOf(i));
    try expect(a==2);
    try expect(*u16==*align(a) u16);
    try expect(@TypeOf(j)==*align(a) u16);
}

4.4.3.2. 指针对齐隐式转换(pointer assignment implicitly cast)

可以将对齐值较大的指针隐式转换为对齐值较小的指针,反之则不行。

本例中,ptr的类型是 *align(4) u8 ,可隐式转换为ptr1的类型 *u8

run_alignimplcast.zig

const print=@import("std").debug.print;
pub fn main() void{
    var x:u8 align(4) =100;
    var ptr=&x;
    print("{}\n",.{@TypeOf(ptr)}); // *align(4) u8
    var ptr1:*u8=ptr;
    print("{} {}\n",.{@TypeOf(ptr1),ptr1.*}); // *u8 100
}

4.4.4. @alignCast

@alignCast(comptime alignment:u29,ptr:anytype) anytype

操作数类型:指针或切片

说明:ptr可以是 *T, ?*T, []T 。返回值和ptr相同类型,返回值的对齐值调整到新值。

如果指针指向类型(或切片元素类型)的对齐值较小,但可以安全的转换为对齐值更大的类型,那么可用@alignCast可将指针或切片安全的改为对齐值更大的指针或切片。

这个步骤运行时是无额外操作的,但会有安全检查,如不通过则产生未定义行为。

示例:

test_alignCast.zig

const mem=@import("std").mem;
const expect=@import("std").testing.expect;
test "safe alignCast"{
    var a=[_]u32{0x11111111,0x22222222};
    const bytes=mem.sliceAsBytes(a[0..]);
    const s=bytes[0..4];
    const su=mem.bytesAsSlice(u32,@alignCast(4,s));
    try expect(su[0]==0x11111111);
}    
test "UB alignCast"{
    var a=[_]u32{0x11111111,0x22222222};
    const bytes=mem.sliceAsBytes(a[0..]);
    const s=bytes[1..5];
    const su=mem.bytesAsSlice(u32,@alignCast(4,s));
    try expect(su[0]==0x22111111);
    print("\n{x}\n",.{su[0]});
}
shell

$ zig test --test-filter "safe alignCast" test_alignCast.zig
All 1 tests passed.
$ zig test --test-filter "UB alignCast" test_alignCast.zig
Test [1/1] test.UB alignCast... thread 5304 panic: incorrect alignment

本例可通过,是因为此时a是文件级静态变量,而上例中的a是局部变量。

test_alignCast1.zig

const mem=@import("std").mem;
const expect=@import("std").testing.expect;
var a=[_]u32{0x11111111,0x22222222};
const bytes=mem.sliceAsBytes(a[0..]);
test "safe alignCast"{
    const s=bytes[0..4];
    const su=mem.bytesAsSlice(u32,@alignCast(4,s));
    try expect(su[0]==0x11111111);
}    
test "UB alignCast"{
    const s=bytes[1..5];
    const su=mem.bytesAsSlice(u32,@alignCast(4,s));
    try expect(su[0]==0x22111111);
    print("\n{x}\n",.{su[0]});
}
shell

$ zig test test_alignCast1.zig
All 2 tests passed.

4.4.5. @setAlignStack

@setAlignStack(comptime alignment:u29)

说明:确保函数的栈对齐至少为alignment个字节。

5. 整数、浮点数和布尔类型(integer and float and bool)

整数类型参见2.1.1. 整数(integer)

浮点数类型参见2.1.2. 浮点数(float)

5.1. 最大最小值(maximum)

可以用 std.math.floatMax, floatMin, maxInt, minInt函数,取得不同类型整数和浮点数的最大值最小值。

也可以直接用std.math.f16_max f16_min f32_max f32_min f64_max f64_min fl128_max, fl128_min 常量的值,取得相应类型的浮点数的最大最小值。

run_intfloat_maximum.zig

const print=@import("std").debug.print;
const math=@import("std").math;
pub fn main() void {
    print("{} {}\n",.{math.floatMax(f32),math.floatMin(f32)});
    print("{} {}\n",.{math.f32_max,math.f32_min});
    print("{} {}\n",.{math.maxInt(i8),math.minInt(i8)});
    print("{} {}\n",.{math.maxInt(u8),math.minInt(u8)});
    print("{} {}\n",.{math.maxInt(u3),math.minInt(u3)});
    print("{} {}\n",.{math.maxInt(i3),math.minInt(i3)});
}

3.40282346e+38 1.17549435e-38
3.4028234663852886e+38 1.1754943508222875e-38
127 -128
255 0
7 0
3 -4

5.2. 整数溢出(integer overflow)

在计算机中,零和正数用原码表示,负数用补码表示,所以0和正数的最高比特位是0,负数的最高比特位是1。

补码的运算规则是,负数取绝对值后正数的原码,按位取反后加1。

所以根据补码求原码的逆运算是,减1后再按位取反。

以1个字节(8个比特位)为例:

对无符号整数u8(即没有负数),则8比特位都是原码,所以,

最小值 0b0000_0000 对应的是原码0,最大值 0b1111_1111 对应的是原码255。

如果255+1=0b1111_1111+1=0b1_0000_0000。因为u8只有8个比特位,所以截断多出的比特位后,变成 0b0000_0000,则255+1==0。明显错误,这种错误就叫做溢出,再精确些叫上溢。

对于有符号整数i8,情况较复杂,考虑4个数:

0b0000_0000 对应的是0,0b0111_1111 对应的是正数127,

0b1111_1111 是补码,求原码得 0b0000_0001,所以 0b1111_1111 对应的是 -1。

那最大负数怎么求? 0b1111_1111-0b0111_1111=0b1000_0000,换算成十进制整数,则表示-1-127=-128

如果-128-1=0b1000_0000-1=0b0111_1111,则-128-1=127。明显错误,这种错误也叫做溢出,再精确些叫下溢。

其实正确的-128-1是这样的,把类型提升到i16后,-128-1=-128+(-1)=0b1111_1111_1000_0000+0b1111_1111_1111_1111=0b1_1111_1111_0111_1111,截断多出的最高比特位1,得0b1111_1111_0111_1111,再通过补码求原码,则原码是:0b1000_0001,对应十进制整数为129,所以-128-1=-129,计算结果正确。

所以对于结果超出最大最小值范围的,须对操作数进行整数类型提升才可以得出正确结果。

下面再考虑有符号整数i8求相反数:

求相反数相当于对零和正数是原码求补码,对负数是补码求原码。

0的相反数为:0b0000_0000取反得0b1111_1111,加1得0b1_0000_0000,截断最高比特位1得0b0000_0000,所以0的相反数是0;

1的相反数为:0b0000_0001取反得0b1111_1110,加1得0b1111_1111,所以1的相反数是-1;

-127相反数为:0b1000_0001减1得0b1000_0000,取反得0b0111_1111,所以-127的相反数是127。

-128相反数为:0b1000_0000减1得0b0111_1111,取反得0b1000_0000,所以-128的相反数还是-128?明显错误,这是因为发生了溢出。

正确的-128求相反数是把类型提升为i16后,0b1111_1111_1000_0000减1得0b1111_1111_0111_1111,取反得0b1000_0000_1000_0000,在i16类型下,此二进制表示128,所以结果正确。

名词解释:

符号位(:symbol bit) 用固定数量的比特表示整数,左面最高1位为符号位,0表示正数,1表示负数。

补码(:complement) 整数的二进制表示中,原码是和数值一样,反码是原码的按位取反,补码是反码加1。例如:5在8比特位整数中,原码是 0b0000_0101 ;反码是 0b1111_1010 ,即 0xFA ;补码是 0b1111_1011 ,即 0xFB 。

用补码来表示相反数,是为了简化减法运算,即把减法运算变为和相反数相加。如:7-5=7+(-5)=0b0000_0111 + 0b1111_1011 = 0b1_0000_0010 = 2

5.2.1. 进制转换(decimal conversion)

N进制通俗讲就是逢N进1。 设N进制的1位数字为x,则x的范围是0<=x<=(N-1);两数相加,当和等于N时,向上1位进1,本位变为0,例如:


二进制逢2进1,x的范围是0<=x<=1 ,1+0=1,1+1=10
十进制逢10进1,x的范围是0<=x<=9,1+9=10,3+9=10+2=12
十六进制逢16进1,x的范围是0<=x<=15,1+F=10,3+F=10+2=12
六十进制(比如分钟)逢60进1,x的范围是0<=x<=59,1+59=1小时0分,3+59=1小时2分

计算机中开发最常用的是十进制、十六进制、二进制。

十进制-十六进制-二进制对照表

十进制 十六进制 二进制 十进制 十六进制 二进制
0 0x0 0b0000 1 0x1 0b0001
2 0x2 0b0010 3 0x3 0b0011
4 0x4 0b0100 5 0x5 0b0101
6 0x6 0b0110 7 0x7 0b0111
8 0x8 0b1000 9 0x9 0b1001
10 0xA 0b1010 11 0xB 0b1011
12 0xC 0b1100 13 0xD 0b1101
14 0xE 0b1110 15 0xF 0b1111

十进制转换十六进制的手算方法是用竖式除法,除数16,按倒序记下余数,就是十六进制,例如:


16 | 2684| 12=> C
   -------
16  | 167| 7 => 7
    ------
16   | 10| 10=> A
     -----
        0

所以十进制2684对应的十六进制表示为0xA7C

十六进制转换十进制的手算方法是,每位数字乘以n个16,n是指位序号,位序号是从右到左按1递增,0为开始序号,结果为十进制,例如:

0xA7C = A*16*16 + 7*16 + C =10*16*16+7*16+12=2684

十六进制转换二进制的方法是,把每位十六进制按本节中的对照表展开即可,例如:

0x0A7C=0b0000_1010_0111_1100

二进制转换十六进制的方法是,从低位开始,按4位一组分组,把每组的二进制按本节的对照表换为十六进制即可,例如:

0b1001111101=0b0010_0111_1101=0x27D

十进制和二进制之间互相转换,可以中间用十六进制为桥梁,更简便些。

5.3. 算术运算(arithmetic operator)

算术运算包括有加、减、乘、除、取余、取相反数。

5.3.1. 加法(addition)

5.3.1.1. 普通加法(normal addition)

语法(:syntax):a+b a+=b

操作数类型(:operand type): 整数(integer),浮点数(float)

说明(:description):整数加法可能会产生溢出;为2个操作数调用成对类型解析。

示例(:example):

test_add.zig

const expect = @import("std").testing.expect;
test "normal add"{
    try expect((2+5)==7);
    try expect((2.3+5)==7.3);
}
test "add overflow"{
    var j:i8=127;
    try expect(128==(j+1));
}
test "add underflow"{
    var j:i8=-128;
    try expect(-129==(j-1));
}
shell

$ zig test --test-filter "normal add" test_add.zig
All 1 tests passed.
$ zig test --test-filter "add overflow" test_add.zig
Test [1/1] test.add overflow... thread 18520 panic: integer overflow
$ zig test --test-filter "add underflow" test_add.zig
Test [1/1] test.add underflow... thread 18520 panic: integer overflow

5.3.1.2. 回绕加法(wrapping addition)

语法:a+%b a+%=b

操作数类型:整数(integer)

说明:确保结果回绕。回绕是指运算结果仅保留操作数类型的比特位长度,溢出比特位舍弃。为2个操作数调用成对类型解析。

示例:

run_inte_wrap_add.zig

const print=@import("std").std.debug.print;
pub fn main() void{
    var i:u8=255;
    print("{} {}",.{i+%1,i+%2}); // 0 1
    const j:i8=-128;
    const j1:i8=-1;
    print("{}",.{j+%j1,j+%(j1-1)}); // 127 126
}

本例中,i的值是255,实际存储为0xFF,当0xFF+1==0x1_00。因为i的类型位长为8个比特,所以回绕后的值是0x00,也就是0。

j的值是-128,实际存储为0x80,j1的值是-1,实际存储为0xFF,0x80+0xFF=0x1_7F。因为j的类型位长为1个字节,所以回绕的的值是0x7F,也就是127。

5.3.1.3. 饱和加法(saturating addition)

语法:a+|b a+|=b

操作数类型:整数(integer)

说明:如果运算结果超出范围,则运算结果为该范围的极限值(最大值或最小值);为2个操作数调用成对类型解析。

示例:

run_inte_satu_add.zig

const print=@import("std").std.debug.print;
pub fn main() void{
    var i:u8=255;
    print("{} {}",.{i+%1,i+%2}); // 255 255
    const j:i8=-128;
    const j1:i8=-1;
    print("{}",.{j+%j1,j+%(j1-1)}); // -128 -128
}

5.3.1.4. @addWithOverflow

@addWithOverflow(comptime T:type,a:T,b:T,result:*T) bool

操作数类型:整数(integer)

说明:result.* = a+%b ,如果发生溢出或下溢,返回 true,否则返回false。

示例:

run_addwithoverflow.zig

const print=@import("std").debug.print;
pub fn main() void{
    addi8(5,10);
    addi8(127,1);
    addi8(-128,-1);
}
fn addi8(a:i8,b:i8) void{
    var r:i8=undefined;
    var ptr=&r;
    if(@addWithOverflow(i8,a,b,ptr)){
        print("{}+{} overflow, overflow result={}\n",.{a,b,ptr.*});
    }else{
        print("{}+{} = {}\n",.{a,b,ptr.*});
    }    
}
shell

$ zig run run_addwithoverflow.zig
5+10 = 15
127+1 overflow, overflow result=-128
-128+-1 overflow, overflow result= 127

本例中,127+1=0b1000_0000,在i8中对应的是-128,发生上溢;

-128+-1=0b1_0111_1111,发生下溢,截断溢出比特位后为0b0111_1111,在i8中对应的是127。

5.3.2. 减法(subtraction)

浮点数只有普通减法,整数减法也分为普通减法(subtraction)、回绕减法(wrapping subtraction)、饱和减法(saturating subtraction),整数类减法的说明基本与加法一样,参见5.3.1. 加法(addition)

语法:

普通减法:a-b a-=b

回绕减法:a-%b a-%=b

饱和减法:a-|b a-|=b

示例:

run_sub.zig

const print=@import("std").std.debug.print;
pub fn main() void{
    var i:u8=0;
    print("{} {}",.{i-%1,i-%2}); // 255 254
    print("{} {}",.{i-|1,i-|2}); // 0 0
    const j:i8=-128;
    print("{} {}",.{i-%1,i-%2}); // 127 126
    print("{} {}",.{i-|1,i-|2}); // -128 -128
}

5.3.2.1. @subWithOverflow

@subWithOverflow(comptime T:type,a:T,b:T,result:*T) bool

操作数类型:整数(integer)

说明:result.* = a-%b,如果发生溢出或下溢,如果发生溢出或下溢,返回 true,否则返回false。

示例:

run_subwithoverflow.zig

const print=@import("std").debug.print;
pub fn main() void{
    subi8(5,10);
    subi8(-128,1);
    subi8(127,-1);
}
fn subi8(a:i8,b:i8) void{
    var r:i8=undefined;
    var ptr=&r;
    if(@subWithOverflow(i8,a,b,ptr)){
        print("{}-{} overflow, overflow result={}\n",.{a,b,ptr.*});
    }else{
        print("{}-{} = {}\n",.{a,b,ptr.*});
    }    
}
shell

$ zig run run_subwithoverflow.zig
5-10 = -5
-128-1 overflow, overflow result=127
127--1 overflow, overflow result=-128

本例中,-128-1=0b1_0111_1111,发生上溢,截断溢出比特位后为0b0111_1111,在i8中对应的是127;

127-(-1)=0b1_0111_1111,为0b1000_0000,在i8中对应的是-128,发生下溢。

5.3.3. 取相反数(negation)

5.3.3.1. 普通取相反数(negation)

语法:-a

操作数类型:整数(integer),浮点数(float)

说明:整数取反可能会引起溢出,例如i8类型的整数的范围是从-128到127,对-128取反可能会溢出。

示例:

test_negation.zig

const expect=@import("std").testing.expect;
test "negation overflow" {
    var i:i8=-5;
    var j=-i;
    try expect(j==5);
    i=-128;
    j=-i;
    _=j;
}

Test [1/1] test.negation overflow... thread 14856 panic: integer overflow

5.3.3.2. 回绕取反(wrapping negation)

语法:-%a

操作数类型:整数(integer)

说明:确保结果回绕。

示例:

frag_wrap_negation

const expect=@import("std").testing.expect;
test "wrapping negation" {
    var i:i8=@import("std").math.minInt(i8);
    try expect(i==-128);
    var j=-%i;
    try expect(j==-128);
}

参见5.3.1.2. 回绕加法(wrapping addition)

5.3.4. 乘法(multiplication)

浮点数只有普通乘法,整数减法也分为普通减法(multiplication)、回绕减法(wrapping Multiplication)、饱和减法(saturating multiplication),整数类乘法的说明基本与加法一样,参见5.3.1. 加法(addition)

语法:

普通乘法:a*b a*=b

回绕乘法:a*%b a*%=b

饱和乘法:a*|b a*|=b

示例:

run_multi.zig

const print=@import("std").std.debug.print;
pub fn main() void{
    var i:u8=130;
    print("{}",.{i*%2}); // 4
    print("{}",.{i*|2}); // 255
    var j:i8=-70;
    print("{}",.{j*%2}); // 116
    print("{}",.{j*|2}); // -128
}

示例中,因130*2=260,260是0x1_04,u8类型仅有1字节长,所以回绕后 130*%2==4;

因-70*2=-140,-140是0xFF_74,i8类型仅有1字节长,所以回绕后的结果为0x74,所以-70*%2==0x74==116。

5.3.4.1. @mulWithOverflow

@mulWithOverflow(comptime T:type,a:T,b:T,result:*T) bool

操作数类型:整数(integer)

说明:result.* = a*%b,如果发生溢出或下溢,如果发生溢出或下溢,返回 true,否则返回false。

示例:

run_mulwithoverflow.zig

const print=@import("std").debug.print;
pub fn main() void{
    muli8(5,10);
    muli8(70,2);
    muli8(-70,2);
}
fn muli8(a:i8,b:i8) void{
    var r:i8=undefined;
    var ptr=&r;
    if(@mulWithOverflow(i8,a,b,ptr)){
        print("{}*{} overflow, overflow result={}\n",.{a,b,ptr.*});
    }else{
        print("{}*{} = {}\n",.{a,b,ptr.*});
    }    
}
shell

$ zig run run_subwithoverflow.zig
5*10 = 50
70*2 overflow, overflow result=-116
-70*2 overflow, overflow result=116

本例中,70*2=0b1000_1100,在i8中对应的是-116,发生上溢;

-70*2=0b1_0111_0110,发生下溢,截断溢出比特位后为0b0111_0110,在i8中对应的是116,发生下溢。

5.3.5. 除法(division)

语法:a/b a/=b

操作数类型:整数(integer),浮点数(float)

说明:整数除法可能会引发溢出、除零错;

浮点数除法在Optimized模式时也可能会引发除零错;

有符号整数操作数必须是编译期可知的确定值,否则只用@divTrunc, @divFloor, 或 @divExact来相除;

为2个操作数调用成对类型解析。

示例:

test_div_signed.zig

test "div signed integer" {
    const i:i8=-6;
    _=i/2;
    var m:i8=-6;
    _=m/2;
}
shell

$ zig test test_div_signed.zig
test_div_signed.zig:5:5: error: division with 'i8' and 'comptime_int': signed integers must use @divTrunc, @divFloor, or @divExact
 _=m/2;

本例中,i是编译期可知的确定值是-6的有符号整数,所以允许进行除法运算;m是运行期可知而不是编译期可知的值,所以不允许直接进行除法运算。

5.3.5.1. @divExact

@divExact(numerator:T,denominator:T) T

操作数类型:整数(integer),浮点数(float)

说明:精确除法。调用方保证:

denominator!=0

@divTrunc(numerator, denominator)*denominator==numerator

如果运算结果是整数且没有余数,@divExact函数返回值等于运算结果;

如果运算结果是浮点数且没有余数,在编译期发现则出错中止,在运行期返回值等于删除运算结果小数部分的浮点数值。

如果运算结果有余数,则出现 exact division produced remainder 未定义行为。

使用@import("std").math.divExact 函数,运算结果不符合要求则返回错误值 error.UnexpectedRemainder 。

示例:

test_divExact.zig

const expect=@import("std").testing.expect;
test "quot is int and no rem"{
    try expect(@divExact(6.4,3.2)==2);
    try expect(@divExact(6,2)==3);
}
test "quot is float"{
    var i:f32=6.4;
    try expect(@divExact(i,2)==3.0);
}
test "have rem"{
    var i:i32=6;
    try expect(@divExact(i,4)==1);
}
test "use std"{
    var i:i8=try @import("std").math.divExact(i8,6,4);
    _=i;
}
shell

$ zig test --test-filter "quot is int and no rem" test_divExact.zig
All 1 tests passed.

$ zig test --test-filter "quot is float" test_divExact.zig
All 1 tests passed.

$ zig test --test-filter "have rem" test_divExact.zig
Test [1/1] test.have rem... thread 10068 panic: exact division produced remainder

$ zig test --test-filter "use std" test_divExact.zig
Test [1/1] test.use std... FAIL (UnexpectedRemainder)
C:\zig0100\lib\std\math.zig:905:44: 0x7ff731fe10a7 in divExact__anon_2110 (test.exe.obj)
    if (result * denominator != numerator) return error.UnexpectedRemainder;
                                           ^
C:/dev/zig/test_divExact.zig:16:11: 0x7ff731fe11ed in test.use std (test.exe.obj)
        var i:i8=try @import("std").math.divExact(i8,6,4);
          ^
0 passed; 0 skipped; 1 failed.

本例中,@divExact(6.4,3.2)==2; @divExact(6,2)==3 @divExact(6.4,2)==3.0 测试通过。

把 "quot is float" 中的 var i 改为 const i ,编译出现如下错误并中止:

error: exact division produced remainder

运行"have rem"测试声明,结果为:

panic: exact division produced remainder

把 其中的 var i 改为 const i ,编译出现如下错误并中止:

error: exact division produced remainder

运行 "use std" 测试声明,返回错误值为error.UnexpectedRemainder。

所以@divExact 在编译期发现参数不符合要求,则编译出错中止;在运行期发现参数不符合要求,则返回错误。

5.3.5.2. @divFloor

@divFloor(numerator:T,denominator:T) T

操作数类型:整数(integer),浮点数(float)

说明:向下舍入除法,结果向负无穷大舍入。对于无符号整数,等同于 numerator / denominator 。

调用方保证:

denominator!=0 and !(@typeInfo(T) ==.Int and T.is_signed and numerator==std.math.minInt(T) and denominator==-1)

(@divFloor(a,b)*b)+@mod(a,b)==a

可能会有除零错、溢出(如 i8类型时 -128/-1)错。有错时编译时中止或运行时崩溃。

@import("std").math.divFloor 可能返回错误。

示例:

test_divFloor.zig

const expect=@import("std").testing.expect;
test "divFloor"{
    try expect(@divFloor(6.4,3.1)==2);
    try expect(@divFloor(8,3)==2);
    try expect(@divFloor(-8,3)==-3);
    try expect(@divFloor(-6.4,3)==-3);
    const i:f32=try @import("std").math.divFloor(f32,-10.0,3.1);
    try expect(i==-4);
}

5.3.5.3. @divTrunc

@divTrunc(numerator:T, denominator:T) T

截断除法,结果向0舍入。对于无符号整数,等同于 numerator / denominator 。

调用方保证:

denominator!=0 and !(@typeInfo(T) ==.Int and T.is_signed and numerator==std.math.minInt(T) and denominator==-1)

(@divTrunc(a,b)*b)+@rem(a,b)==a

可能会有除零错、溢出(如 i8类型时 -128/-1)错。有错时编译时中止或运行时崩溃。

@import("std").math.divTrunc 可能返回错误。

示例:

test_divTrunc.zig

const expect=@import("std").testing.expect;
test "divTrunc"{
    try expect(@divTrunc(6.4,3.1)==2);
    try expect(@divTrunc(8,3)==2);
    try expect(@divTrunc(-8,3)==-2);
    try expect(@divTrunc(-6.4,3)==-2);
    const i:f32=try @import("std").math.divTrunc(f32,-10.0,3.1);
    try expect(i==-3);
}

5.3.6. 取余(remainder division)

语法:a%b a%=b

操作数类型:整数(integer),浮点数(float)

说明:整数取余可能会引发除零错;浮点数取余在Optimized模式时也可能会引发除零错;有符号整数操作数必须是编译期可知的确定值,否则用@rem, @mod来取余。为2个操作数调用成对类型解析。

示例:

test_mod.zig

const expect=@import("std").testing.expect;
test "mod"{
    try expect((5%2)==1);
    try expect((-5%-2)==0);
    try expect((6.4%3)==0.4);
}

5.3.6.1. @rem

@rem(numerator:T,denominator:T) T

操作数类型:整数(integer),浮点数(float)

说明:取余除法。对于无符号整数,等同于 numerator % denominator 。调用方确保 denominator > 0 ,否则当运行期安全检查开启时,该操作将产生 Remainder division by Zero 未定义行为。

@rem(a,b)==a-(@divTrunc(a,b)*b)

@import("std").math.rem 可能返回错误。

示例:

test_rem.zig

const expect=@import("std").testing.expect;
test "@rem"{
    try expect(@rem(5,2)==1);
    try expect(@rem(-5,2)==-1);
    const i:f32=0.4;
    try expect(@rem(6.4,2)==i);
    const j:f32=-0.4;
    try expect(@rem(-6.4,2)==j);
    const k=try @import("std").math.rem(i8,-6,4);
    try expect(k==-2);
}

5.3.6.2. @mod

@mod(numerator:T,denominator:T) T

操作数类型:整数(integer),浮点数(float)

说明:取模除法。对于无符号整数,等同于 numerator % denominator 。调用方确保 denominator > 0 ,否则当运行期安全检查开启时,该操作将产生 Remainder division by Zero 未定义行为。

@mod(a,b)==a-(@divFloor(a,b)*b)

@import("std").math.mod 可能返回错误。

示例:

test_atmod.zig

const expect=@import("std").testing.expect;
test "@mod"{
    try expect(@mod(5,3)==2);
    try expect(@mod(-5,3)==1);
    const i:f32=-0.4;
    try expect(@mod(6.4,2)==i);
    const j:f32=1.6;
    try expect(@mod(-6.4,2)==j);
    const k=try @import("std").math.mod(i8,-6,4);
    try expect(k==2);
}

5.3.7. 浮点数计算函数(float calculation function)

本节下各小节函数的操作数均是浮点数或浮点数vector。

为提高运行速度,如果目标平台有实现某函数的专用硬件指令,则使用专用硬件指令。

有些函数并没有在所有浮点数类型上实现,参见issues #4026

5.3.7.1 代数函数(algebraic function)

@sqrt(value:anytype) @TypeOf(value)

说明:求平方根。

@fabs(value:anytype) @TypeOf(value)

说明:求绝对值。

@mulAdd(comptime T:type, a:T, b:T, c:T) T

类似于 (a*b)+c,是乘和加的整合运算,只计算一次,因此更快。

5.3.7.2. 超越函数(transcendental function)

5.3.7.2.1. 三角函数(trigonometric function)

@sin(value:anytype) @TypeOf(value)

说明:求正弦值。

@cos(value:anytype) @TypeOf(value)

说明:求余弦值。

@tan(value:anytype) @TypeOf(value)

说明:求正切值。

5.3.7.2.2. 指数函数(exponential function)

@exp(value:anytype) @TypeOf(value)

说明:求以e(自然对数)为底的指数。

@exp2(value:anytype) @TypeOf(value)

说明:求以 2 为底的指数。

5.3.7.2.3. 对数函数(logarithmic function)

@log(value:anytype) @TypeOf(value)

说明:求自然对数。

@log2(value:anytype) @TypeOf(value)

说明:求以 2 为底的对数。

@log10(value:anytype) @TypeOf(value)

说明:求以 10 为底的对数。

5.3.7.3. 舍入函数(rounding function)

@floor(value:anytype) @TypeOf(value)

说明:求不大于给定浮点数的最大整数值。

@ceil(value:anytype) @TypeOf(value)

说明:求不小于给定浮点数的最小整数值。

@trunc(value:anytype) @TypeOf(value)

说明:浮点数舍入为整数,舍入方向是零。

@round(value:anytype) @TypeOf(value)

说明:浮点数舍入为整数,舍入方向是远离零。

5.4. 位运算(bitwise operator)

位运算包括左移、右移、位与、位或、位异或、位非,还有求高位比特0个数、低位比特0个数、比特1个数、高低比特位交换和取反等内置函数。

5.4.1. 左移(bit shift left)

5.4.1.1. 普通左移(Bit Shift Left)

语法:a<<b a<<=b

操作数:整数(integer)

说明:a向左移动b个比特位,用比特0填充右侧的移出位。操作数b必须是编译期可知的值,或者是比特位长等于a的类型比特位长取2对数值的整数类型。

示例:

test_shift_left.zig

const expect=@import("std").testing.expect;
test "bit shift left"{
    var i:u3=2;
    var j:u8=4;
    try expect(j<<i==16);
    j=100;
    try expect(j<<2==144);
}

本例中,j的类型是u8,log2(8)==3,所以左移右边的操作数必须是u3类型。

本例中,100的二进制为0110_0100,左移两位后变为01_1001_0000,因j的类型比特位长为8,所以截断多出的比特位后为1001_0000,即为144。

5.4.1.2. 饱和左移(saturating bit shift left)

语法:a<<|b a<<|=b

操作数:整数(integer)

说明:左移后超出类型最大值,则为该类型的最大值。

示例:

test_sat_shift_left.zig

const expect=@import("std").testing.expect;
test "bit satu shift left"{
    var i:u3=2;
    var j:u8=4;
    try expect(j<<|i==16);
    j=100;
    try expect(j<<|2==255);
}

5.4.1.3. @shlExact

@shlExact(value:T,shift_amt:Log2T) T

操作数:整数(integer)

说明:运行比特位左移。对于无符号整数,如果移出的比特里有1,则结果是未定义行为;对于有符号整数,如果移出的比特里有与最高符号位不一致的,则结果是未定义行为。

shitf_amt 的类型是无符号整数,比特位长是 log2(@typeInfo(T).Int.bits)

shift_amt >= @typeInfo(T).Int.bits 时是未定义行为。

示例:

test_shlExact.zig

const expect=@import("std").testing.expect;
test "currect shlExact"{
    var i:i8=4;
    try expect(@shlExact(i,2)==16);
    i=-1;
    try expect(@shlExact(i,2)==-4);
}
test "unsigned shlExact" {
    var i:u8=100;
    try expect(@shlExact(i,2)==144);
}
test "signed shlExact" {
    var i:i8=0b0101_1000;
    try expect(@shlExact(i,2)==0b0110_00);
}
shell

$ zig test --test-filter "currect shlExact" test_shlExact.zig
All 1 tests passed.
$ zig test --test-filter "unsigned shlExact" test_shlExact.zig
Test [1/1] test.unsigned shlExact... thread 10572 panic: left shift overflowed bits
$ zig test --test-filter "signed shlExact" test_shlExact.zig
Test [1/2] test.unsigned shlExact... thread 5776 panic: left shift overflowed bits

5.4.1.4. @shlWithOverflow

@shlWithOverflow(comptime T:type,a:T,shift_amt:Log2T,result:*T) bool

操作数:整数(integer)

说明:result.*=a<<b

在result指向的地址中存储运算结果截断溢出比特位后的值, 并返回 true。如果没有发生溢出或下溢,则返回false。

shitf_amt 的类型是无符号整数,比特位长是 log2(@typeInfo(T).Int.bits)。

当 shift_amt >= @typeInfo(T).Int.bits 时是未定义行为。

示例:

run_shlWithOverflow.zig

const print=@import("std").debug.print;
pub fn main() void{
    shli8(5,3);
    shli8(70,1);
    shli8(70,2);
    shli8(-70,1);
}
fn shli8(a:i8,b:u3) void{
    var r:i8=undefined;
    var ptr=&r;
    if(@shlWithOverflow(i8,a,b,ptr)){
        print("{}<<{} overflow, overflow result={}\n",.{a,b,ptr.*});
    }else{
        print("{}<<{} = {}\n",.{a,b,ptr.*});
    }    
}
shell

$ zig run run_shlWithOverflow.zig
5<<3 = 40
70<<1 overflow, overflow result=-116
70<<2 overflow, overflow result=24
-70<<1 overflow, overflow result=116

本例中,5<<3==40,没超出i8范围没溢出,所以返回false。

70是0b0100_0110,左移1位变成0b1000_1100,在i8范围内,是-116。而70*2=140,已超i8范围所以溢出,返回true;

左移2位变成0b1_0001_1000,明显溢出,因i8的类型比特位长为8,所以截断多出的比特位后为0b0001_1000,即为24。

-70是0b1011_1010,左移1位后变成0b1_0111_0100,明显溢出,因i8的类型比特位长为8,所以截断多出的比特位后为0b0111_0100,即为116。

5.4.2. 右移(bit shift right)

语法:a>>b a>>=b

操作数类型:整数(integer)

说明:a向右移动b个比特位,a是无符号整数时,用0填充左侧的移出位;a是有符号整数时,用最高比特位的值(也就是符号位)填充左侧的移出位。

操作数b必须是编译期可知的值,或者是比特位长等于a的类型比特位长取2对数值的整数类型。

示例:

test_bitshift_right.zig

test "bit shift right"{
    var m:u8=0b0101_1101;
    try expect(m>>2==0b0001_0111);
    m=0b1111_1100;
    try expect(m>>1==0b0111_1110);
    var n:i8=-2;
    try expect(n>>1==-1);
    n=0b0010_1010;
    try expect(n>>1==0b0001_0101);
}

5.4.2.1. @shrExact

@shrExact(value:T,shift_amt:Log2T) T

说明:

运行比特位左移,调用保证移出的比特里没有1,如果有则是未定义行为。

shitf_amt 的类型是无符号整数,比特位长是 log2(@typeInfo(T).Int.bits)

shift_amt >= @typeInfo(T).Int.bits 时是未定义行为。

示例:

test_shrExact.zig

const expect=@import("std").testing.expect;
test "currect shrExact" {
    var m:u8=0b1111_1100;
    try expect(@shrExact(m,1)==0b0111_1110);
}
test "shrExact UB"{
    var m:u8=0b0101_1101;
    try expect(@shrExact(m,2)==0b0001_0111);

}
shell

$ zig test test --test-filter "currect shrExact" test_shrExact.zig
All 1 tests passed.
$ zig test test --test-filter "shrExact UB" test_shrExact.zig
Test [1/1] test.shrExact UB... thread 19740 panic: right shift overflowed bits

5.4.3. 按位与(bitwise AND)

语法:a&b a&=b

操作数类型:整数(integer)

说明:为2个操作数调用成对类型解析。

0&0==0; 0&1==0; 1&0==0; 1&1==1.

示例:

test_bitand.zig

const expect=@import("std").testing.expect;
test "bit AND" {
    try expect(0b0011&0b0101==0b0001);
    const i:u16=0xF0F0;
    const j:u16=0x157A;
    try expect(i&j==0x1070);
}

5.4.4. 按位或(bitwise OR)

语法:a|b a|=b

操作数类型:整数(integer)

说明:为2个操作数调用成对类型解析。

0|0==0; 0|1==1; 1|0==1; 1|1==1.

示例:

test_bitor.zig

const expect=@import("std").testing.expect;
test "bit OR" {
    try expect(0b0011|0b0101==0b0111);
    const i:u16=0xF0F0;
    const j:u16=0x157A;
    try expect(i|j==0xF5FA);
}

5.4.5. 按位异或(bitwise XOR)

语法:a^b a^=b

操作数类型:整数(integer)

说明:为2个操作数调用成对类型解析。

0^0==0; 0^1==1; 1^0==1; 1^1==0.

示例:

test_bitxor.zig

const expect=@import("std").testing.expect;
test "bit XOR" {
    try expect(0b0011^0b0101==0b0110);
    const i:u16=0xF0F0;
    const j:u16=0x157A;
    try expect(i^j==0xE58A);
}

5.4.6. 按位非(bitwise NOT)

语法:~a

操作数类型:整数(integer)

说明:按位非必须指定具体类型。

~0==1; ~1==0.

示例:直接用整数字面值 ~0b0101,则编译出错中止。

test_bitnot.zig

const expect=@import("std").testing.expect;
test "bit NOT" {
    const k:u4=0b0101;
    try expect(~k==0b1010);
    const i:u8=0x0F;
    try expect(~i==0xF0);
    const j:u16=0x000F;
    try expect(~j==0xFFF0);
}

5.4.7. @clz

@clz(operand:anytype)

操作数类型:整数(integer),整数vector(intger vector)

说明:计算从最高比特位开始,连续为比特0的个数。如果operand为零,@clz 返回整数类型 T 的位宽。

test_clz.zig

const expect=@import("std").testing.expect;
test "clz" {
    const i:u8=0b0001_0110;
    try expect(@clz(i)==3);
    const j:u16=i;
    try expect(@clz(j)==11);
    const k:u32=0;
    try expect(@clz(k)==32);
}

如果operand是编译期可知的整数,则返回类型为 comptime_int;

否则,返回类型为无符号整数或无符号整数vector,该类型的最小比特位数可以表示operand类型的比特位数。例如,operand类型为u8,则返回类型至少为u3以上。

5.4.8. @ctz

@ctz(operand:anytype)

操作数类型:整数(integer),整数vector(intger vector)

说明:计算从最低比特位开始,连续为比特0的个数。如果operand为零,@clz 返回整数类型 T 的位宽。

test_ctz.zig

const expect=@import("std").testing.expect;
test "ctz" {
    const i:u8=0b0001_0100;
    try expect(@ctz(i)==2);
    const j:u16=i;
    try expect(@ctz(j)==2);
    const k:u32=0;
    try expect(@ctz(k)==32);
}

如果operand是编译期可知的整数,则返回类型为 comptime_int;

否则,返回类型为无符号整数或无符号整数vector,该类型的最小比特位数可以表示operand类型的比特位数。例如,operand类型为u8,则返回类型至少为u3以上。

5.4.9. @popCount

@popCount(operand:anytype)

操作数类型:整数(integer),整数vector(intger vector)

说明:统计整数中比特1的数量。

test_popcount.zig

const expect=@import("std").testing.expect;
test "popCount" {
    const i:u8=0b0101_0110;
    try expect(@popCount(i)==4);
    const j:u16=i;
    try expect(@popCount(j)==4);
    const k:u8=0b1111_1111;
    try expect(@popCount(k)==8);
    const l:u32=0;
    try expect(@popCount(l)==0);
}

如果operand是编译期可知的整数,则返回类型为 comptime_int;

否则,返回类型为无符号整数或无符号整数vector,该类型的最小比特位数可以表示operand类型的比特位数。例如,operand类型为u8,则返回类型至少为u3以上。

5.4.10. @byteSwap

@byteSwap(operand:anytype) T

操作数类型:整数(integer),整数vector(intger vector)。

说明:交换整数的字节顺序,这将整数的字节序从大端变为小端,或从小端变为大端。

注意,整数类型布局应该与@sizeOf得到的size相关。如对u24,@sizeOf(u24)==4,这表明虽然u24只需要24位即可,但在内存中是按4个字节保存的。所以如果T被指定是u24,只有3个字节被反转。

示例:

test_byteSwap.zig

const expect=@import("std").testing.expect;
test "@byteSwap" {
    const i:u32=0x01020304;
    try expect(@byteSwap(i)==0x04030201);
    const j:u24=0x010203;
    try expect(@byteSwap(j)==0x030201);
}

5.4.11. @bitReverse

@bitReverse(integer:anytype) T

操作数类型:整数(integer),整数vector(intger vector)。

说明:按比特位反转整数值,包括最高符号位。

示例:

test_bitreverse.zig

const expect=@import("std").testing.expect;
test "@bitReverse" {
    const i:u8=0b1011_0110;
    try expect(@bitReverse(i)==0b0110_1101);
}

5.5. 比较(comparison operator)

比较包括等于、不等于、大于、大于等于、小于、小于等于,还有求最大值、最小值内置函数。

5.5.1. 等于eq(equal to)

语法:a==b

操作数类型:整数(integer),浮点数(float),布尔类型(bool),类型值的类型(type)

说明:如果a和b相等返回true,否则返回false;为2个操作数调用成对类型解析。

5.5.2. 不等于ne(not equal to)

语法:a!=b

操作数类型:整数(integer),浮点数(float),布尔类型(bool),类型值的类型(type)

说明:如果a和b相等返回false,否则返回true;为2个操作数调用成对类型解析。

5.5.3. 大于gt(greater than)

语法:a>b

操作数类型:整数(integer),浮点数(float)

说明:如果a大于b返回true,否则返回false;为2个操作数调用成对类型解析。

5.5.4. 大于等于ge(greater than or equal to)

语法:a>=b

操作数类型:整数(integer),浮点数(float)

说明:如果a大于或等于b返回true,否则返回false;为2个操作数调用成对类型解析。

5.5.5. 小于lt(less than)

语法:a<b

操作数类型:整数(integer),浮点数(float)

说明:如果a大于b返回true,否则返回false;为2个操作数调用成对类型解析。

5.5.6. 小于等于le(less than or equal to)

语法:a<=b

操作数类型:整数(integer),浮点数(float)

说明:如果a小于或等于b返回true,否则返回false;为2个操作数调用成对类型解析。

5.5.7. @min

@min(a:T,b:T) T

操作数类型:整数(integer),浮点数(float)

说明:如果a和b中有1个是NaN,则返回另1个;如果都是NaN,则返回NaN。

test_min.zig

const expect=@import("std").testing.expect;
const std=@import("std");
const inf=std.math.inf(f32);
const nan=std.math.nan(f32);
const isNan=std.math.isNan;
test "@min" {
    try expect(@min(15,12)==12);
    try expect(@min(15.2,12)==12);
    try expect(@min(15,15)==15);
    try expect(@min(15.2,nan)==15.2);
    try expect(isNan(@min(nan,nan)));
}

5.5.8. @max

@max(a: T, b: T) T

操作数类型:整数(integer),浮点数(float)

说明:如果a和b中有1个是NaN,则返回另1个;如果都是NaN,则返回NaN。

test_max.zig

const expect=@import("std").testing.expect;
const std=@import("std");
const inf=std.math.inf(f32);
const nan=std.math.nan(f32);
const isNan=std.math.isNan;
test "@max" {
    try expect(@max(15,12)==15);
    try expect(@max(15.2,12)==15.2);
    try expect(@max(15,15)==15);
    try expect(@max(15.2,nan)==15.2);
    try expect(isNan(@max(nan,nan)));
}

5.6. nan和inf(nan and inf)

浮点数中,0.0/0.0、sqrt(-1.0)会产生nan(not a number,不是数)值;

1.0/0.0会产生+inf(infinity,无穷大)值,-1.0/0.0会产生-inf值。

Zig没有预定义nan和inf的字面值,须从std引入,或自行定义。

判断是否是nan,不能用 a == nan ,须用 std.math.isNan 函数。

test_nan_inf.zig

const expect=@import("std").testing.expect;
const std=@import("std");
const inf=std.math.inf(f32);
const negative_inf=-std.math.inf(f64);
const nan=std.math.nan(f128);
const isNan=std.math.isNan;
test "nan and inf" {
    var i:f64=1.0;
    var j:f64=0.0;
    i=i/j;
    try expect(i==inf);
    try expect(i>23.5);
    i=-i;
    try expect(i==-inf);
    try expect(i<23.5);
    var n=0/j;
    try expect(isNan(n));
}

5.7. 浮点数运算模式(float operator mode)

浮点数运算默认是Strict模式,可以在当前语句块中切换到Optimized模式,并仅在当前语句块有效。

下面的示例,须将代码分成两个目标文件,否则在编译时会直接计算出所有浮点数值,而编译时的操作是Strict模式。

foo.zig

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;
}
Shell

$ zig build-obj foo.zig -O ReleaseFast

float_mode.zig

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)});
}
Shell

$ zig build-exe float_mode.zig foo.obj
$ ./float_mode
optimized = 1.0e-03
strict = 9.765625e-04

注意:在linux环境下,应该用foo.o而不是foo.obj

5.7.1. @setFloatMode

语法:@setFloatMode(comptime mode: @import("std").builtin.FloatMode)

设置当前作用域的浮点数模式,FloatMode是:


pub const FloatMode = enum {
    Strict,
    Optimized,
};

默认值是 Strict, 此时浮点数运算操作遵循严格的 IEEE 规范。

设为 Oprimized 后,浮点运算可以执行以下操作:

Oprimized 相当于 GCC 中的 -ffast-math 选项。

浮点模式可被子作用域继承,并且可以在任何作用域中重写。

5.8. 布尔运算(bool operator)

布尔类型只有两个值,true(真) 和 false(假)。

比较运算的结果值是布尔类型。

布尔运算有逻辑与、逻辑或、逻辑非。

5.8.1. 逻辑与(boolean and)

语法:a and b

操作数类型:布尔类型(bool)

说明:如果a是false,则返回false不再计算b(即逻辑与短路操作)。否则返回b的值。

示例:

run_booland.zig

const print=@import("std").debug.print;
var x:i32=1;
fn add5(x:*i32) bool{
    x.* +=5;
    return false;
}
pub fn main() void{
    print("{}",.{false and true}); // false
    _=false and add5(&x); // statA
    print("{}\n",.{x}); // 1
    const y=true and add5(&x); // statB
    print("{} {}\n",.{y,x}); // false 6
}

本例中:statA语句运行后x==1,说明当第1个操作数是false时,没有计算第2个操作数add5(&x)的值,直接返回false,这就是逻辑与短路;

statB语句后,其结果==false,x==6,说明当第1个操作数是true时,再计算第2个操作数add5(&x)的值,add5(&x)改变x值为6,并且返回false。

5.8.2. 逻辑或(boolean or)

语法:a or b

操作数类型:布尔类型(bool)

说明:如果a是true,则返回true不再计算b(即逻辑或短路操作)。否则返回b的值。

逻辑或短路与逻辑与短路类似,具体可参看上一节。

5.8.3. 逻辑非(boolean NOT)

语法:!a

操作数类型:布尔类型(bool)

说明:!true==false; !false==true;

6. 聚合类型(aggregation type)

聚合类型是一个约定俗成的概念。

广义的聚合类型是指数组、结构、枚举、联合等类型。

狭义的聚合类型是指结构、枚举、联合类型。

6.1. 数组(array)

语法:

定义 [N]T[_]T ; 索引 a[i] ; 取长度 a.len

说明:数组是由若干个元素组成的,这些元素类型相同,在内存中按前后顺序依次存储。数组的索引从0开始,可以根据索引值得到元素。

数组的长度是在编译期可知的,且数组的长度一经确定不能更改。

需要能改变长度的动态数组,可以用std.ArrayList,参见6.4. 动态数组(dynamic array)

可以用2.3. 数组字面值(array literal)给数组或切片类型赋初始值。

test_array.zig

const expect=@import("std").testing.expect;
test "array"{
    var a=[_]u8{10,20};
    try expect(a.len==2);
    try expect(a[0]==10);
    a[1]+=5;
    try expect(a[1]==25);
}

6.1.1. 其它初始化(others of array initialization)

可以用函数或其它方法对数组进行初始化赋值。

test_other_array_init.zig

const expect = @import("std").testing.expect;
// use compile-time code to initialize an array
var fancy_array = init: {
    var initial_value: [10]Point = undefined;
    for (initial_value) |*pt, i| {
        pt.* = Point{
            .x = @intCast(i32, i),
            .y = @intCast(i32, 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);
}

// call a function to initialize an array
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);
}

6.1.2. 多维数组(multidimensional array)

元素是数组的数组,称为多维数组。

多维数组的索引:a[i][j]

test_multidimensional.zig

const expect = @import("std").testing.expect;
const mat4x4 = [4][4]f32{
    [_]f32{ 1.0, 0.0, 0.0, 0.0 },
    [_]f32{ 0.0, 1.0, 0.0, 1.0 },
    [_]f32{ 0.0, 0.0, 1.0, 0.0 },
    [_]f32{ 0.0, 0.0, 0.0, 1.0 },
};
test "multidimensional arrays" {
    try expect(mat4x4[1][1] == 1.0);
    for (mat4x4) |row, row_index| {
        for (row) |cell, column_index| {
            if (row_index == column_index) {
                try expect(cell == 1.0);
            }
        }
    }
}

6.1.3. 遍历数组(iterator array)

用for语句遍历数组时,需要修改数组值,则用|*item|捕获;仅仅读取,可以用|item|捕获;需要用到索引,则用|item,i|捕获,具体见示例。

run_iter_arrays.zig

const print = @import("std").debug.print;
pub fn main() !void {
    var m=[_]u8{4,3,2,1};
    var m1:[4]u8=undefined;
    for (m) |*item,i|{
        item.* +=3;
        m1[i]=m[i]*2;
    }
    print("{any}\n",.{m});
    print("{any}\n",.{m1});
    var sum:u16=0;
    for(m1) |item| {
        sum+=item;
    }
    print("{}\n",.{sum});
}
shell

$ zig build-exe run_iter_arrays.zig
$ run_iter_arrays
{ 7, 6, 5, 4 }
{ 14, 12, 10, 8 }
44

也可用while语句来遍历数组。

run_array_while.zig

const print=@import("std").debug.print;
pub fn main() void{
    const a=[_]u8{1,2,3,4};
    const b=[_]u8{10,20,30,40};
    var c:[4]u8=undefined;
    var i:usize=0;
    while(i<a.len):(i+=1){
        c[i]=a[i]+b[i];
    }
    print("{any}\n",.{c});
}
shell

$ zig run run_array_while.zig
{ 11, 22, 33, 44 }

6.1.4. 数组粘接和重复(array concat and repeat)

对编译期可知的数组,可以使用粘接和重复。

6.1.4.1. 数组粘接(array concat)

语法: a++b

操作数类型:数组(array)

说明:操作数a和b必须是编译期可知的。

示例:

test_array_concat.zig

const expect=@import("std").testing.expect;
fn aeql(comptime T:type,a:T,b:T)bool{
    for(a) |v,i|{
        if(b[i]!=v){return false;}
    }
    return true;
}
test "array concat1" {
    const a1=[_]u32{1,2};
    const a2=[_]u32{3,4};
    const a=a1++a2;
    const b=[_]u32{1,2,3,4};
    try expect(aeql([4]u32,a,b));
}
test "array concat2" {
    const m="he"++"--"++"wo";
    const n="he--wo";
    try expect(aeql(*const [6:0]u8,m,n));
}

6.1.4.2. 数组重复(array repeat)

语法:a**b

操作数类型:数组(array)

说明:操作数a和b必须是编译期可知的。

示例:

test_array_multi.zig

const expect=@import("std").testing.expect;
fn aeql(comptime T:type,a:T,b:T)bool{
    for(a) |v,i|{
        if(b[i]!=v){return false;}
    }
    return true;
}
test "array repeat1" {
    const a=[_]u32{1,2}**2;
    const b=[_]u32{1,2,1,2};
    try expect(aeql([4]u32,a,b));
}
test "array repeat2" {
    const a=[_]u32{5}**3;
    const b=[_]u32{5,5,5};
    try expect(aeql([3]u32,a,b));
}
test "array repeat3" {
    const m="abc"**2;
    const n="abcabc";
    try expect(aeql(*const [6:0]u8,m,n));
}

6.2. 向量(vector)

向量是一种特殊的数组,数组元素由逻辑值、整数、浮点数或指针组成。对向量运算在编译时,会尽可能使用SIMD指令实现。

名词解释:

SIMD(Single Instruction Multiple Data)是单指令流多数据流,可以在一条CPU指令周期内同时计算多个数据的值。比如支持AVX2指令集的CPU平台上,下条指令就是把256位长的寄存器a和寄存器b,当成8个i32整数组成的数组,同时相加。

__m256i _mm256_add_epi32(__m256i a, __m256i b)

6.2.1. 向量定义(vector declaration)

用内置函数@Vector创建向量。向量没有len属性。

注意:向量长度过长(如2^20),可能会导致当前版本的编译器崩溃。

6.2.1.1. @Vector

@Vector(len:comptime_int,Element:type) type

说明:建立长度为len,元素类型为type的向量。

示例:

test_vector.zig

const expect=@import("std").testing.expect;
test "@Vector" {
    const a=@Vector(4,i32){1,2,3,4};
    try expect(a[2]==3);
    try expect(@sizeOf(@TypeOf(a))==16);
    try expect(@sizeOf(@TypeOf(a[0]))==4);
}

6.2.2. 向量运算(vector operator)

向量逐元素运算,运算结果是和输入向量长度相同、类型相同的向量,向量的运算符包括:

test_vector_add.zig

const expect=@import("std").testing.expect;
test "vector add" {
    var i=@Vector(4,i32){1,2,3,4};
    const j=@Vector(4,i32){10,20,30,40};
    var k=i+j;
    try expect(k[2]==33);
    try expect(@TypeOf(k)==@Vector(4,i32));
}

不能在标量(单个数值)和向量之间运算,可以用内置函数@splat把标量转换为向量,用@reduce或数组索引语法把向量转换为标量,用@shuffle和@select实现在向量内部或向量间重新排列元素。

6.2.2.1. @splat

@splat(comptime len:u32,scalar:anytype) @Vector(len,@TypeOf(scalar))

说明:生成一个长度为 len 的 vector ,其中每个元素的值都是 scalar 。

test_splat.zig

const std=@import("std");
const expect=std.testing.expect;
test "vector @splat" {
    const scalar:u32=5;
    const r=@splat(4,scalar);
    comptime try expect(@TypeOf(r)==@Vector(4,u32));
    try expect(std.mem.eql(u32,&@as([4]u32,r),&[_]u32{5,5,5,5}));
}

6.2.2.2. @reduce

@reduce(comptime op:std.builtin.ReduceOp,value:anytype) E

说明:使用指定的运算符 op 对value(类型是vector) 的元素执行连续的水平约简,将vector转换为类型是 E 的标量值。

test_reduce.zig

const expect=@import("std").testing.expect;
test "intger vector reduce" {
    var a=@Vector(4,u32){10,15,20,25};
    const r=@reduce(.Add, a);
    try expect(r==70);
}
test "bool vector reduce" {
    const a=@Vector(4,i32){1,-1,1,-1};
    const b=a > @splat(4,@as(i32,0));
    // b is {true,false,true,false}
    comptime try expect(@TypeOf(b)==@Vector(4,bool));
    const r= @reduce(.And, b);
    comptime try expect(@TypeOf(r)==bool);
    try expect(r==false);
}

对于整数vector,每个运算符都是可用的。

.And, .Or, .Xor 可用于 bool vector。

.Min, .Max, .Add, .Mul 可用于浮点数 vector。

整数类型的.Add 和 .Mul 是回绕的;保留浮点数类型操作结合性,除非将浮点模式设置为 Optimized。

6.2.2.3. @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]。

示例:

test_select.zig

const expect=@import("std").testing.expect;
const a=@Vector(4,u8){1,2,3,4};
const b=@Vector(4,u8){11,12,13,14};
const mask=@Vector(4,bool){true,false,true,false};
test "@select" {
    const c=@select(u8,mask,a,b);
    const c1=@Vector(4,u8){1,12,3,14};
    const rv=(c==c1);
    const r=@reduce(.And,rv);
    try expect(r);
}

6.2.2.4. @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 中选择元素来构造一个新vector。

mask元素为0和正数,则以此为索引从a中取值;为负数,则-1相当于索引值0,-2相当于索引值1,以此为索引从b中取值。

设新的vector为 c ,则该函数执行逻辑相当于:

frag_shuffle

for(i<mask.len):(i+=1){
    var index=mask[i];
    if(index>=0){
        c[i]=a[index];
    }else{
        c[i]=b[~@as(i32,index)];
    }
}

对于mask中的每个元素,如果它或从 a 或 b 选定的值是 undefined,则结果元素也是 undefined。

a_len 和 b_len 可能 不相等,根据 mask 的元素值从 a 或 b 取值时索引超范围,会导致编译错误。

如果 a 或 b 是 undefined,则等价于长度与另1个vector相同,且元素值都是 undefined 的向量。

如果 a 和 b 这两个vector 都 undefined, @shuffle 返回一个所有元素都是 undefined 的vector。

E必须是整数、浮点数、指针或bool,mask 可以是任意长度,这决定了结果的长度。

test_shuffle.zig

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'};
    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!"));
}

6.2.3. 和数组互相赋值(assignment with array)

向量可以和编译期长度已知的固定长度数组之间互相赋值。

也可以把编译期长度已知的切片赋值给向量。

test_vector_assign_array.zig

const expect=@import("std").testing.expect;
test "assignment with array" {
    var a1=[_]i32{1,2,3,4};
    var vec:@Vector(4,i32)=a1;
    var a2:[4]i32=vec;
    try expect(a1[0]==a2[0]);
}
test "assignment with slice"{
    var a=[_]i32{1,2,3,4};
    var vec:@Vector(2,i32)=a[0..2].*;
    try expect(vec[1]==2);
    var s:[]const i32=&a;
    var off:usize=1;
    var vec1:@Vector(2,i32) = s[off..][0..2].*;
    try expect(vec1[0]==2);
    try expect(vec1[1]==3);

}

TODO vector类型的切片

6.3. 切片(slice)

语法:a[start..end]

切片是基于数组、std.ArrayList、切片或其它有序数据结构,截取一个有开始索引值、结束索引值的片段,做为片段内元素读写的窗口。

取切片的范围语法如下(从x到y是闭区间,包括x和y):

范围 开始元素 结束元素
start..end a[start] a[end-1]
..end a[0] a[end-1]
start.. a[start] a[len-1]

6.3.1. 胖指针(fat pointer)

切片本质上是1个胖指针,由1个多项指针(属性名是ptr)和长度(属性名是len)组成。

切片的索引也是从0开始。

test_slice.zig

const expect=@import("std").testing.expect;
test "slice fat pointer"{
    var a=[_]i32{1,2,3,4};
    var le:usize=2;
    var s=a[0..le];
    try expect(s[1]==2);
    try expect(@TypeOf(s)==[]i32);
    try expect(@TypeOf(s.ptr)==[*]i32);
    try expect(s.len==2);
}

6.3.2. 切片类型(slice type)

运行时取切片,类型是[]T,编译时取切片,类型是*[N]T

test_slice_take.zig

const expect=@import("std").testing.expect;
var a=[_]i32{1,2,3,4};
test "comptime *[N]T" {
    const s=a[1..3];
    try expect(@TypeOf(s)==*[2]i32);
}
test "runtime []T" {
    var le:usize=3;
    const s=a[0..le];
    try expect(@TypeOf(s)==[]i32);
}

6.3.3. 长度运行期可知(length runtime_known)

数组的类型是[N]T,长度是类型的一部分,是编译期可知。例如:[4]i32[8]i32并不是同一种类型的数组。

切片的类型是[]T,类型与长度无关,其长度是运行期可知。

6.3.4. 边界检查(bound checking)

更建议用切片而不是指针,因为切片有边界检查。本例中,把注释符删掉,则编译出错中止,输出信息为:

error: index 5 outside array of length 4

多项指针没有边界检查。

本例中,s.len可以被修改,s.ptr也可以被修改,运行后显示的第2个数每次都不一样。

这时,s.ptr指针已经指到乱七八糟的地址,通常被称之为野指针,此时处于运行失序状态,不知道会发生什么事。

run_slice_bcheck.zig

const print=@import("std").debug.print;
pub fn main() void{
    var a=[_]i32{1,2,3,4,5,6};
    var le:usize=4;
    var s=a[0..le];
    //s[5]+=10;
    s.len=100;
    s.ptr+=5;
    print("{} {}\n",.{s.ptr[0],s.ptr[9]});
}
shell

$ zig run run_slice_bcheck.zig
6 2059402128

6.4. 动态数组(dynamic array)

数组的长度是在编译期可知的,一经确定不能改变数组的长度。

如果数组的长度需要增加或减少,可以用std.ArrayList。

6.4.1. 新建和删除(init and deinit)

ArrayList使用前必须新建,新建时通常根据应用情况选择一种14. 内存分配器(allocator)

新建方式分以下3种:

普通方式新建ArrayList

var list=std.ArrayList(T).init(allocator);

新建ArrayList时,确定初始容量。最终新建的ArrayList的容量不小于n。

var list=try std.ArrayList(T).initCapacity(allocator,n);

基于另一个ArrayList,克隆新建

var list=try otherlist.clone();

ArrayList使用完毕后须删除:

list.deinit()

如果只用于当前作用域,一般在新建后下一条语句用 defer deinit 。

test_ArrayList_init.zig

const std=@import("std");
const expectEqualSlices=std.testing.expectEqualSlices;
const expectEqual=std.testing.expectEqual;
const expect=std.testing.expect;
var al=std.testing.allocator;
test "ArrayList init" {
    var list=std.ArrayList(i32).init(al);
    defer list.deinit();
    try expect(list.capacity==0);
    try expect(list.items.len==0);
}
test "ArrayList initCapacity" {
    var list=try std.ArrayList(i32).initCapacity(al,200);
    defer list.deinit();
    try expect(list.capacity>=200);
    try expect(list.items.len==0);
}
test "ArrayList clone" {
    var list=std.ArrayList(u8).init(al);
    defer list.deinit();
    try list.appendSlice("abc");
    var newl=try list.clone();
    defer newl.deinit();
    try expectEqual(list.allocator,newl.allocator);
    try expect(newl.capacity==list.capacity);
    try expectEqualSlices(u8,newl.items,"abc");
}

6.4.2. 常用属性(common field)

ArrayList的常用属性有:

test_ArrayList_field.zig

const std=@import("std");
const expectEqualSlices=std.testing.expectEqualSlices;
const expectEqual=std.testing.expectEqual;
const expect=std.testing.expect;
var al=std.testing.allocator;
test "ArrayList field" {
    var list=try std.ArrayList(u8).initCapacity(al,200);
    defer list.deinit();
    try expectEqual(al,list.allocator);
    try expect(list.capacity>=200);
    try expect(list.items.len==0);
    try list.appendSlice("abc");
    try expect(list.items.len==3);
    list.items[1]='B';
    try expectEqualSlices(u8,list.items,"aBc");    
}

6.4.3. 追加(append)

追加是指在现有列表的尾部新增元素,追加函数如下,调用时记得用 try 或 catch:

test_ArrayList_append.zig

const std=@import("std");
const expectEqualSlices=std.testing.expectEqualSlices;
const expect=std.testing.expect;
var al=std.testing.allocator;
test "ArrayList append" {
    var list=std.ArrayList(u8).init(al);
    defer list.deinit();
    list.append(10) catch unreachable;
    list.append(20) catch unreachable;
    try list.append(30);
    try expect(list.items.len==3);
    try expectEqualSlices(u8,list.items,&[_]u8{10,20,30});
}
test "ArrayList appendSlice" {
    var list=std.ArrayList(u8).init(al);
    defer list.deinit();
    const a=[_]u8{10,20};
    try list.appendSlice(&a);
    try list.append(100);
    try list.appendSlice(&a);
    try expect(list.items.len==5);
    const r=[_]u8{10,20,100,10,20};
    try expectEqualSlices(u8,list.items,&r);
}
test "ArrayList appendUnalignedSlice" {
    var list=std.ArrayList(u32).init(al);
    defer list.deinit();
    try list.append(100);
    const a align(1)=[_]u32{1,2,3};
    try list.appendUnalignedSlice(&a);
    try expect(list.items.len==4);
    const r=[_]u32{100,1,2,3};
    try expectEqualSlices(u32,list.items,&r);
}
test "ArrayList appendNTimes" {
    var list=std.ArrayList(u8).init(al);
    defer list.deinit();
    try list.append(10);
    try list.appendNTimes(50,4);
    try expect(list.items.len==5);
    const r=[_]u8{10,50,50,50,50};
    try expectEqualSlices(u8,list.items,&r);
}
test "ArrayList appendNTimesAssumeCapacity" {
    var list=try std.ArrayList(u8).initCapacity(al,100);
    defer list.deinit();
    try list.append(10);
    list.appendNTimesAssumeCapacity(50,4);
    try expect(list.items.len==5);
    const r=[_]u8{10,50,50,50,50};
    try expectEqualSlices(u8,list.items,&r);
}

appendAssumeCapcacity, appendSliceAssumeCapcacity, appendUnalignedSliceAssumeCapcacity, appendNTimesAssumeCapacity

表示调用时保证空余容量大于等于追加元素的个数。这样就不返回错误了,调用时不用 try 或 catch 。

6.4.4. 插入(insert)

插入是指把输入值插入索引n处,[n..]的元素向尾部移动。插入函数如下,记得调用时用 try 或 catch:

test_ArrayList_insert.zig

const std=@import("std");
const expectEqualSlices=std.testing.expectEqualSlices;
var al=std.testing.allocator;
test "ArrayList insert" {
    var list=std.ArrayList(u8).init(al);
    defer list.deinit();
    const a=[_]u8{1,2,3};
    try list.appendSlice(&a);
    try list.insert(0,66);
    const r1=[_]u8{66,1,2,3};
    try expectEqualSlices(u8,list.items,&r1);
    try list.insert(2,77);
    const r2=[_]u8{66,1,77,2,3};
    try expectEqualSlices(u8,list.items,&r2);
    try list.insert(list.items.len,88);
    const r3=[_]u8{66,1,77,2,3,88};
    try expectEqualSlices(u8,list.items,&r3);
}
test "ArrayList insertSlice" {
    var list=std.ArrayList(u8).init(al);
    defer list.deinit();
    const a=[_]u8{1,2,3};
    list.appendSlice(&a) catch unreachable;
    const i=[_]u8{44,55};
    list.insertSlice(1,&i) catch unreachable;
    const r=[_]u8{1,44,55,2,3};
    try expectEqualSlices(u8,list.items,&r);
}

6.4.5. 删除(remove)

把指定位置的元素删除,删除函数如下,因不返回错误,所以调用时不用 try :

尽可能的用 swapRemove 而不是 orderedRemove ,因为 swapRemove 运行效率更高。

test_ArrayList_remove.zig

const std=@import("std");
const expectEqualSlices=std.testing.expectEqualSlices;
const expect=std.testing.expect;
var al=std.testing.allocator;
test "ArrayList orderRemove" {
    var list=std.ArrayList(u8).init(al);
    defer list.deinit();
    const a=[_]u8{11,22,33,44};
    try list.appendSlice(&a);
    const b=list.orderedRemove(1);
    try expect(b==22);
    const r=[_]u8{11,33,44};
    try expectEqualSlices(u8,list.items,&r);
}
test "ArrayList swapRemove" {
    var list=std.ArrayList(u8).init(al);
    defer list.deinit();
    const a=[_]u8{11,22,33,44,55};
    try list.appendSlice(&a);
    const b=list.swapRemove(1);
    try expect(b==22);
    const r=[_]u8{11,55,33,44};
    try expectEqualSlices(u8,list.items,&r);
}
test "ArrayList popOrNull" {
    var list=std.ArrayList(u8).init(al);
    defer list.deinit();
    const a=[_]u8{11,22};
    try list.appendSlice(&a);
    try expect(list.items.len==2);
    var b=list.popOrNull();
    try expect(b.?==22);
    try expect(list.items.len==1);
    b=list.popOrNull();
    try expect(b.?==11);
    try expect(list.items.len==0);
    b=list.popOrNull();
    try expect(b==null);
    try expect(list.items.len==0);
}

6.4.6. 片段替换(replaceRange)

可以用一个切片,来替换 ArrayList 中部分元素。

调用方式: replaceRange(start:usize,l:usize,new_items:[]const T) !void

等同于从 ArrayList 的索引 start 处开始,先删掉 l 个元素,然后再插入 new_items。

如果 start+l > list.items.len ,则产生越界的未定义行为。把本例中最后一行的注释符去掉,测试中崩溃。

test_ArrayList_replaceRange.zig

const std=@import("std");
const expectEqualSlices=std.testing.expectEqualSlices;
var al=std.testing.allocator;
test "ArrayList replaceRange" {
    const rep=[_]u8{55,66,77};
    var list=std.ArrayList(u8).init(al);
    defer list.deinit();
    const a=[_]u8{1,2,3,4,5};

    try list.appendSlice(&a);
    try list.replaceRange(2,0,&rep);
    const r1=[_]u8{1,2,55,66,77,3,4,5};
    try expectEqualSlices(u8,list.items,&r1);
    
    list.items.len=0;
    try list.appendSlice(&a);
    try list.replaceRange(2,2,&rep);
    const r2=[_]u8{1,2,55,66,77,5};
    try expectEqualSlices(u8,list.items,&r2);

    list.items.len=0;
    try list.appendSlice(&a);
    try list.replaceRange(2,3,&rep);
    const r3=[_]u8{1,2,55,66,77};
    try expectEqualSlices(u8,list.items,&r3);
    
    list.items.len=0;
    try list.appendSlice(&a);
    //try list.replaceRange(2,4,&rep);
}

6.4.7. 改变长度(resize)

直接改变 items.len 的长度是危险的操作,用 resize 主要是增加长度,用 shrinkAndFree 主要是减少长度。

test_ArrayList_resize.zig

const std=@import("std");
const expectEqualSlices=std.testing.expectEqualSlices;
const expect=std.testing.expect;
var al=std.testing.allocator;
test "ArrayList resize" {
    var list=try std.ArrayList(u8).initCapacity(al,10);
    defer list.deinit();
    try list.append(11);
    try expect(list.items.len==1);
    try expect(list.capacity>=10);
    const cap=list.capacity;

    try list.resize(8);
    try expect(list.items.len==8);
    try expect(list.capacity==cap);

    try list.resize(2000);
    try expect(list.items.len==2000);
    try expect(list.capacity>=2000);
    try expect(list.capacity>=cap);
}
test "ArrayList shrinkAndFree" {
    var list=try std.ArrayList(u8).initCapacity(al,10);
    defer list.deinit();
    try list.appendSlice(&[_]u8{1,2,3,4});
    try expect(list.items.len==4);
    try expect(list.capacity>=10);

    list.shrinkAndFree(2);
    try expect(list.items.len==2);
    try expect(list.capacity==2);
    try expectEqualSlices(u8,list.items,&[_]u8{1,2});
}

6.4.8. 当做writer(as writer)

ArrayList 还可以当做字符串输出缓冲区。见本例。

test_ArrayList_writer.zig

const std=@import("std");
const expectEqualSlices=std.testing.expectEqualSlices;
var al=std.testing.allocator;
test "ArrayList writer" {
    var buf=std.ArrayList(u8).init(al);
    defer buf.deinit();
    const writer=buf.writer();
    try expectEqualSlices(u8,buf.items,"");
    const i:i32=42;
    try writer.print("i={}\n",.{i});
    try expectEqualSlices(u8,buf.items,"i=42\n");
    try writer.writeAll(" all");
    try expectEqualSlices(u8,buf.items,"i=42\n all");
    try writer.writeAll(" ok!");
    try expectEqualSlices(u8,buf.items,"i=42\n all ok!");
}

6.4.9. 0字节元素(0 byte item)

如果元素字节位长是0,则 ArrayList 并不用分配内存。

本例中,FailingAllocator.init的参数为0,表示如果有任何内存分配则出错。

test_ArrayList_0byte.zig

const std=@import("std");
const expect=std.testing.expect;
var al=std.testing.allocator;
test "std.ArrayList(u0)" {
    var fa=std.testing.FailingAllocator.init(al, 0);
    const fall=fa.allocator();
    var list=std.ArrayList(u0).init(fall);
    defer list.deinit();
    try list.append(0);
    try list.append(0);
    try expect(list.items.len==2);
    var count:usize=0;
    for (list.items) |x| {
        try expect(x==0);
        count += 1;
    }
    try expect(count==2);
}

6.5. 结构(struct)

结构由0到多个属性(field)、0到多个方法(method)、0到多个变量(const/var)组成。

定义语法:


struct {
    fieldname1:T1,fieldname2:T2,fieldname3:T3,
    fn method1(var list) T {statements;}
    fn method2(var list) T {statements;}
    const varname1:T=initval1;
    var varname2:T=initval2;
}

使用属性时,用结构变量名.属性名,如下一节示例中的 f.x ;

使用方法和静态变量时,用结构类型名.方法名(或静态变量名),如下一节示例中的 foo.nop() 和 foo.z 。

方法和静态变量并不在结构变量对应的内存单元中,放在结构内仅为了使用名字空间。

6.5.1. 结构赋值(struct assignment)

定义结构变量时,用2.4. 结构字面值(struct literal)赋初始值,或者用 undefined 稍后再赋值。

test_struct_decl1.zig

const expect=@import("std").testing.expect;
const foo=struct{
    x:i32,y:i32,
    fn nop() void {}
    const z:i64=-5;
};
test "struct decl"{
    var f:foo=.{.x=3,.y=4};
    try expect(f.x==3);
    f.x=f.x+f.y;
    try expect(f.x==7);
    try expect(foo.z==-5);
    foo.nop();
}
test "struct undefined"{
    var f:foo=undefined;
    try expect(foo.z==-5);
    f.x=13;
    f.y=foo.z;
    try expect(f.y==-5);
    foo.nop();
}

6.5.1.1. 默认属性值(default field value)

属性可以用在编译期执行的表达式来设默认初始值,这些属性在变量定义时可以省略。

run_defaultfieldvalues.zig

const expect=@import("std").testing.expect;
const foo=struct{a:i32=15, b:i32};
test "default field value" {
    var f:foo=.{.b=10};
    try expect(f.a==f.b+5);
    var f1:foo=.{.a=5,.b=6};
    try expect(f1.a+f1.b==11);
}

6.5.1.2. 结构类型推导(struct infer type)

结构的类型可以根据匿名结构字面值推导出来。

test_struct_anon.zig

const expect=@import("std").testing.expect;
test "fully anonymous struct"{
    try dump(.{.int = @as(u32, 1234),
        .float = @as(f64, 12.34),
        .b = true,
        .s = "hi",});
}
fn dump(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');
}

本例中的匿名结构字面值是有属性名的,而4.5. 元组(tuple)中的示例是

.{@as(u32,1234),@as(f64,12.34),true,"hi"}

没有属性名,所以其属性名是数字0,1,2。

6.5.2. 子结构(substruct)

结构定义内可以嵌套子结构定义。

test_struct_nesting.zig

const expect=@import("std").testing.expect;
const foo=struct{
    x:i32,
    y:struct{m:f32,
        n:struct{i:bool},
    },
};
test "struct nesting"{
    var f:foo=.{.x=1,.y=.{.m=3.2,.n=.{.i=true}}};
    try expect(f.y.m==3.2);
    try expect(f.y.n.i);
}

6.5.3. 结构名字(struct name)

所有的结构都是匿名的。根据定义方式,可以用变量表达式、返回函数值、匿名结构字面值来命名结构类型名称。

本例中,foo是变量表达式定义的,其中y是foo的子结构;

list(i32)是函数返回值定义的,名字中的参数i32是编译期确定的;这种方式通常用于泛型实现。

struct{}是匿名结构,名字是struct_2576,2576是程序随机分配的数字。

run_struct_name.zig

const print=@import("std").debug.print;
const foo=struct{x:i32, y:struct{i:i8}};
fn list(comptime T: type) type {
    return struct{z:T};
}
pub fn main() void{
    const f=.{.x=1,.y=.{.i=@as(i8,2)}};
    print("variable: {s}\n",.{@typeName(foo)});
    print("sub struct:{s}\n",.{@typeName(@TypeOf(f.y))});
    print("anonymous:{s}\n", .{@typeName(struct{})});
    print("function:{s}\n", .{@typeName(list(i32))});
}
shell

$ zig run run_struct_name.zig
variable: run_struct_name.foo
sub struct:struct{i: i8}
anonymous:run_struct_name.main__struct_2576
function:run_struct_name.list(i32)

6.5.4. 结构比特位长(struct size)

结构类型对应的内存单元(即比特位长)仅包括其属性,不包括结构内定义的静态变量和函数。

test_struct_size.zig

const expect=@import("std").testing.expect;
const foo=struct{
    x:i32,
    const y:i64=5;
    fn nop() void {}
};
test "struct size"{
    var f:foo=.{.x=1};
    const s=@sizeOf(@TypeOf(f.x));
    try expect(@sizeOf(foo)==s);
}

出于优化需要,普通结构不保证结构比特位长等于属性比特位长之和,也不保证内存中属性顺序和定义一致,但保证属性是基于C语言ABI对齐的。

test_struct_size_equal.zig

const expect=@import("std").testing.expect;
const foo=struct{
    x:i32,y:i8,z:i64,
};
test "size not equal"{
    const f:foo=.{.x=1,.y=2,.z=3};
    const sx=@sizeOf(@TypeOf(f.x));
    const sy=@sizeOf(@TypeOf(f.y));
    const sz=@sizeOf(@TypeOf(f.z));
    try expect(sx==4);
    try expect(sy==1);
    try expect(sz==8);
    const s=@sizeOf(foo);
    try expect(s==16);
    try expect(s != sx+sy+sz);
}
const foo1=struct{x:i8,y:i8};
test "size equal"{
    const f:foo1=.{.x=11,.y=12};
    const sx=@sizeOf(@TypeOf(f.x));
    const sy=@sizeOf(@TypeOf(f.y));
    try expect(sx==1);
    try expect(sy==1);
    const s=@sizeOf(foo1);
    try expect(s==2);
    try expect(s == sx+sy);
}

6.5.5. 方法(method)

结构内定义的函数称为方法,使用structname.method(a,b)来调用。

当方法的第1个参数是本结构自身类型,则可以用简略调用方式:varname.method(b)来调用。

本例中,f是foo类型变量,则 f.plus(15) 等同于 foo.plus(f,15)

use_structmethods.zig

const expect=@import("std").testing.expect;
const foo=struct{x:i32,
    fn init(x:i32) foo{
        return foo{.x=x};
    }
    fn plus(self:foo,p:i32) i32{
        return self.x+p;
    }
};
test "struct method self" {
    const f=foo.init(1);
    try expect(f.x==1);
    const i=foo.plus(f,15);
    const j=f.plus(15);
    try expect(i==j);
}

6.5.6. 属性数量0(0 fields)

结构内的定义语句、变量不占用结构空间,如果结构的属性数量为0,则结构的比特位长为0。

此种结构常用于名字空间和静态变量。

test_empty_struct.zig

const expect=@import("std").testing.expect;
const empty=struct{};
pub const mathconst=struct {
    pub const pi=3.14;
    pub const e=2.72;
};
test "null struct" {
    const nothing:empty=.{};
    try expect(0==@sizeOf(@TypeOf(nothing)));
    try expect(@sizeOf(mathconst)==0);
    const c=2*mathconst.pi;
    try expect(c==6.28);
}

6.5.7. 从属性指针获取结构指针(get struct pointer from field pointer)

结构属性的顺序由编译器根据优化需要而定,所以须使用@fieldParentPtr函数,根据属性指针得到结构的指针。

6.5.7.1. @fieldParentPtr

@fieldParentPtr(comptime ParentType:type,comptime field_name:[]const u8,field_ptr:*T) *ParentType

说明:根据属性指针 filed_ptr,返回 struct 指针。

示例:

test_fieldParentPtr.zig

const expect=@import("std").testing.expect;
const point=struct{x:f32,y:f32};
fn setYBasedOnX(ptrx:*f32,v:f32) void {
    const p=@fieldParentPtr(point,"x",ptrx);
    p.y=v;
}
test "fieldParentPtr" {
    var p=point{.x=0.1234,.y=undefined};
    setYBasedOnX(&p.x,0.9);
    try expect(p.y==0.9);
}

6.5.8. 压缩结构(packed struct)

在 struct 前加 packed 定义压缩结构。

与普通结构不同,压缩结构内存布局保证如下:

run_packed_struct.zig

const print=@import("std").debug.print;
const foo=packed struct{
    x:i8=1,
    y:i16=0x0202,
    z1:bool=true,
    z2:u3=0b000,
    z3:u4=0b1111,
    m:i16=0x0303,
};
pub fn main() void{
    const f:foo=.{};
    const ptr=@ptrCast(*[@sizeOf(foo)]u8,&f);
    const s=ptr.*;
    for(s) |e,i| {
        if(i==3){
            print("{b} ",.{e});
        }else{
            print("{x} ",.{e});
        }
    }
    print("\nstruct foo size is {}\n",.{@sizeOf(foo)});
}
shell

$ zig run test_packed_struct.zig
1 2 2 11110001 3 3 0 0
struct foo size is 8

把本例中的packed去掉,再运行输出如下,其中aa是填充字节,bool和u3 u4类型均按1个字节存储:

shell

$ zig run test_packed_struct.zig
1 aa 2 10 1 0 f aa 3 3
struct foo size is 10

注意:用易变压缩结构可能会编译错误,后续会改进。详见[issue 1761](https://github.com/ziglang/zig/issues/1761)。

6.5.8.1. 重新解读内存(reinterpret memory)

压缩结构可以用@bitCast或@ptrCast重新解读内存信息,在编译期也适用。

下例中用@bitCast把Full中的x,重新解读为Divided中的half, i1, i2。

test_packed_struct.zig

const native_endian = @import("builtin").target.cpu.arch.endian();
const expect = @import("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();
    comptime try doTheTest();
}
fn doTheTest() !void {
    try expect(@sizeOf(Full) == 2);
    try expect(@sizeOf(Divided) == 2);
    var full = Full{ .number = 0x1234 };
    var divided = @bitCast(Divided, full);
    switch (native_endian) {
        .Big => {
            try expect(divided.half1 == 0x12);
            try expect(divided.quarter3 == 0x3);
            try expect(divided.quarter4 == 0x4);
        },
        .Little => {
            try expect(divided.half1 == 0x34);
            try expect(divided.quarter3 == 0x2);
            try expect(divided.quarter4 == 0x1);
        },
    }
}

6.5.8.2. 非字节对齐属性(non-byte-aligned field)

对结构的非字节对齐属性(类似于C语言中的位域),可正常读写,也可以用@bitOffsetOf和@offsetOf来查看其在结构中的位置。

test_non_byte_aligned_field.zig

const expect=@import("std").testing.expect;
const BitField=packed struct{
    a: u3,b: u3,c: u2,d:u16};
var foo=BitField{.a=1,.b=2,.c=3,.d=7};
test "non-bit-aligned field"{
    var ptr=&foo;
    try expect(ptr.*.b==2);
    comptime{
        try expect(@bitOffsetOf(BitField,"a")==0);
        try expect(@bitOffsetOf(BitField,"b")==3);
        try expect(@bitOffsetOf(BitField,"c")==6);
        try expect(@bitOffsetOf(BitField,"d")==8);
        try expect(@offsetOf(BitField,"a")==0);
        try expect(@offsetOf(BitField,"b")==0);
        try expect(@offsetOf(BitField,"c")==0);
        try expect(@offsetOf(BitField,"d")==1);
    }
}

可以把非字节对齐属性取址赋值给指针。但这个指针不能像普通指针一样做函数参数。

指向非字节对齐属性的指针与其宿主整数中的其他字段共享相同的地址:

test_non_byte_aligned_field_ptr.zig

const expect=@import("std").testing.expect;
const BitField=packed struct{
    a: u3,b: u3,c: u2,d:u8};
var foo=BitField{.a=1,.b=2,.c=3,.d=5};
test "pointers of sub-byte-aligned fields share addresses" {
    try expect(@ptrToInt(&foo.a) == @ptrToInt(&foo.b));
    try expect(@ptrToInt(&foo.a) == @ptrToInt(&foo.c));
}
test "pointer to non-byte-aligned field" {
    const ptr = &foo.c;
    try expect(ptr.* == 3);
}

6.5.9. 外部结构(extern struct)

外部结构的内存布局保证匹配目标的C语言ABI接口。外部结构只适用于与C语言ABI接口兼容。

定义外部结构是在类型定义中的 struct 前加 extern 。

6.6. 枚举(enum)

类似于C语言中的枚举,枚举是由0到多个属性组成,属性对应的整数值默认从0开始,逐个加1。

类似于结构定义,枚举中也可以定义函数和静态变量。

枚举赋值可以用2.6. 枚举字面值(enum literal)或其它枚举类型变量。

如果变量类型已确定,赋值时可以省略枚举类型。

test_enum.zig

const expect=@import("std").testing.expect;
const state=enum{ok,notok,
    const p:i32=1;
    fn nop() void{}
};
test "enum"{
       try expect(@enumToInt(state.ok)==0);
       try expect(@enumToInt(state.notok)==1);
    try expect(u1==@TypeOf(@enumToInt(state.ok)));
    const s=state.ok;
    const s1:state=.ok;
    try expect(s==s1);
    try expect(state.p==1);
    state.nop();
}

通常用switch语句处理枚举类型,在分支判断处可省略枚举类型。

test_enum_switch.zig

const expect=@import("std").testing.expect;
const state=enum{ok,notok};
test "enum switch" {
    var i=state.ok;
    var r=switch(i){
        .ok=>true,
        .notok=>false,
    };
    try expect(r);
}

6.6.1. 枚举标记类型(enum tag type)

枚举的默认标记类型是能容纳属性的最大整数值的无符号整数类型,比如上一节示例中对应的 u1 类型。

也可以指定枚举的标记类型,语法为:enum(T)

可设定某属性的标记为特定整数值,设定后其后的属性对应的整数值在此基础上逐个加1。语法为: filedname=value

test_enum_tag_type.zig

const expect=@import("std").testing.expect;
const foo=enum(u32){i1=100,
    i2,i3,
    i4=1100};
test "set enum ordinal value" {
    try expect(@enumToInt(foo.i1)==100);
    try expect(@enumToInt(foo.i2)==101);
    try expect(@enumToInt(foo.i3)==102);
    try expect(@enumToInt(foo.i4)==1100);
}

6.6.2. 枚举方法(enum method)

与结构类似,枚举内也可以定义函数做为枚举的方法,并且也可以用简略调用方式。

test_enum_method.zig

const expect=@import("std").testing.expect;
const foo=enum{ok,not_ok,
    fn isok(self:foo) bool{
        return self==foo.ok;}
};
test "Enum Methods"{
    const f=foo.ok;
    try expect(f.isok());
    try expect(foo.isok(f));
}

6.6.3. 非穷尽枚举(non-exhaustive enum)

最后属性为 _ 的是非穷尽枚举,非穷尽枚举必须指定标记类型。

switch处理非穷尽枚举,可以用'_'分支替代else分支,此时switch须分析所有已知属性,否则会编译错误。

test_non_exha_enum.zig

const expect=@import("std").testing.expect;
const foo=enum(u8){one,two,
    _,};
test "switch on non-exhaustive enum"{
    var f=foo.one;
    var r=switch(f){
        .one=>true,
        .two=>false,
        _=>false,
    };
    try expect(r);
    r=switch(f){
        .one=>true,
        else=>false,
    };
    try expect(r);
    f=@intToEnum(foo,12);
    r=switch(f){
        .one=>false,
        .two=>false,
        _=>true,
    };
    try expect(r);
}

在非穷尽枚举上使用 @intToEmum ,会影响 @intCast 整数标记类型的安全语义。

6.6.4. 外部枚举(extern enum)

枚举不保证与C语言ABI接口兼容。

exenum.zig

const foo=enum{a,b,c};
export fn entry(f:foo) void {_=foo;}
shell

zig build-obj exenum.zig
exenum.zig:4:17: error: parameter of type 'exenum.foo' not allowed in function with calling convention 'C'
export fn entry(f:foo) void {_=f;}
                ^``
exenum.zig:4:17: note: enum tag type 'u2' is not extern compatible
exenum.zig:4:17: note: only integers with power of two bits are extern compatible
exenum.zig:3:11: note: enum declared here
const foo=enum{a,b,c};
          ^``

明确枚举的标签类型,可以与C语言ABI接口兼容,如下例明确为c_int。

exenum1.zig

const foo=enum(c_int){a,b,c};
export fn entry(f:foo) void {_=foo;}
shell

zig build-obj exenum.zig

6.5. 联合(union)

联合是指对1块内存区域用不同类型表示。联合的属性可以是不同类型。

联合的语法和结构枚举类似,需要用 union 关键字。

联合也可以定义静态变量,也可以定义函数做为方法,方法也可使用简略调用方式。

test_union.zig

const expect=@import("std").testing.expect;
const expect=@import("std").testing.expect;
const foo=union{
    score:u8,notscore:void,
    fn pass(self:foo) bool{
        return self.score>=60;
    }
};
test "union" {
    var f1=foo{.score=70};
    try expect(foo.pass(f1));
    f1.score=55;
    try expect(!f1.pass());
}

6.5.1. @unionInit

@unionInit(comptime Union:type,comptime active_field_name:[]const u8,init_expr) Union

说明:等同于联合初始化语法相同,只不过属性名参数是编译期可知的字符串,而不是标识符。

test_unionInit.zig

const expect=@import("std").testing.expect;
const foo=union{x:i32,y:f64};
test "@unionInit" {
    const i=foo{.x=10};
    const j=@unionInit(foo,"x",10);
    try expect(i.x==j.x);
}

6.5.2. 激活属性(active field)

普通联合只能使用第一次赋值的属性,也就是激活属性。

本例中 .i是激活的属性,所以不能使用 .f属性。

test_notactive_field.zig

test "not active field" {
    const foo=union{i:i64,f:f64,b:bool,};
    var f1=foo{.i=1234};
    f1.i+=1;
    var f2=f1.f;
    _=f2;
}

$ zig test test_notactive_field.zig
Test [1/1] test.enum switch... thread 7432 panic: access of inactive union field

可给变量重新赋值改变激活属性。

test_reassign_field.zig

test "reassign active field" {
    const foo=union{i:i64,f:f64,b:bool,};
    var f1=foo{.i=1234};
    f1=foo{.f=12.5};
}

也可以使用@ptrCast、外部联合或压缩联合,来访问非激活属性。

test_packed_notact_field.zig

test "packed not active field" {
    const foo=packed union{i:i64,f:f64,b:bool,};
    var f1=foo{.i=1234};
    const f2=f1.f;
    _=f2;
}

6.5.3. 标记联合(tagged union)

标记联合是用枚举类型来定义联合的标记。语法是:union(enumT)

用作标记的枚举类型和联合类型的属性名须完全一致。

普通联合不能用switch语句处理,标记联合可以。

6.5.3.1. switch语句处理

可用switch语句处理标记联合,分支判断部分可以省略类型名称。

用switch语句处理标记联合,捕获时用指针才可以改写其值。

test_tagged_union_switch.zig

const expect=@import("std").testing.expect;
const fooTag=enum{ok,notok};
const foo=union(fooTag){ok:u8,notok:void};
test "switch on tagged union" {
    const f=foo{.ok=5};
    switch (f){
        fooTag.ok => |v| try expect(v==5),
        fooTag.notok => unreachable,
    }
}
test "modify tagged union in switch" {
    var f=foo{.ok=6};
    switch (f){
        .ok => |*v| v.* += 1,
        .notok => unreachable,
    }
    try expect(f.ok==7);
}

6.5.3.2. 推导枚举标记类型(infer enum tag type)

可以根据联合属性名,自动推导对应的枚举标记,来生成标记联合。语法是:union(enum)

test_union_method.zig

const expect=@import("std").testing.expect;
const foo=union(enum){
    i:i32, b:bool,none,
    fn truthy(self:foo) bool{
        return switch(self){
            foo.i => |x| x!=0,
            foo.b => |x| x,
            foo.none => false,
        };
    }
};
test "union method"{
    var f1=foo{.i=1};
    var f2=foo{.b=false};
    try expect(f1.truthy());
    try expect(!f2.truthy());
}

6.5.3.3. 标记联合类型转换(tagged union cast)

标记联合类型可用@as函数转换为标记的枚举类型。

标记联合类型值可以强制转换为标记值。

test_tagged_union_cast.zig

const expect=@import("std").testing.expect;
const fooTag=enum{ok,notok};
const foo=union(fooTag){ok:u8,notok:void};
test "cast tag type"{
    const f=foo{.ok=2};
    try expect(@as(fooTag,f)==fooTag.ok);
}
test "coerce to enum" {
    const f1=foo{.ok=4};
    const f2=foo.notok;
    try expect(f1==.ok);
    try expect(f2==.notok);
}

6.5.4. 其它union(others union)

extern union 可与目标C语言ABI接口兼容。

packed union 可以定义在 packed struct 中。

7. 其它类型(other type)

其它类型还有可选类型、错误联合类型、特定值结尾类型,以及 void 等类型。

7.1. 可选类型(optional)

狭义的NULL引用意思是当指针值为 null 时,仍然按指向正常地址的逻辑运行;

广义的NULL引用意思是某个变量的值处于无意义状态,仍然按正常值的逻辑运行。

这是引起许多诡异和严重的运行异常原因之一。

根源是把变量的正常值和无意义状态(也就是空值)混在一起处理。所以Zig语言设计了可选类型。

可选类型的语法: ?T 。T称为载荷类型。

可选类型的计算操作可以是赋值、比较、取载荷。

可选类型不能和其它类型直接进行加法等其它计算操作。

test_optional_add1.zig

test "optional add UB" {
    var i:?i32=1;
    i+=1;
}

error: invalid operands to binary expression: 'Optional' and 'Int'

可选类型之间也不能直接进行加法等其它计算操作。

test_optional_add2.zig

test "optional add UB" {
    var i:?i32=1;
    var j:?i32=2;
    i+=j;
}

error: invalid operands to binary expression: 'Optional' and 'Optional'

名词解释:

载荷(:payload) 可选类型、错误联合类型等组合类型中的,其有效值的类型。

7.1.1. 可选类型赋值(optional assignment)

可以给可选类型变量赋值为载荷类型的值,也可以赋值为 null ,也可赋值为另1个可选类型值。

test_optional_assign.zig

const expect=@import("std").testing.expect;
var opt:?i32=undefined;
test "assignment payload" {
    const i:i32=10;
    opt=i;
    try expect(opt==@as(?i32,10));
}
test "assignment null" {
    opt=null;
    try expect(opt==null);
}
test "assignment other optional" {
    const i:?i32=11;
    opt=i;
    try expect(opt==@as(?i32,11));
}

不能把可选类型的值直接赋给普通变量。

test_opt_assign_payload.zig

const expect=@import("std").testing.expect;
test "payload assignment" {
    var i:?i32=10;
    var j:i32=i;
    try expect(i==j);
}

error: expected type 'i32', found '?i32'

不能把 null 赋给普通类型变量。

test_assign_null.zig

test "get null payload" {
    var j:i32=null;
    _=j;
}

error: expected type 'i32', found '@TypeOf(null)'

7.1.2. 可选类型比较(optional comparison)

相同的可选类型之间可以互相比较。

可选类型可以和其载荷类型互相比较。

test_optional_compare.zig

const expect=@import("std").testing.expect;
const i:?i32=10;
test "optional comparison" {
    var j:?i32=20;
    try expect(i!=j);
    j=10;
    try expect(i==j);
}
test "payload comparison" {
    var k:i32=30;
    try expect(i!=k);
    k=10;
    try expect(i==k);
}

7.1.2.1. 空值比较运算(null comparison operator)

语法: a==null a!=null

操作数类型:可选类型(optional)

说明:如果a是null返回true,否则返回false。

null只能和可选类型比较,不可以和载荷类型比较。

示例:

test_null_compare.zig

const expect=@import("std").testing.expect;
test "null comparison" {
    var i:?i32=10;
    try expect(i!=null);
    i=null;
    try expect(i==null);
}

7.1.3. 取可选类型载荷(get optional payload)

语法: a.?

操作数类型:可选类型(optional)

说明:等同于 a orelse unreachable,如果a是载荷值则取出,如果a是 null ,则崩溃。

可选类型载荷取出后,可以像正常值或变量一样运算。

示例:

test_get_payload.zig

const expect=@import("std").testing.expect;
test "get payload" {
    var i:?i32=10;
    try expect(i.?==10);
    i.? +=5;
    try expect(i.?==15);
    const j=i.?;
    try expect(@TypeOf(j)==i32);
    try expect(j==15);
}

当可选类型是 null 时取载荷,发生未定义行为。

test_null_payload.zig

test "get null payload" {
    var i:?i32=null;
    _=i.?;
}

Test [1/1] test.get null payload... thread 11472 panic: attempt to use null value

7.1.4. null值(null value)

test_optional_size.zig

const expect=@import("std").testing.expect;
test "optional size" {
    comptime try expect(@sizeOf(?*i32) == @sizeOf(*i32));
}
test "null value" {
    var i:?i32=null;
    var j=@ptrCast(*i32,&i);
    try expect(j.*==0);
}
test "null not equal to 0" {
    var i:?i32=null;
    var j:i32=0;
    try expect(i!=j);
}

7.1.5. null指针(null pointer)

不用 null 会使源代码变冗长,这里比较一下C语言和Zig语言代码见下面例子。

任务: 调用malloc函数的结果如果为null,则返回null。

call_malloc.c

#include <stdlib.h>
struct foo *do(void){
    char *ptr=malloc(1234);
    if(!ptr) return null;
    // ...
}
call_malloc.zig

extern fn malloc(size:size_t) ?*u8;
fn do() ?*foo{
    const ptr=malloc(1234) orelse return null;
    // ...
}

这表明,Zig语言至少和C语言一样方便。 orelse 解包裹可选类型取出其载荷类型,所以ptr在函数中保证是非null。

另一种处理NULL的方式是:

check_null.c

void do_foo(struct foo *f) {}
void do(struct foo *f){
    // do some stuff
    if(f) {
        do_foo(f);
    }
    // do some stuff
}
check_null.zig

do_foo(f: *foo) void{_=f;}
do(f: ?*foo) void{
    // do some stuff
    if(f) |ptr| {
        do_foo(ptr);
    }
    // do some stuff
}

在if语句中,捕获f的值为ptr时,不再是可选类型指针,而是不能为 null 的普通指针。

这样有指针参数的函数,在GCC中可以用 __attribute__((nonnull)) 属性进行注释,编译时可更好优化。

7.1.6. orelse运算(orelse operator)

语法: a orelse b

操作数类型:可选类型(optional)

说明:如果a是null,则返回b;否则返回a的载荷值。b也可以是类型为noreturn的值。

示例:

test_orelse.zig

const expect=@import("std").testing.expect;
test "orelse payload" {
    var i:?i32=5;
    const j=i orelse 10;
    try expect(j==5);
    try expect(@TypeOf(j)==i32);
}
test "orelse null" {
    var i:?i32=null;
    const j=i orelse 10;
    try expect(j==10);
    try expect(@TypeOf(j)==i32);
}

7.2. 错误(error)

主流的错误处理有返回错误码、异常处理两种方式。返回错误码实现简单效率高,但把正常值和错误码混到一起,易出错;异常处理实现复杂功能丰富,但效率略低且错误处理流程不易直观阅读和分析。

Zig等现代程序设计语言往往使用错误联合类型的方式来处理错误,尽可能兼顾错误码和异常处理的优点。

错误用标识符命名,名字相同则是相同的错误,其对应整数编码也相同。

错误的类型是其所属的错误集。参看下一节。

整个编译期间,所有不同的错误名统一被编码为大于0的无符号整数。现阶段被硬编码为 u16 类型,计划将来改为由不重复错误值数量来确定错误编码类型。参见issues #786

从错误集中取错误,简略方式是用 setname.errorname 语法,标准方式是(error{a,b}).a,还可以用error.errorname

如果可以根据初始值推导出来错误集类型,则变量定义时可省略类型。

test_error_encode.zig

const expect=@import("std").testing.expect;
const err1=error{erra0,erra1};
const err2=error{erra0};
test "error encode" {
    const x:err1=err1.erra0;
    const y=err2.erra0;
    try expect(x==y);
    const z=(error{erra0,erra1}).erra1;
    try expect(err1.erra0!=z);
    const w=error.erra0;
    const i=@errorToInt(w);
    const j=@errorToInt(w);
    try expect(i==j);
    try expect(@TypeOf(i)==u16);
}

错误只能进行赋值和比较运算。

7.2.1. 错误集类型(error set type)

语法: error{a,b,...}

由1到多个错误名组成的集合,称为错误集类型。

可以同时存在多个错误集,不同的错误集可以包含同样的错误名。如上一节示例中,err1错误集和err2错误集均包含erra0错误。

7.2.1.1. 错误集的超集和子集(error superset and subset)

如果某错误集内所有错误名都包含在另一个错误集内,则称这个错误集为子集,另一个错误集为超集。

可以将错误从子集类型转换为超集类型,不可将超集类型转换为子集类型。

本例中,AllocationError是子集,FileOpenError是超集,可将错误类型由前者转换为后者。

test_error_subtosuper.zig

const expect=@import("std").testing.expect;
const FileOpenError=error{
    AccessDenied,
    OutOfMemory,
    FileNotFound,
};
const AllocationError=error{
    OutOfMemory,
};
test "coerce subset to superset" {
    const err= foo(AllocationError.OutOfMemory);
    try expect(err==FileOpenError.OutOfMemory);
}
fn foo(err:AllocationError) FileOpenError{
    return err;
}

本例中,把错误从超集类型转换为子集类型,则编译出错中止。

test_err_supertosub.zig

const superset=error{err1,err2,err3};
const subset=error{err2};
test "coerce superset to subset"{
    foo(superset.err2) catch {};
}
fn foo(e:superset) subset{
    return e;
}
shell

error: expected type 'error{err2}', found 'error{err1,err2,err3}'
 return e;
        ^

7.2.1.2. 全局错误集(anyerror)

anyerror 关键字是全局错误集,包含所有错误,是所有错误集的超集。

可将任何错误集转换为全局错误集。也可将全局错误集明确转换为其它错误集,这将插入语言级断言,以确保错误值实际存在于转换后的错误集中。

使用全局错误集,在编译时无法获知会出现哪些error。而在编译时能获知错误集,可有助于生成文档和有用错误信息,所以应避免使用全局错误集。

名词解释:

断言(:assert) 断言是指如果表达式值为假,则用调试模式生成的二进制程序会运行中止,发布模式生成的通常不会中止。

7.2.1.3. 合并错误集(merging error set)

语法: a||b

操作数类型:错误集合类型(error set)

说明:用 || 可将两个错误集合并,结果包含了两个错误集的错误。

这对根据编译期分支返回不同错误集的函数特别有用,例如Zig std打开文件的错误集是 LinuxFileOpenError || WindowsFileOpenError。

示例:

本例也演示了if和switch组合起来,处理error。

test_merage_errset.zig

const A = error{
    NotDir,
    PathNotFound,
};
const B = error{
    OutOfMemory,
    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 => {},
    }
}

7.2.2. 错误联合类型(error union type)

语法: errset ! T! T

错误集类型和普通类型组合成错误联合类型,表示要么是1个普通类型,要么是1个错误集类型。

错误集类型可以省略,表示由值来推导出错误集类型。

通常错误联合类型比错误集类型更常用。

test_error_union.zig

const expect=@import("std").testing.expect;
const foo=error{notbool,notint};
fn intobool(i:i32) foo!bool{
    if(i>0) {
        return true;
    }else if(i==0){
        return false;
    }else{
        return error.notbool;
    }
}
test "error union type"{
    var r=try intobool(10);
    try expect(r==true);
    var e1:foo=undefined;
    _=intobool(-10) catch |e| {e1=e;};
    try expect(e1==foo.notbool);
}

7.2.2.1. 推导错误集(infer error set)

当返回值的错误联合类型中省略错误集定义,则会转换为类型 anyerror!T,此时会尽可能推导出具体的错误集类型。

在获取函数指针、不同编译构建目标之间错误集保持一致等场景下,错误集推导不太友好。另外错误集推导不兼容递归。所以建议明确定义错误集。

test_err_infer.zig

const expect=@import("std").testing.expect;
pub fn add_inferred(comptime T:type,a:T,b:T) !T{
    var r:T=undefined;
    return if(@addWithOverflow(T,a,b,&r)) error.Overflow else r;
}
pub fn add_explicit(comptime T:type,a:T,b:T) Err!T{
    var r:T=undefined;
    return if(@addWithOverflow(T,a,b,&r)) error.Overflow else r;
}
const Err=error{Overflow};
test "inferred error set" {
    if (add_inferred(u8,255,1)) |_| unreachable
        else |err| switch (err) {
            error.Overflow => {},
        }
    if (add_explicit(u8,255,1)) |_| unreachable
        else |err| switch (err) {
            error.Overflow => {},
        }
}

7.2.2.2. 抓取(catch)

语法:a catch b a catch |err| b

操作数类型:错误联合类型(error union)

说明:如果a是错误,则返回b,否则返回a的载荷值。注意b也可以是类型为noreturn的值。

err是捕获到的错误,其作用域是在表达式b范围内。

示例:

test_error_catch.zig

const expect=@import("std").testing.expect;
test "error union catch"{
    var a:anyerror!i32=error.notbool;
    const b1:i32=a catch 13;
    try expect(b1==13);
    const b4:i32=a catch |e| @errorToInt(e);
    try expect(b4>=0);
    a=1;
    const b2=a catch 13;
    try expect(b2==1);
    const b3=a catch unreachable;
    try expect(b3==1);
}

7.2.2.3. 尝试(try)

语法:try a

说明:变量为正常值时继续,错误时返回。

const b=try a;

等同于:

const b=a catch |e| return e;

函数内有try,则函数的返回值必须是错误联合类型。

如果函数没必要返回错误联合类型,则用catch关键字。

比如,如果确定表达式不会出错,则可以:

const b=a catch unreachable;

7.2.3. 错误追踪(error trace)

7.2.3.1. 错误返回追踪(error return trace)

错误返回追踪是在函数中用try后,当有错误发生时,显示代码中返回错误的所有函数调用点。

run_err_trace.zig

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.EONE => try hello(),}
}
fn baz() !void {
    try bang1();
}
fn quux() !void {
    try bang2();
}
fn hello() !void {
    try bang2();
}
fn bang1() !void {
    return error.EONE;
}
fn bang2() !void {
    return error.ETWO;
}
shell

$ zig run run_err_trace.zig
error: ETWO
run_err_tracerun_err_trace.zig:26:5: 0x7ff6be80123e in bang1 (run_err_trace.exe.obj)
    return error.EONE;
    ^
run_err_tracerun_err_trace.zig:17:5: 0x7ff6be801345 in baz (run_err_trace.exe.obj)
    try bang1();
    ^
run_err_tracerun_err_trace.zig:29:5: 0x7ff6be80136e in bang2 (run_err_trace.exe.obj)
    return error.ETWO;
    ^
run_err_tracerun_err_trace.zig:23:5: 0x7ff6be8013e5 in run_err_trace (run_err_trace.exe.obj)
    try bang2();
    ^
run_err_tracerun_err_trace.zig:14:31: 0x7ff6be801484 in bar (run_err_trace.exe.obj)
        error.EONE => try run_err_trace(),}
                              ^
run_err_tracerun_err_trace.zig:8:18: 0x7ff6be8014e5 in foo (run_err_trace.exe.obj)
    if (x >= 5) {try bar();
                 ^
run_err_tracerun_err_trace.zig:5:5: 0x7ff6be80153a in main (run_err_trace.exe.obj)
    try foo(12);
    ^

仔细分析上面例子,当foo(12)时,其函数调用栈和return error过程如下,try语句返回error的步骤用 # 标记。

带 # 的行和执行程序时屏幕显示内容顺序一样。


main foo bar baz bang1
#bang1 return EONE
#in baz: try bang1(); return EONE
in bar: error.EONE => try hello()
#    bang2() return ETWO
#    in hello: try bang2() return ETWO
#in bar: try hello() return ETWO
#in foo: try bar() return ETWO
#in main: try foo() returen ETWO

虽然程序最后返回的错误是ETWO,但第一个返回的错误是EONE,在bar函数中,switch返回ETWO。错误返回追踪清楚的表明了函数间返回错误的全过程,使程序的错误处理更易于解读、分析和调试。

7.2.3.2. 错误返回追踪激活方式(error return trace activation mode)

错误返回追踪在Debug 和 ReleaseSafe 构建模式下默认启用,在ReleaseFast 和 ReleaseSmall 构建模式下默认禁用。

激活错误返回追踪的方式有:

7.2.3.2.1. @errorReturnTrace

@errorReturnTrace() ?*builtin.StackTrace

如果程序是用错误返回追踪构建的,且在1个函数(其中调用了返回值是错误或错误联合类型的函数)中@errorReturnTrace被调用,则返回栈追踪对象,否则返回null。

7.2.3.3. 栈追踪(stack trace)

本例把所有的错误处理(try和!)等都去掉。程序执行崩溃后栈追踪。

分析输出行可看出,栈追踪没有显示baz函数在中间的作用。

如果要debug,必须打开调试器或分析测试代码,这样就不如错误返回追踪。

run_stack_trace.zig

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("ETWO");
}
shell

$zig run run_stack_trace.zig
thread 10116 panic: ETWO
D:/ziglearn/run_stack_trace.zig:29:5: 0x7ff78a2720c6 in bang2 (run_stack_trace.exe.obj)
    @panic("ETWO");
    ^
D:/ziglearn/run_stack_trace.zig:23:10: 0x7ff78a29e15e in run_stack_trace (run_stack_trace.exe.obj)
    bang2();
         ^
D:/ziglearn/run_stack_trace.zig:14:14: 0x7ff78a272090 in bar (run_stack_trace.exe.obj)
        run_stack_trace();}
             ^
D:/ziglearn/run_stack_trace.zig:8:21: 0x7ff78a2717e3 in foo (run_stack_trace.exe.obj)
    if (x >= 5) {bar();
                    ^
D:/ziglearn/run_stack_trace.zig:5:8: 0x7ff78a2712e3 in main (run_stack_trace.exe.obj)
    foo(12);
       ^
C:\zig\lib\std\start.zig:385:41: 0x7ff78a271297 in WinStartup (run_stack_trace.exe.obj)
    std.debug.maybeEnableSegfaultHandler();
                                        ^
???:?:?: 0x7ffa7b1f7033 in ??? (???)
???:?:?: 0x7ffa7c5a26a0 in ??? (???)

名词解释

栈(:stack) 栈是一种先进后出的队列,始终在尾部写入和弹出。栈是实现函数调用的核心数据结构。函数调用栈中有函数参数、栈帧指针、返回地址、局部变量等重要数据。栈追踪是重要的故障检查排除手段之一。

7.2.3.4. 具体实现(implementation detail)

从两种情况来分析性价比:无错误返回;有错误返回。

当无错误返回时,是1个单一内存写。比如,1个返回void的函数调用1个返回错误函数。在栈内存初始化结构 StrackTrace :

stack_trace_struct.zig

pub const StackTrace = struct {
    index: usize,
    instruction_addresses: [N]usize,
};

其中 N 是通过调用图分析确定的最大函数调用深度,递归被忽略且计为2。

StrackTrace指针做为隐藏参数,放在参数表的第1位,传递给每个可以返回错误的函数。

所以当无错误返回时,没有性能开销。

为返回错误的函数生成代码时,在返回错误的return语句之前,生成对这个函数的调用:

zig_return_error_fn.zig

// marked as "no-inline" in LLVM IR
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个算术运算和一些内存读写。这些内存受限,且在错误冒泡返回期间保持缓存。

至于生成代码大小,在返回语句前调用1个函数问题不大。尽管这样,仍有个计划,把 __zig_return_error 函数设计成尾调用,这样可将代码大小开销降到零。

当生成代码有错误返回追踪时,没有错误返回追踪的return语句生成跳转指令。

7.3. 指定值结尾类型(sentinel-terminated type)

指定值结尾类型包括指定值结尾数组、切片和指针。

C语言字符串就是以数字 0 结尾的数组。

名词解释

指定值(:sentinel) 用指定值来表明元素有特殊含义,如列表头、数组结尾等。通常直译为哨兵,但我觉得指定值更易理解。

7.3.1. 指定值结尾数组(sentinel-terminated array)

语法:[N:x]T[_:x]T

说明,数组元素的类型是T,长度是N,在其长度值(len)对应的索引位置的元素值,是指定值x。

示例:

test_null_terminated_array.zig

const expect = @import("std").testing.expect;
test "null terminated 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);
}

7.3.2. 指定值结尾切片(sentinel-terminated slice)

指定值结尾切片的类型是[:x]T,该类型的长度值已知,长度值对应索引的值等于指定值x。

这种切片并不保证切片中间没有指定值x。

可以使用a[start..end:x]来创建指定值结尾切片,a可以是多项指针、数组或切片。

run_null_terminated_slice.zig

const print=@import("std").debug.print;
pub fn main() void{
    const s1:[:0]const u8="hello";
    print("{} {}\n",.{s1.len,s1[5]}); // 5 0
    var array=[_]u8{3,2,0,13,12,0,23,22,0};
    var l:usize=5;
    const s2=array[0..l :0];
    print("{} {}\n",.{s2.len,@TypeOf(s2)}); // 5 [:0]u8
    print("{} {} {} {}\n",.{s2[2],s2[3],s2[4],s2[5]}); // 0 13 12 0
}

本例中,s1的长度len已知等于5,且s1[5]==0,等于指定值。

从s2中可看出,s2[0]==0,指定值结尾切片并不保证切片中没有指定值。

指定值结尾的切片,可以确定结尾处的元素值是指定值,否则运行时会崩溃。

test_null_term_slice.zig

test "null term slice" {
    var a=[_]u8{3,2,1,0};
    var l:usize=2;
    const s=a[0..l :0];
    _=s;
}

Test [1/1] test.nullterm not 0... thread 18708 panic: sentinel mismatch: expected 0, found 1

7.3.2.1. 字符串切片(string slice)

字符串字面值的类型是*const [len:0]u8,len是字符串长度,字符串是UTF-8编码。下面是用切片来拼接字符串的示例。

run_concatslicestring.zig

const std=@import("std");
const fmt=std.fmt;
const print=std.debug.print;
pub fn main() !void{
    const h:[]const u8="hello";
    const w:[]const u8="世界";
    var all:[100]u8=undefined;
    const alls=all[0..];
    const hw=try fmt.bufPrint(alls,"{s} {s}",.{h,w});
    print("{s}\n",.{hw}); // Hello 世界
}

7.3.3. 指定值结尾指针(sentinel-terminated pointer)

语法[*:x]T描述了一个指针,其长度由一个指定值x决定,具有防止缓冲区溢出和读出边界的保护。

run_sentinel_term_pointer.zig

const std = @import("std");
// This is also available as `std.c.printf`.
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;
pub fn main() anyerror!void {
    const msg="hello,world\n";
    const msg1:[msg.len]u8=msg.*;
    _=printf(&msg);
    _=printf(&msg1);
}
shell

zig build-exe run_sentineltermpointer.zig -lc
run_sentineltermpointer.zig:9:11: error: expected type '[*:0]const u8', found '*const [12]u8'
 _=printf(&msg1);
          ^``
run_sentineltermpointer.zig:9:11: note: a single pointer cannot cast into a many pointer

7.4. 不透明类型(opaque)

用 opaque {} 定义1个未知位长(肯定不为0)和对齐的类型。通常用于与不公开结构细节C语言代码交互时的类型安全。

test_opaque.zig

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);
}
shell

$ zig test test_opauqe.zig
test_opauqe.zig:10:9: error: expected type '*test_opauqe.Derp', found '*test_opauqe.Wat'
    bar(w);
        ^
test_opauqe.zig:10:9: note: pointer type child 'test_opauqe.Wat' cannot cast into pointer type child 'test_opauqe.Derp'
test_opauqe.zig:6:13: note: opaque declared here
const Wat = opaque {};
            ^``` `
test_opauqe.zig:5:14: note: opaque declared here
const Derp = opaque {};
             ^``` `

7.5. 无返回类型(noreturn)

noreturn是下列语句或表达式的类型:

break, continue, return, unreachable, while(true){}

解析类型时,如if子句或switch分支时,noreturn 类型与其他所有类型兼容。例如:

test_noreturn.zig

fn foo(condition:bool, b:u32) void {
    const a=if (condition) b else return;
    _=a;
    @panic("do something with a");
}
test "noreturn" {
    foo(false,1);
}

下面是 exit函数使用noreturn的实例。

noreturn_from_exit.zig

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 == .i386) .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;
}
Shell

$ zig test noreturn_from_exit.zig -target x86_64-windows --test-no-exec

7.6. 零比特类型(zero bit type)

以下类型的的比特位长是零,称为零比特类型:


void, int u0, int i0,
array [0]T, vector [0]T
array [N]T, vector [N]T, element is zero bit type
struct all fields is zero bit type
enum only 1 tag
union only 1 field

示例如下:

test_zero_bit.type.zig

const expect=@import("std").testing.expect;
test "zero bit type" {
    const zero:void={};
    try expect(@sizeOf(@TypeOf(zero))==0);
    try expect(@sizeOf(u0)==0);
    try expect(@sizeOf(i0)==0);
    try expect(@sizeOf([0]i32)==0);
    try expect(@sizeOf(@Vector(0,i32))==0);
    const array=[_]void{zero,zero};
    try expect(@sizeOf(@TypeOf(array))==0);
    const struct0=struct{zero:void,zero1:void};
    try expect(@sizeOf(struct0)==0);
    const enum1=enum{one};
    try expect(@sizeOf(@TypeOf(enum1))==0);
    const union1=union{one:i32};
    try expect(@sizeOf(@TypeOf(union1))==0);
}

这些类型仅有一个值,可以等同于用零位来表示。

使用这些类型的源代码不包括在最终生成的机器代码中。

zerobit.zig

export fn entry() void {
    var x: void = {};
    var y: void = {};
    x = y;
}

即使在Debug模式下,上面的源代码函数内的语句也不会生成对应的机器代码,比如在x86_64上,生成:


0000000000000010 <entry>:
  10:    55                       push   %rbp
  11:    48 89 e5                 mov    %rsp,%rbp
  14:    5d                       pop    %rbp
  15:    c3                       retq   

这些汇编指令没有任何与void相关联的代码,只执行函数的前导和后续。

名词解释:

汇编(:assembly) 与CPU的二进制指令对应的英文文本语句,汇编语句与特定CPU和汇编器密切相关。

7.6. void

void主要用于实例化泛型。

例如,给定 Map(Key,Value),设Value的类型是void,则可变成Set(Key)。

值类型用void而不是使用虚拟值,hashmap条目类型中没有值属性,因此hashmap内存空间更少,且所有读写值的代码都会被删除。

test_hashmap_toset.zig

const std=@import("std");
const expect=@import("std").testing.expect;
test "turn HashMap into a set with void" {
    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));
}

void 与 anyopaque 不一样,void 的字节位长是已知的0字节长,而 anyopaque 的字节位长是未知的非0值。

只有void类型的表达式才可被忽略。变量或函数返回值也可用赋值给 _ 明确表示忽略。

test_void_ignore.zig

test "void is ignored" {
    returnsVoid();
}
fn returnsVoid() void {}

test "explicitly ignoring expression value" {
    _ = foo();
}
fn foo() i32 {
    return 1234;
}

8. 指针(pointer)

追求速度的程序设计语言都有指针类型,追求易用性的都没有。

指针能提高运行速度的本质原因是可以最大程度的减少内存复制,高效率的同时带来了巨多的复杂性。

指针变量也是类型的一种,也有6个要素:名字、类型、值、地址、占用内存字节长度、对齐。

设变量a的类型为T,值为value,内存地址为addr,把变量a的内存地址,保存到另一个变量b,则:

8.1. 指针基本操作(pointer basic operator)

8.1.1. 指针定义(pointer declaration)

语法: *T

说明:T可以是绝大多数类型。

run_pointer_decl.zig

const print=@import("std").debug.print;
pub fn main() void {
    const i:*i32=undefined;
    print("{}\n",.{i});
}
shell

$ zig run run_pointer_decl.zig
i32@c98c3ff6d8
$ zig run run_pointer_decl.zig
i32@a393ffae8

输出数字是i的自身值,因为i没有被赋值,所以这个数是个无意义的野指针。

8.1.2. 取地址(address of)

语法: &a

操作数类型:所有类型(all type)

说明:取变量a的地址。

示例:

run_pointer_address.zig

const print=@import("std").debug.print;
pub fn main() void {
    const a=[_]i32{1,2,3,4};
    var i=&a[1];
    print("{}\n",.{@sizeOf(i32)});
    print("{}\n",.{i});
    i=&a[2];
    print("{}\n",.{i});
}
shell

4
i32@7ff7c806529c
i32@7ff7c80652a0

a[2]的地址是52a0,a[1]的地址是529c,25a0-529c==4,正好是i32类型的字节长度。

8.1.3. 解引用(dereference)

语法:a.*

操作数类型:指针(pointer)

说明:解引用时运算,等同于对指针指向变量的运算。

因为函数参数在函数体内只能读不能改,所以想在函数内部修改外部变量值,只能使用指针。这也是指针最普遍的用途之一。

示例:

frag_pointer_deref

const expect=@import("std").testing.expect;
test "dereference" {
    var i:i32=10;
    const j=&i;
    try expect(j.*==10);
    j.* += 20;
    try expect(i==30);
}
fn add5(a:*i32) void{
    a.*+=5;
}
test "pointer parameter" {
    var i:i32=20;
    add5(&i);
    try expect(i==25);
}

8.2. const指针(const pointer)

设变量b为指向变量a的指针,类型为T,解引用后修改变量a的值。则根据指针b的类型不同,分别如下:

语法 指针b的自身值 变量a的值
*T 可以修改 可以间接修改
*const T 可以修改 不能修改
const *T 不能修改 可以间接修改
const *const T 不能修改 不能修改

把本例中任意一行的注释符去掉,都编译出错中止。

test_pointer_const.zig

const expect=@import("std").testing.expect;
test "*T" {
    var i:i32=1;
    var j:*i32=&i;
    j.*+=3;
    try expect(i==4);
    var k:i32=5;
    j=&k;
    try expect(j.*==5);
}
test "*const T" {
    const i:i32=1;
    var j=&i;
    // j.* +=3;
    var k:i32=6;
    j=&k;
    try expect(j.*==6);
}
test "const *T" {
    var i:i32=1;
    const j=&i;
    j.*+=3;
    try expect(i==4);
    const k:i32=5;
    // j=&k;
    try expect(k==5);
}
test "const *const T" {
    const i:i32=1;
    const j=&i;
    // j.*+=3;
    const k:i32=1;
    // j=&k;
    try expect(j.*==k);
}

8.3. 单项指针(single-item pointer)

类型名:*T

单项指针仅只向1个单独的变量。

支持的操作有:解引用,重新赋值;不能进行指针加减、索引和取切片运算。

把本例中任意一行的注释符去掉,都编译出错中止。

test_single_pointer.zig

const expect=@import("std").testing.expect;
test "single item pointer" {
    var i:i32=1;
    var ptr=&i;
    try expect(ptr.*==1);
    // ptr+=1;
    // try expect(ptr[0]==1);
}

指向数组某个元素的指针是单项指针。

test_single_pointer_item.zig

const expect=@import("std").testing.expect;
test "single item pointer array" {
    var i=[_]i32{1,2,3,4};
    const ptr=&i[2];
    try expect(@TypeOf(ptr)==*i32);
}

8.4. 多项指针(many-item pointer)

类型名:[*]T

多项指针是指向未知个数元素的指针,元素的类型是T。

T必须是已知位长的类型,不能是anyopaque或其它不透明类型(opaque type)。

支持的操作有:指针加减整数、索引和取切片运算,不支持取长度、解引用操作。

8.4.1. 多项指针赋值(many-item pointer assignment)

可把数组的地址赋值给多项指针,赋值时多项指针类型声明必不可少。

可把编译期切片和运行期切片赋给多项指针。

test_many_item_assign.zig

const expect=@import("std").testing.expect;
var a=[_]i32{1,2,3,4};
test "assign array address" {
    const ptr:[*]i32=&a;
    try expect(ptr[1]==a[1]);
}
test "assign comptime slice *[N]T" {
    const ptr:[*]i32=a[1..3];
    try expect(ptr[1]==a[2]);
}
test "assign runtime slice []T" {
    var le:usize=3;
    const ptr:[*]i32=a[1..le].ptr;
    try expect(ptr[1]==a[2]);
}

8.4.1.1. 单项指针赋值给多项指针(single-item is assigned to many-item pointer)

把单项指针的自身值赋值给多项指针,也就是单项指针转换为多项指针,可以用 @ptrCast函数。

还可以把单项指针转换为有1个元素的数组指针,再把数组指针转换成多项指针。

通常不建议这么做,因为多项指针没有边界检查,如本例中的ptr[3]是未知用途的。

test.single_to_many_ptr.zig

const expect=@import("std").testing.expect;
test "single-item is assigned to many-item" {
    var i:i32=1;
    var ptr1:*[1]i32=&i;
    var ptr:[*]i32=ptr1;
    ptr[0]=2;
    try expect(i==2);
    ptr[3]=138705;
}

8.4.2. 多项指针索引和计算(many-item pointer index and arithmetic)

多项指针可以索引,也可以加减整数值。

test_many_item_index_arith.zig

const expect=@import("std").testing.expect;
var a=[_]i32{1,2,3,4};
test "many-item pointer index" {
    const ptr:[*]i32=&a;
    try expect(ptr[1]==2);
    ptr[2]=33;
    try expect(a[2]==33);
}
test "many-item pointer arithmetic" {
    var ptr:[*]i32=&a;
    ptr+=2;
    ptr[0]=333;
    try expect(a[2]==333);
    ptr-=1;
    try expect(ptr[0]==2);
}

多项指针互相不能加减

test_many_item_add.zig

var a=[_]i32{1,2,3,4};
test "many-item pointer add" {
    var ptr:[*]i32=&a;
    var ptr1=ptr+2;
    const ptr2=ptr+ptr1;
    _=ptr2;
}
shell

$ zig test test_many_item_add.zig
error: expected type 'usize', found '[*]i32'

8.4.3. 多项指针取切片(many-item pointer take slice)

可以从多项指针上取切片,取出后的类型为 *[N]T ,是数组指针。

frag_manyitem4

const expect=@import("std").testing.expect;
var a=[_]i32{1,2,3,4};
test "many-item pointer add" {
    var ptr:[*]i32=&a;
    var s=ptr[1..3];
    try expect(@TypeOf(s)==*[2]i32);
    try expect(s.len=2);
    try expect(s[1]==3);
}

8.5. 数组指针(array pointer)

类型名:*[N]T

指向N个元素的指针,元素的类型是T。

支持的操作有:取长度、解引用、索引和取切片运算,不支持指针加减整数操作。

8.5.1. 数组指针赋值(array pointer assignment)

可以把数组的地址赋值给数组指针,赋值时可以省略类型。

可以把切片赋值给数组指针。

test_array_pointer_assign.zig

const expect=@import("std").testing.expect;
var a=[_]i32{1,2,3,4};
test "assign array address" {
    var ptr=&a;
    try expect(@TypeOf(ptr)==*[4]i32);
}
test "assign slice" {
    var ptr=a[1..3];
    try expect(@TypeOf(ptr)==*[2]i32);
}

8.5.2. 数组指针索引和取长度(array pointer index and get length)

数组指针可以索引和取长度。

test_arrayptr_index.zig

const expect=@import("std").testing.expect;
var a=[_]i32{1,2,3,4};
var ptr=&a;
test "array ptr index and get len" {
    try expect(ptr[1]==2);
    try expect(ptr.len==4);
}

8.5.3. 数组指针解引用(array pointer dereference)

数组指针解引用后,类型为[N]T,是数组。

test_arrayptr_deref.zig

const expect=@import("std").testing.expect;
var a=[_]i32{1,2,3,4};
test "array ptr dereference" {
    const a1=ptr.*;
    try expect(@TypeOf(a1)==[4]i32);
    try expect(a1[2]==3);
}

8.5.4. 数组指针取切片(array pointer get slice)

可以从数组指针上取切片,取出后的类型为 *[N]T ,是数组指针。

test

const expect=@import("std").testing.expect;
var a=[_]i32{1,2,3,4};
var ptr=&a;
test "array ptr get slice" {
    const s=ptr[1..3];
    try expect(@TypeOf(s)==*[2]i32);
    try expect(s[0]==2);
}

8.6. 切片指针(slice pointer)

运行期取切片,类型是[]T,编译期取切片,类型是*[N]T

因为运行时切片的长度编译时不可知,所以运行时取切片类型中不含长度。

切片指针其实就是切片,详见6.3. 切片(slice)

支持的操作有:取长度、索引和取切片运算,不支持指针加减整数操作。

运行期切片不支持解引用操作,编译期切片支持解引用操作。

8.7. 可选类型指针(optional pointer)

语法 ?*T 是一个可选类型,其载荷是指向类型T的指针。

test_pointer_optional.zig

const expect=@import("std").testing.expect;
test "optional pointer" {
    var i:i32=5;
    var j:?*i32=&i;
    const k:*i32=j orelse unreachable;
    const l:i32=k.*;
    try expect(l==i);
}

8.8. 错误联合指针(error union pointer)

语法 !*T 是一个错误联合类型,其载荷是一个指向类型T的指针。

test_pointer_error_union.zig

const expect=@import("std").testing.expect;
test "error union pointer" {
    var i:i32=5;
    var j:anyerror!*i32=&i;
    const k:*i32=try j;
    const m:i32=k.*;
    try expect(m==i);
}

8.9. 错误联合可选类型指针(error union optional pointer)

语法 !?*T 是一个错误联合类型,其载荷是一个可选类型,该可选类型的载荷是指向类型T的指针。

test_pointer_error_union_optional.zig

const expect=@import("std").testing.expect;
test "error union optional pointer" {
    var i:i32=5;
    var j:anyerror!?*i32=&i;
    const k:?*i32=try j;
    const l:*i32=k orelse unreachable;
    const m:i32=l.*;
    try expect(m==i);
}

8.10. 函数指针(function pointer)

函数指针类型是在函数定义基础上加 *const前缀。

函数指针可以是运行期可知。

test_func_ptr.zig

const expect=@import("std").testing.expect;
fn add5(a:i8) i8{return a+5;}
fn sub7(a:i8) i8{return a-7;}
const call_op=*const fn(a:i8) i8;
fn do(fn1:call_op,x:i8) i8{return fn1(x);}
test "function pointer"{
    try expect(do(add5,10)==15);
    var f:call_op=sub7;
    try expect(do(f,17)==10);
    f=add5;
    try expect(do(f,17)==22);
}

8.11. 指向子结构的指针自动解引用(automatic dereference of pointer to substruct)

结构内属性是指向子结构的指针可自动解引用,可省略 .*,属性指向普通类型的指针不能自动解引用。

run_auto_deref_pointer.zig

const expect=@import("std").testing.expect;
test "automatic dereference of pointer to substruct" {
    const foo1=struct{x:i32};
    const foo=struct{ptr:*foo1,ptr1:?*foo1,ptr2:*i32};
    var i:i32=10;
    var f1=foo1{.x=5};
    var f2=foo1{.x=10};
    var f=foo{.ptr=&f1,.ptr1=&f2,.ptr2=&i};
    try expect(f.ptr.*.x==f.ptr.x);
    try expect(f.ptr.x==5);
    try expect(f.ptr1.?.*.x==f.ptr1.?.x);
    try expect(@TypeOf(f.ptr2.*)==i32);
    try expect(@TypeOf(f.ptr2)==*i32);    
}

8.12. 允许地址值为0(allowzero)

在祼机操作系统上,地址0可以被寻址,这时须加allowzero属性,否则会崩溃。

如果想表示空指针,请使用可选类型。

test_allowzero.zig

var zero:usize=0;
test "allowzero" {
    var ptr=@intToPtr(*allowzero i32, zero);
    _=ptr.*;
}
test "not allowzero" {
    var ptr=@intToPtr(*i32, zero);
    _=ptr.*;
}
shell

$ zig test test_allowzero.zig
Test [2/2] test.not allowzero... thread 19532 panic: cast causes pointer to be null
D:/dev/zig/hello.zig:11:2: 0x7ff7a04e10e9 in test.not allowzero (test.exe.obj)
        var ptr=@intToPtr(*i32, zero);

8.13. 易变性(volatile)

如果给定的读取或写入有副作用,比如内存映射的输入/输出(MMIO),从外设接口地址读取或写入时,应使用volatile。在下面的示例中,mmio_ptr的读取和写入被保证全部发生,而且顺序与源代码相同。

易变性与并发和原子操作(atomic)无关,如果在MMIO外使用 volatile ,通常是个错误。

run_volatile.zig

const print=@import("std").debug.print;
pub fn main() void{
    const mmio_ptr = @intToPtr(*volatile u8, 0x12345678);
    print("{}\n",.{@TypeOf(mmio_ptr)}); // *volatile u8
}

8.14. 常用指针(common pointer)

设a是变量,则指针解读和取值方法为:

语法 取a值方式 解读
*a a.* 指针,指向a
?*a a.*.? 可选类型,载荷是指向a的指针
*?a a.?.* 指针,指向载荷为a的可选类型
!*a try a.* 错误联合类型,其载荷是指针,指针指向a
!?*a try a.?.* 错误联合类型,其载荷是可选类型,可选类型的载荷是指向a的指针
!*?a try a.*.? 错误联合类型,其载荷是指针,指向载荷为a的可选类型

设T是类型,则数组相关指针为:

只读相关指针解读参见:8.2. const指针(const pointer)

9. 类型转换(type cast)

类型转换是指变量在内存单元中值不变,把变量的类型转换成另一种。

类型强转是完全安全和明确的转换,精确转换是不希望发生意外的转换,成对类型解析是用于有多个操作数时,确定结果类型的转换。

9.1. 类型强转(type coercion)

当需要的类型和提供的类型不同、完全明确类型如何转换、保证安全不发生未定义行为时,自动进行类型强转。

test_type_coerction.zig

test "type coercion - variable declaration" {
    var a:u8=1;
    var b:u16=a;  _=b;
}
fn foo(b: u16) void {
    _=b;
}
test "type coercion - function call" {
    var a:u8=1;
    foo(a);
}
test "type coercion - @as builtin" {
    var a:u8=1;
    var b=@as(u16,a);  _=b;
}

9.1.1. @as

@as(comptime T:type,expression) T

说明:如果转换是明确和安全的,则允许此强转,这是在类型之间进行转换的首选方法。

test_as.zig

test "type coercion - @as builtin" {
    var a: u8 = 1;
    var b = @as(u16, a);
    _ = b;
}

9.1.2. 更严格限定(stricter qualification)

运行时相同表现形式的值可以强转到更严格的限定,反之则不行。包括:

这些强转在运行时是无操作的,因为值表现形式没有改变。

test_strict_quali.zig

fn foo(_:*const i32) void{}
test "const qualification"{
    var a:i32=1;
    var b:*i32=&a;
    foo(b);
}
const std=@import("std");
const expect=std.testing.expect;
const mem=std.mem;
test "cast *[1][*]const u8 to [*]const ?[*]const u8" {
    const window_name = [1][*]const u8{"window name"};
    const x: [*]const ?[*]const u8 = &window_name;
    try expect(mem.eql(u8, std.mem.sliceTo(@ptrCast([*:0]const u8, x[0].?), 0), "window name"));
}

9.1.3. 整数和浮点数变大(integer and float widening)

整数可强转到能表示旧类型每个值的整数类型,同样浮点数可强转到能表示旧类型每个值的浮点类型。

test_intfloat_widen.zig

const builtin = @import("builtin");
const expect=@import("std").testing.expect;
test "integer widening" {
    var a: u8 = 250;
    var b: u16 = a;
    var c: u32 = b;
    var d: u64 = c;
    var e: u64 = d;
    var f: u128 = e;
    try expect(f == a);
}
test "implicit unsigned integer to signed integer" {
    var a: u8 = 250;
    var b: i16 = a;
    try expect(b == 250);
}
test "float widening" {
    // Note: there is an open issue preventing this from working on aarch64:
    // https://github.com/ziglang/zig/issues/3282
    if (builtin.target.cpu.arch == .aarch64) return error.SkipZigTest;
    var a: f16 = 12.34;
    var b: f32 = a;
    var c: f64 = b;
    var d: f128 = c;
    try expect(d == a);
}

9.1.4. 浮点数强转到整数(coercion float to int)

小数部分为0的浮点数可强转为整数。

本例中,产生编译错误是对的,因表达式有两种强转结果,编译器无所适从:

54.0 :comptime_int 54/5 ==> 10 :f32

5 :comptime_float 54.0/5.0 ==> 10.8 :f32

test_ftoi.zig

test "implicit cast to comptime_int" {
    var f: f32 = 54.0 / 5;
    _ = f;
}
shell

$ zig test test_ftoi.zig
test_ftoi.zig:2:23: error: ambiguous coercion of division operands 'comptime_float' and 'comptime_int'; non-zero remainder '4'
    var f: f32 = 54.0 / 5;
                 ``~^~~

9.1.5. 可选类型和错误联合类型(optional and error union)

可选类型的载荷类型,和 null 一样,可被强转为可选类型,也可被强转为错误联合类型。

错误联合类型的载荷类型,和错误集类型一样,也可被强转为错误联合类型。

test_coerce_oeu.zig

const expect=@import("std").testing.expect;
test "coerce to optionals" {
    const x: ?i32 = 1234;
    const y: ?i32 = null;
    try expect(x.? == 1234);
    try expect(y == null);
}
test "coerce to optionals wrapped in error union" {
    const x: anyerror!?i32 = 1234;
    const y: anyerror!?i32 = null;
    try expect((try x).? == 1234);
    try expect((try y) == null);
}
test "coercion to error unions" {
    const x: anyerror!i32 = 1234;
    const y: anyerror!i32 = error.Failure;
    try expect((try x) == 1234);
    try std.testing.expectError(error.Failure, y);
}

9.1.6. 编译期可知数值(compile-time known number)

如果可以在目标类型中表示编译期可知数值,则该数值可以被强转。

test_compnum_coerce.zig

const expect=@import("std").testing.expect;
test "coercing large integer type to smaller one when value is comptime known to fit" {
    const x: u64 = 255;
    const y: u8 = x;
    try expect(y == 255);
}

9.1.7. 联合和枚举(union and enum)

标记联合可以被强转成枚举。

当枚举是编译期可知的标记联合的属性,且该属性只有1个值,可被强转为标记联合。

test_union_enum_coerce.zig

const expect=@import("std").testing.expect;
const E=enum{one,two,three};
const U=union(E){
    one:i32,two:f32,
    three};
test "coercion between unions and enums" {
    var u=U{.two=12.34};
    var e:E=u;
    try expect(e==E.two);
    const three=E.three;
    var otheru:U=three;
    try expect(otheru==E.three);
}

9.1.8. 指针类型转换(pointer type cast)

指针类型转换参见9.2.4. 指针和整数精确转换(pointer and integer explicit cast)

单项指针转换为多项指针,参见8.4.1.1. 单项指针赋值给多项指针(single-item is assigned to many-item pointer)

9.1.8.1. 数组类指针互转(array class pointer cast)

多项指针: [*]T

运行期切片指针:[]T

数组指针(也就是编译期切片指针): *[N]T

这三者指针互相转换的方式见本例。

test_array_pointer_class_cast.zig

const expect=@import("std").testing.expect;
var a=[_]i32{1,2,3,4};
test "[]T to *[N]T" {
    var a=[4]i32{1,2,3,4};
    var s:[]i32=&(a[0..2].*);
    var a1:*[2]i32=s.ptr[0..2];
    try expect(@TypeOf(s)==[]i32);
    try expect(@TypeOf(a1)==*[2]i32);
}
test "[*]T to *[N]T" {
    const ptr:[*]i32=a[1..3];
    const a1:*[2]i32=ptr[0..2];
    try expect(a1[1]==a[2]);
}
test "*[N]T to []T" {
    var le:usize=3;
    const s:[]i32=a[1..le];
    try expect(s[1]==a[2]);
}
test "[*]T to []T" {
    const ptr:[*]i32=a[1..3];
    var le:usize=2;
    const s:[]i32=ptr[0..le];
    try expect(s[1]==a[2]);
}
test "*[N]T to [*]T" {
    const ptr:[*]i32=a[1..3];
    try expect(ptr[1]==a[2]);
}
test "[]T to [*]T" {
    var le:usize=3;
    const s:[]i32=a[1..le];
    const ptr:[*]i32=s.ptr;
    try expect(ptr[1]==a[2]);
}

9.1.8.2. 切片收缩转换(slice shrinking cast)

run_slice_shrinking_cast.zig

const print=@import("std").debug.print;
pub fn main() void{
    const bytes align(@alignOf(u32)) = [_]u8{ 0x12, 0x13, 0x14, 0x15 };
    const u32_value = @import("std").mem.bytesAsSlice(u32, bytes[0..])[0];
    print("{x}",.{u32_value}); // 15141312
}

9.1.9. 未定义(undefined)

undefined可转换为任意类型。

9.2. 精确转换(explicit cast)

用内置函数来进行精确转换。

这些内置函数可能是安全的,可能是执行语言级断言,可能是在运行期是无操作的。

9.2.1. 整数和浮点数精确转换(integer and float explicit cast)

9.2.1.1. @intCast

@intCast(comptime DestType:type,int:anytype) DestType

说明:将一个整数转换为另一个整数,同时保持相同的数值。试图转换超出目标类型范围的数字是未定义行为。

test_intcast.zig

test "@intCast" {
    var a:u16=12;
    var b=@intCast(u8,a);
    _=b;
}
test "@intCast panic" {
    var a:u16=0xabcd;
    var b:u8=@intCast(u8,a);
    _=b;
}
shell

zig test test_intcast.zig
Test [1/1] test.@intCast panic... thread 25940 panic: integer cast truncated bits

9.2.1.2. @truncate

@truncate(comptime T:type,integer:anytype) T

说明:从整数类型中截断比特位,生成较小或相同大小的整数类型。

这个函数总是截断整数的有效位,而不管目标平台上的字节序是什么。

可对目标类型范围之外的数字调用 @truncate 。

test_truncate.zig

const expect=@import("std").testing.expect;
test "integer truncation" {
    var a: u16 = 0xabcd;
    var b: u8 = @truncate(u8, a);
    try expect(b == 0xcd);
}

使用@intCast 转换确保符合目标类型的数值。

如果输入值类型是 comptime_int ,那么在语义上等同于类型强转。

名词解释:

字节序(:endianness) 又称为端序,分为大端序和小端序,规定了多个字节的数据类型字节存放方式。单字节的数据类型(如u8数组)无字节序的区分。

通常书写变量值时以左边为高位,右边为低位;书写内存地址时以左边为低位,右边为高位。

如:x:i32=0x11223344 ,11为高位,写在左边,44为低位,写在右边。

大端序(:big-endian) 数据类型的低位字节放在内存的高位地址,高位字节放在低位地址。很久以前的主机多是大端序,网络字节序也是大端序(比如IP头)。

小端序(:little-endian) 数据类型的低位字节放在内存的低位地址,高位字节放在高位地址。目前的CPU多是小端序。

则x在不同字节序的保存方式为:

内存地址 0xB800 0xB801 0xB802 0xB803
小端序 44 33 22 11
大端序 11 22 33 44

9.2.1.3. @floatCast

@floatCast(comptime DestType:type,value:anytype) DestType

说明:把浮点数类型类型转换为 DestType 。这个转换是安全的,但数值可能会失去精度。

test_floatcast.zig

const expect=@import("std").testing.expect;
test "@floatCast" {
    var i:f64=13.5;
    try expect(@floatCast(f32,i)==13.5);
    i=1.8e40;
    var j:f32=0;
    const inf:f32=1.0/j;
    try expect(@floatCast(f32,i)==inf);

}

本例中,因为1.8e40已超过f32的最大值,所以精确转换为正无穷,没有发生未定义行为。

9.2.1.4. @floatToInt

@floatToInt(comptime DestType:type,float:anytype) DestType

说明:浮点数的整数部分类型转换为 DestType 。

浮点数的整数部分不适合 DestType,是未定义行为。

test_floatToInt.zig

const expect=@import("std").testing.expect;
test "@floatToInt" {
    var f:f32=5.4;
    try expect(@floatToInt(u8,f)==5);
}
test "@floatToInt UB" {
    var f:f32=100000.15;
    try expect(@floatToInt(u8,f)==100000);
}

Test [2/2] test.@floatToInt UB... thread 3060 panic: integer part of floating point value out of bounds

9.2.1.5. @intToFloat

@intToFloat(comptime DestType:type,int:anytype) DestType

说明:将整数转换为最接近的浮点数,这种情况总是安全的。

test_intToFloat.zig

const expect=@import("std").testing.expect;
test "@intToFloat" {
    var i:i32=15;
    var f=@intToFloat(f32,i);
    try expect(f==15);
    try expect(@TypeOf(f)==f32);
}

9.2.2. 布尔枚举和整数转换(bool, enum and integer explicit cast)

9.2.2.1. @boolToInt

@boolToInt(value:bool) u1

说明:转换true 为 @au(u1,1) , false 为 @as(u1,0)。

如果value在编译时可知,则返回类型是comptime_int。

test_boolToInt.zig

const expect=@import("std").testing.expect;
test "comptime @boolToInt" {
    const i:bool=true;
    const j=@boolToInt(i);
    try expect(j==1);
    try expect(@TypeOf(j)==comptime_int);
}
test "runtime @boolToInt" {
    var i:bool=false;
    var j=@boolToInt(i);
    try expect(j==0);
    try expect(@TypeOf(j)==u1);
}

9.2.2.2. @enumToInt

@enumToInt(enum_or_tagged_union:anytype) anytype

说明:把enum的值转换为其整数标记值,把标记union的枚举值转换为其整数标记值。

如果只有1个可能的enum值,结果值是编译期可知的,类型为comptime_int。

test_enumToInt.zig

const expect=@import("std").testing.expect;
const e1=enum{one};
const e=enum{one,two,three,four};
const u=union(enum){one:i32,two:i64};
test "enum to integer" {
    const i=@enumToInt(e.two);
    try expect(@TypeOf(i)==u2);
    try expect(i==1);
}
test "tagged union to integer" {
    const i=@enumToInt(u.one);
    try expect(@TypeOf(i)==u1);
    try expect(i==0);
}
test "one field" {
    const i=@enumToInt(e1.one);
    try expect(@TypeOf(i)==u0);
    try expect(i==0);
    // try expect(@TypeOf(i)==comptime_int);
}

9.2.2.3. @intToEnum

@intToEnum(comptime DestType:type,integer:anytype) DestType

说明:将整数转换为enum值。

test_intToEnum.zig

const expect=@import("std").testing.expect;
const e=enum{one,two,three,four};
test "@intToEnum" {
    const i:i32=2;
    const j=@intToEnum(e,i);
    try expect(j==e.three);
}

试图转换在所选枚举类型中不表示值的整数,是未定义行为。

test_intToEnum.zig

const expect=@import("std").testing.expect;
const e=enum{one,two,three,four};
test "@intToEnum" {
    const i:i32=10;
    const j=@intToEnum(e,i);
    try expect(j==e.three);
}

error: enum 'test_intToEnum.e' has no tag with value '10'

###注:把const i改为var i,测试可以通过。与预期不符。

9.2.3. 错误和整数精确转换(error and integer explicit cast)

9.2.3.1. @errSetCast

@errSetCast(comptime T:DestType,value:anytype) DestType

说明:把value类型转换为另一个错误集。

test_errSetCast.zig

const expect=@import("std").testing.expect;
const err1=error{one,two};
test "@errSetCast"{
    const e=error.two;
    const e1=@errSetCast(err1,e);
    try expect(@TypeOf(e1)==err1);
}

转换不在 DestType中的错误会导致受安全保护的未定义行为。

test_errSetCast_ub.zig

const err1=error{one,two};
test "@errSetCast UB"{
    var e=error.six;
    var e1=@errSetCast(err1,e);
    _=e1;
}

error: error sets 'error{six}' and 'error{one,two}' have no common errors

9.2.3.2. @errorToInt

@errorToInt(err:anytype) std.meta.Int(.unsigned,@sizeOf(anyerror)*8)

操作数类型:全局错误集,错误集,错误联合类型

说明:将 err 转换为错误的整数表示。

因为随着源代码变动,错误的整数表示是不稳定的,通常情况下不建议使用此函数。

run_errorToInt.zig

const print=@import("std").debug.print;
const err1=error{one,two};
pub fn main() !void{
    const i=@errorToInt(err1.two);
    print("{} {}\n",.{i,@TypeOf(i)}); // 2 u16
}

9.2.3.3. @intToError

@intToError(value:std.meta.Int(.unsigned,@sizeOf(anyerror)*8)) anyerror

说明:将错误的整数表示转换为全局错误集类型。

因为随着源代码变动,错误的整数表示是不稳定的,通常情况下不建议使用此函数。

尝试转换与任何错误不对应的整数是未定义行为。

run_intToError.zig

const print=@import("std").debug.print;
const err1=error{one,two};
pub fn main() !void{
    const i=@errorToInt(err1.two);
    const e=@intToError(i);
    print("{}\n",.{e==err1.two});
    var j:u16=0x7FFF;
    const e1=@intToError(j);    
    print("{}\n",.{e1});
}
shell

$ zig run run_intToError.zig
true
thread 17936 panic: invalid error code

9.2.4. 指针和整数精确转换(pointer and integer explicit cast)

9.2.4.1. @ptrCast

@ptrCast(comptime DestType:type,value:anytype) DestType

将指针转换为另一类型的指针。

允许使用optional指针。

test_ptrCast.zig

const expect=@import("std").testing.expect;
var i:i32=0x0A0A_0A0A;
var ptr=&i;
test "cast *i8"{
    var p=@ptrCast(*i8,ptr);
    try expect(p.*==0x0A);
}
test "cast [*]i16"{
    var p=@ptrCast([*]i16,ptr);
    try expect(p[0]==0x0A0A);
}
test "optional cast *u32"{
    var p:?*i32=ptr;
    var p1=@ptrCast(*u32,p);
    try expect(p1.*==0x0A0A_0A0A);        
}

将null的optional指针强转为非optional指针将调用安全检查的未定义行为。

test_ptrCast_ub.zig

test "null cast UB"{
    var p:?*i32=null;
    var p1=@ptrCast(*i16,p);
    _=p1;
}

Test [1/1] test.null cast UB... thread 14120 panic: cast causes pointer to be null

9.2.4.2. @ptrToInt

@ptrToInt(value:anytype) usize

操作数类型:是指针 *T 或可选类型指针 ?*T

说明:把指针的自身值,转换为usize类型的值。

run_ptrToInt.zig

const print=@import("std").debug.print;
pub fn main() !void{
    var i:i32=1;
    var ptr=&i;
    var j=@ptrToInt(ptr);
    print("{} {x}\n",.{@TypeOf(j),j});
}

usize f35bfff8d4

9.2.4.3. @intToPtr

语法:@intToPtr(comptime DestType:type,address:usize) DestType

说明:将整数转换为指针。

如果 DestType 是允许地值为0的类型,则整数0可正常转换。

如果 DestType 是可选类型,则整数0可正常转换为null。

test_intToPtr.zig

const expect=@import("std").testing.expect;
test "@intToPtr" {
    const a=[_]i32{11,22,33,44};
    const ptr=&a[1];
    var addr=@ptrToInt(ptr);
    addr+=@sizeOf(i32);
    var ptr1=@intToPtr(*i32,addr);
    try expect(ptr1.*==a[2]);
}
test "cast allowzero" {
    const i:usize=0;
    const ptr=@intToPtr(*allowzero i32,i);
    _=ptr;
}
test "cast null" {
    const i:usize=0;
    const ptr=@intToPtr(?*i32,i);
    try expect(ptr==null);
}

如果 DestType 是不允许地值为0的非可选类型,转换整数0将引发未定义行为。

test_intToPtr_ub.zig

test "cast 0" {
    const i:usize=0;
    const ptr=@intToPtr(*i32,i);
    _=ptr;
}

error: pointer type '*i32' does not allow address zero

9.2.4.4. @alignCast

参见4.4.3.3. @alignCast

9.2.4.5. @addrSpaceCast

@addrSpaceCast(comptime addrspace:std.builtin.AddressSpace, ptr:anytype) anytype

将指针从一个地址空间转换为另一个地址空间。

根据当前目标和地址空间的不同,此强制转换可能是无操作、复杂操作或非法的。如果强制转换是合法的,则生成的指针指向与指针操作数相同的内存位置。在相同的地址空间之间强制转换指针始终是有效的。

9.2.5. @bitCast

@bitCast(comptime DestType:type,value:anytype) DestType

操作数类型:

说明:将一种类型的值转换为另一种类型。

断言:@sizeOf(@TypeOf(value)) == @sizeOf(DestType)

断言:@typeInfo(DestType) != .Pointer。因为针对指针,可以用@ptrCast和@intToPtr。

如果value在编译时可知,则在编译时计算。

对未定义布局的值进行位转换@bitcast是一个编译错误。这意味着,除了具有专用的转换内置函数(枚举、指针、错误集)的类型的限制之外,裸结构、错误联合、切片、选项以及任何其他没有明确定义的内存布局的类型也不能用于此操作。

可以用于类似于:

比特值不变,f32转换为u32, i32转换为u32等。

test_bitCast.zig

const print=@import("std").debug.print;
pub fn main() !void{
    const x:i8=-1;
    const y:u8=@bitCast(u8,x);
    print("{}\n",.{y});
    const a align(4)=[_]u8{0x12,0x13,0x14,0x15};
    const v=@bitCast(u32,a);
    print("{x}\n",.{v});
    var nan:f32=0.0;
    nan=0.0/nan;
    var i:u32=@bitCast(u32,nan);
    print("{x}\n",.{i});
}
shell

255
15141312
ffc00000

9.3. 成对类型解析(peer type resolution)

当表达式有多个操作数时,成对类型解析选择多个操作数都可以强转的类型做为结果类型。

test_peer_type.zig

const std=@import("std");
const expect=std.testing.expect;
const mem=std.mem;
test "peer resolve int widening"{
    var a:i8=12;
    var b:i16=34;
    var c=a+b;
    try expect(@TypeOf(c)==i16);
}
test "peer resolve arrays of different size to const slice"{
    try expect(mem.eql(u8,boolToStr(true),"true"));
    try expect(mem.eql(u8,boolToStr(false),"false"));
    comptime try expect(mem.eql(u8,boolToStr(true),"true"));
    comptime try expect(mem.eql(u8,boolToStr(false),"false"));
}
fn boolToStr(b: bool) []const u8{
    return if (b) "true" else "false";
}
test "peer resolve array and const slice"{
    try testPeerResolveArrayConstSlice(true);
    comptime try 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 "peer type resolution: ?T and 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 "peer type resolution: *[0]u8 and []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 "peer type resolution: *[0]u8,[]const u8,and 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 "peer type resolution: *const T and ?*T"{
    const a=@intToPtr(*const usize,0x123456780);
    const b=@intToPtr(?*usize,0x123456780);
    try expect(a==b);
    try expect(b==a);
}

10. 流程控制语句(flow control statement)

if, while, 表达式中,条件表达式的结果值必须是 bool 类型,或最终载荷值是 bool 类型。

switch,for 表达式中,条件表达式的结果值必须是普通类型,不可以是可选类型、错误联合类型、错误联合可选类型等。

10.1 表达式和语句(expression and statement)

在Zig语言中,if, switch, while, for, break, continue等本质上是表达式,表达式的结果值如果是非 void ,必须要赋值给其它变量;表达式结果是 void ,就是语句。

test_switch_expr_stat.zig

const expect=@import("std").testing.expect;
const i=true;
test "switch expression" {
    const j:u8=switch(i){
        true => 1,
        false => 0,
    };
    try expect(j==1);
}
test "switch statement" {
    switch(i){
        true => {try expect(i);},
        false => {try expect(!i);},
    }
}

10.2. 普通类型(common type)

10.2.1. if

if表达式基本语法如下,其中 condition 是条件表达式。

if(condition) true_value else false_value

因为语句块也是表达式,所以,还可以:

if(condition) {true_block} else {false_block}

else子句可以省略:

if(condition) {true_block}

if表达式可以嵌套:

if(condition) {true_block} else if{block1} else{block2}

test_bool_if.zig

const expect=@import("std").testing.expect;
test "if experession"{
    const a:u32=5;
    const r=if(a!=0) 15 else 100;
    try expect(r==15);
}
test "if nest and block"{
    const a:u32=5; const b:u32=10;
    if(a!=b){
        try expect(true);
    }else if(b==10){
        try expect(true);
    }else{
        unreachable;
    }
}
test "else not required"{
    const a:u32=5;
    if(a==5){
        try expect(true);
    }
}

10.2.2. while

while表达式基本语法:

while(condition) {block}

说明:当条件表达式为真时,运行语句块。

test_basic_while.zig

const expect=@import("std").testing.expect;
test "while basic"{
    var i:i32=0;
    while(i<10){i+=1;}
    try expect(i==10);
}

10.2.2.1. break

用 break 可中途退出循环。

test_break.zig

const expect=@import("std").testing.expect;
test "while break"{
    var i:i32=0;
    while(true){
        if(i==8) break;
        i+=1;}
    try expect(i==8);
}

break 不影响 defer 。

用带标签的 break 可跳出当前块,并可使块表达式有值,参见3.5. 块(block)

10.2.2.2. continue

用 continue 可跳到循环开始处,继续循环。

test_continue.zig

const expect=@import("std").testing.expect;
test "while continue"{
    var i:i32=0;
    var sum:i32=0;
    while(i<5){
        if(i==2) continue;
        sum+=i;
    }
    try expect(i==8);
}

10.2.2.3. 步进表达式(step expression)

step表达式主要用于循环变量递增或递减;循环体内continue语句生效时,也要运行step表达式。

语法:while (cond) : (step) {block};

执行顺序类似于如下伪代码:

LOOP: if(cond==true) {block; step; goto LOOP;};

循环内有continue时的语法:

while (cond) : (step) {block1; if(exp) continue; block2;}

执行顺序类似于如下伪代码:


LOOP: if(cond==true) {
    block1;
    if(exp) {
        step;
        goto LOOP;
    }
    block2;
    step;
    goto LOOP;
}

如果有多个变量需要步进,步进表达式需要用({stepa;stepb;}),因为{}是语句块表达式,而stepa是语句。

run_while_continue_exp.zig

const print=@import("std").debug.print;
pub fn main() void{
    var i:i32=0;
    while(i<5):(i+=1){
        if(i==2) continue;
        print("{}\n",.{i});
    }
    var j:i32=1;
    while(i+j<20):({i*=2;j*=3;}){
        print("{} {}\n",.{i,j});
    }
}
shell

0
1
3
4
5 1
10 3

10.2.2.4. else子句(else clause)

while的条件表达式为 false 时,如果有 else 子句,则执行 else 子句后退出。

break 可以带返回值退出当前while,用 break 退出循环时,不执行 else 子句。

test_else_while.zig

const expect=@import("std").testing.expect;
fn hasnum(num:i32)bool{
    var i:i32=0;
    const j=while(i<10):(i+=1){
        if(i==num) {break true;}
    }else false;
    return j;
}
test "while else" {
    try expect(hasnum(5));
    try expect(!hasnum(-5));
}

10.2.3. for

for表达式基本语法:

for(container) |v| {block}

for循环是对数组、切片、结构等容器进行遍历。

container必须是容器类型,不可以是可选类型、错误联合类型、错误联合可选类型等。

for循环必须要有捕获。

for表达式与while表达式类似,也可用break和continue语句。

for表达式与while表达式类似,也可用else子句。

当遍历正常结束时运行else子句,break时则不运行。

test_for.zig

const expect=@import("std").testing.expect;
var a=[_]i32{1,2,3,4};
var i:i32=0;
test "for" {
    for(a) |v| {
        i+=v;
    }else{
        try expect(i==10);
    }
}

10.2.4. switch

语法:


switch(condition) {
    a => expr,
    b,c => expr,
    d...e => expr,
    f => |v| expr,
    g => |*v| expr,
    else => expr,
}

说明:

test_switch_simple.zig

const expect=@import("std").testing.expect;
fn sw(a:u64)u64{
    const zz:u64=103;
    return switch(a){
        1, 2, 3 => 0,
        5...100 => 1,
        101 => blk:{
            const c:u64=5;
            break :blk c*2+1;
        },
        zz => zz,
        blk:{
            const d:u32=5;
            const e:u32=100;
            break :blk d+e;
        } => 107,
        else => 9,
    };
}
test "switch simple" {
    try expect(sw(0)==9);
    try expect(sw(1)==0);
    try expect(sw(2)==0);
    try expect(sw(3)==0);
    try expect(sw(4)==9);
    try expect(sw(5)==1);
    try expect(sw(6)==1);
    try expect(sw(99)==1);
    try expect(sw(100)==1);
    try expect(sw(101)==11);
    try expect(sw(103)==103);
    try expect(sw(105)==107);
    try expect(sw(200)==9);
}

10.2.4.1. 不完全switch(exhaustive switch)

switch 语句的 else 分支通常是必须的,见上一节示例。

如果所有实例都有对应分支,则else分支可以省略。

test_all_case.zig

test "all case"{
    const b:bool=true;
    _=switch(b){
        true => 1,
        false => 0,};
}

如果分支不包含所有实例时,没有else分支,则会产生编译错误。

test_not_else.zig

test "not else"{
    const b:i8=1;
    _=switch(b){
        1...127 => true,
        0 => false,
    };
}

test_not_else.zig:8:4: error: switch must handle all possibilities

10.2.4.2. 判断表达式是枚举或标记联合(condition is enum or tagged union)

switch的判断表达式是枚举或标记联合时,在分支判断处可省略类型。

在同一个分支上,可以包括相同类型的联合属性,如本例中的 .a1 和 .a3。

test_switch_enum.zig

const expect=@import("std").testing.expect;
const uni=union(enum){a1:u32,a2:i64,a3:u32,b:bool};
const i=uni{.b=false};
test "condition is enum or tagged union" {
    switch(i) {
        .a1, .a3 => {},
        uni.a2 => {},
        .b => try expect(!i.b),
    }
}

10.3. 捕获(capture)

捕获是指在选择、循环或遍历期间,获得条件表达式(或容器元素)的值。

捕获得到的值的作用域仅用于流程控制语句对应的语句块。

捕获语法为:

当条件表达式的值是普通类型时,if, while 表达式不能捕获。

switch表达式捕获是可选的,是在选定分支,进入对应表达式后捕获。

for表达式必须要有捕获,且仅for表达式可捕获索引。

test_capture.zig

const expect=@import("std").testing.expect;
test "capture v"{
    const a:i32=5;
    const r=switch(a) {
        0...4 => false,
        else => |v| if(v==5) true else false,
    };
    try expect(r);
}
test "capture index"{
    const a=[_]i32{11,1,33,44};
    for(a) |v,i| {
        if(v==i){
            try expect(i==1);
        }
    }
}
test "capture pointer"{
    var a=[_]i32{1,2,3,4};
    for(a) |*v|{
        v.*+=10;
    }
    const r=[_]i32{11,12,13,14};
    for(a) |v,i|{
        try expect(v==r[i]);
    }
}
test "capture _ "{
    const a=[_]i32{11,2,33,44};
    for(a) |_| {}
}
test "capture in switch" {
    var a=[_]i32{11,22,33,44};
    try expect(foo(&a,0)==11);
    try expect(foo(&a,1)==22);
    try expect(foo(&a,2)==43);
    try expect(a[2]==43);
}
fn foo(addr:*[4]i32,b:usize) i32{
    switch(addr[b]) {
        0...30 => |v| return v,
        31...50 => |*v| {
            v.*+=10;
            return v.*;
        },
        else => return 100,
    }
}

10.4. 可选类型(optional)

if, while 的条件表达式结果值是可选类型时,else子句不能省略。

如果条件表达式的值为 null ,则进入 else 子句。

if, while 表达式必须要有捕获,捕获得到载荷值。

else 子句不能有捕获。

while 表达式的捕获表达式须放在步进表达式之前。

test_if_while_optional.zig

const expect=@import("std").testing.expect;
test "if" {
    const i:?u32=5;
    if(i) |v| {
        try expect(v==5);
    }else{
        try expect(false);
    }
    const j:?u32=null;
    if(j) |_|{
        unreachable;
    }else{
        try expect(j==null);
    }
}        
test "while" {
    var i:?u32=5;
    var num:i32=0;
    while(i) |*v| :(num+=1)  {
        if(v.*==2){i=null;}
        else{v.* -=1;}
    }else{
        try expect(i==null);
        try expect(num==4);
    }
}

10.5. 错误联合类型(error union)

if, while 的条件表达式结果值是错误联合类型时,else子句不能省略。

如果条件表达式的值为错误值 ,则进入 else 子句。

if, while 表达式必须要有捕获,捕获得到载荷值。

else 子句也必须要有捕获,捕获得到错误值。

while 表达式的捕获表达式须放在步进表达式之前。

test_if_while_error_union.zig

const expect=@import("std").testing.expect;
test "if"{
    var a:anyerror!i32=1;
    if(a) |v|{
        try expect(v==1);
    }else |_|{
        unreachable;
    }
    if(a) |v|{
        try expect(v==1);
    }else |_| {}
    a=error.BadValue;
    if(a) |_|{} else |e|{
        try expect(e==error.BadValue);
    }
}
test "while" {
    var i:anyerror!u32=5;
    var num:i32=0;
    while(i) |*v| :(num+=1)  {
        if(v.*==2){i=error.BadValue;}
        else{v.* -=1;}
    }else |e|{
        try expect(e==error.BadValue);
        try expect(i==error.BadValue);
        try expect(num==4);
    }
}

10.6. 错误联合可选类型(error union optional)

if, while 的条件表达式结果值是错误联合可选类型时,else子句不能省略。

如果条件表达式的值为错误值 ,则进入 else 子句。

if, while 表达式必须要有捕获,捕获得到可选类型值。

else 子句也必须要有捕获,捕获得到错误值。

while 表达式的捕获表达式须放在步进表达式之前。

test_error_union_optional_if.zig

const expect=@import("std").testing.expect;
test "if capture values type is optional"{
    const a:anyerror!?i32=1;
    if(a) |v|{
        try expect(v.?==1);
    }else |_| {}
}
test "if a pointer capture"{
    var a:anyerror!?i32=1;
    if(a) |*v|{
        v.*.?=2;
    }else |_| {}
    try expect((a catch @as(i32,0)).?==2);
}
test "if null"{
    const a:anyerror!?i32=null;
    if(a) |v|{
        try expect(v==null);
    }else |_| {}
}
test "if error"{
    const a:anyerror!?i32=error.BadValue;
    if(a) |_|{} else |e|{
        try expect(e==error.BadValue);
    }
}
test "while" {
    var i:anyerror!?u32=5;
    var num:i32=0;
    while(i) |*v| :(num+=1)  {
        if(v.*.?==2){i=error.BadValue;}
        else{v.*.? -=1;}
    }else |e|{
        try expect(e==error.BadValue);
        try expect(i==error.BadValue);
        try expect(num==4);
    }
}

10.7. 标签循环(labeled loop)

10.7.1. 标签while(labeled while)

内嵌循环的带标签的break 或 continue语句,可跳转至对应的外层带标签while语句。

run_label_while.zig

const print=@import("std").debug.print;
pub fn main() void{
    var i:i32=0;
    outer1: while(true){
        while(i<5):(i+=1){
            print("loop.1 i={}\n",.{i});
            if(i==1) break :outer1;
        }
        print("break out,not output\n",.{});
    }
    i=0;
    outer2: while(i<3):(i+=1){
        var j:i32=i+1;
        while(j>=0):(j-=1){
            print("loop.2 i={},j={}\n",.{i,j});
            if(j==1) continue :outer2;
        }
        print("continue skip,not output\n",.{});
    }
}
shell

loop.1 i=0
loop.1 i=1
loop.2 i=0,j=1
loop.2 i=1,j=2
loop.2 i=1,j=1
loop.2 i=2,j=3
loop.2 i=2,j=2
loop.2 i=2,j=1

10.7.2. 标签for(labeled for)

与while类似,for也可带标签。

run_label_for.zig

const print=@import("std").debug.print;
pub fn main() void{
    outer1: for([_]i32{1,2,3}) |i|{
        for([_]i32{1,2,3}) |j|{
            print("break i={},j={}\n",.{i,j});
            break :outer1;
        }
    }
    outer2: for([_]i32{1,2,3}) |i|{
        for([_]i32{1,2}) |j|{
            print("continue i={},j={}\n",.{i,j});
            continue :outer2;
        }
    }
}
shell

break i=1,j=1
continue i=1,j=1
continue i=2,j=1
continue i=3,j=1

10.8. 编译期流程控制(flow control statement at compile time)

如果if语句的条件表达式的值是编译期可知,那么编译期能确定的不可能进入的分支流程不会生成二进制代码。

10.8.1. 编译期switch(switch at compile time)

switch表达式可以在函数外部用来给变量赋值,如下例中的os_msg赋值语句。

在函数内部,如果判断表达式的值是编译期可知的,switch语句则默认是在编译期计算。

test_switch_comptime.zig

const os_msg=switch(builtin.target.os.tag) {
    .linux => "we found a linux user",
    else => "not a linux user",
};
test "switch inside function" {
    switch (builtin.target.os.tag) {
        .fuchsia => {
            @compileError("fuchsia not supported");
        },
        else => {},
    }
}

在本例中,builtin.target.os.tag 的值是编译期可知的,所以如果在非fuchsia操作系统上,编译时 .fuchsia 分支块不会被解析,其分支内@compileError语句不会执行;如果在fuchsia操作系统上,则 .fuchsia语句块被解析,@compileError执行。

10.8.2. 内联switch(inline switch)

switch表达式中的分支加 inline 标记,表示编译时展开该分支所有可能的值。

本例中,当isfieldopt函数的参数T的值是 struct1 时,其inline分支,编译时等同于isfiledopt_roll函数的分支0和分支1;

编译后,等同于isfiledopt_struct1函数。

test_switch_inline.zig

const expect=@import("std").testing.expect;
const expectError = @import("std").testing.expectError;
fn isFieldOpt(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].field_type) == .Optional,
        else => return error.IndexOutOfBounds,
    };
}
const struct1=struct{a:u32,b:?u32};
fn isFieldOpt_roll(comptime T:type, field_index:usize)!bool {
    const fields=@typeInfo(T).Struct.fields;
    return switch(field_index) {
        0 => @typeInfo(fields[0].field_type) == .Optional,
        1 => @typeInfo(fields[1].field_type) == .Optional,
        else => return error.IndexOutOfBounds,
    };
}
fn isfiledopt_struct1(field_index:usize)!bool{
    return switch(field_index) {
        0 => false,
        1 => true,
        else => return error.IndexOutOfBounds,
    };
}
test "inline switch" {
    var index: usize = 0;
    try expect(!try isFieldOpt(struct1, index));
    index+=1;
    try expect(try isFieldOpt(struct1, index));
    index+=1;
    try expectError(error.IndexOutOfBounds, isFieldOpt(struct1, index));
}

10.8.3. 内联while(inline while)

内联 While 循环会展开循环体,主要用于在编译期执行循环,如本例中把类型做为值。

test_inline_while.zig

const expect=@import("std").testing.expect;
test "inline while loop" {
    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 += @typeName(T).len;
    }
    try expect(sum == 9);
}

10.8.4. 内联for(inline for)

与while类似,for也可inline。

test_inline_for.zig

const expect=@import("std").testing.expect;
test "inline while loop" {
    const nums=[_]i32{2,4,6};
    var sum: usize=0;
    inline for(nums) |v|{
        const T=switch(v){
            2 => f32,
            4 => i8,
            6 => bool,
            else => unreachable,
        };
        sum += @typeName(T).len;
    }
    try expect(sum == 9);
}

10.9. 不可达(unreachable)

用Debug和ReleaseSafe编译模式,则当执行到unreachable语句时,程序会崩溃,显示 reached unreachable code.

用ReleaseFastReleaseSmall模式,优化器根据unreachable,对假设的不可达代码优化。

test_unreachable.zig

test "basic math" {
    const x = 1;
    const y = 2;
    if (x + y != 3) {
        unreachable;
    }
}

std.debug.assert的实现如下:

test_assert.zig

fn assert(ok: bool) void {
    if (!ok) unreachable;
}
test "this will fail" {
    assert(false);
}

1/1 test.this will fail... thread 142805 panic: reached unreachable code

unreachable的类型是noreturn,但本例无法编译,因为执行到内有unreachable的语句,是编译错误。

test_unreachable_comp.zig

const assert = @import("std").debug.assert;
test "type of unreachable" {
    comptime {
        assert(@TypeOf(unreachable) == noreturn);
    }
}

error: unreachable code

11. 编译期(comptime)

Zig语言的重点之一就是表达式在编译期是否可知的concept,这有助于程序更快、更可读和更强大。

11.1. 编译期概念(the compile-time concept)

11.1.1. 编译期参数(compile-time parameter)

编译期参数是编译期鸭子类型,以实现泛型。

在Zig语言中,类型可以分配给变量,做为函数的参数值和返回值。但类型只能在编译期可知的表达式中使用,所以本例中参数 T 必须用comptime修饰。

test_comtime.zig

const expect=@import("std").testing.expect;
fn max(comptime T:type,a:T,b:T) T {
    return if (a>b) a else b;
}
fn BiggerInt(a:u64,b:u64) u64 {
    return max(u64,a,b);
}
test "comptime generic"{
    const a:u64=1; const b:u64=5;
    try expect(BiggerInt(a,b)==5);
    const c:f32=1.1; const d:f32=2.2;
    try expect(max(f32,c,d)==2.2);
}

comptime参数是指:在调用处,必须在编译期可知参数值;在函数定义中,参数值在编译期可知。下例编译出错,因为试图将仅在运行期可知的值cond,传递给编译期参数T。

test_comknown.zig

fn max(comptime T:type,a:T,b:T) T {
    return if (a>b) a else b;
}
fn foo(cond:bool) void{
    const r=max(if(cond) f32 else i32, 1,2);
    _=r;
}
test "try to pass a runtime type"{
    var i:bool=true;
    foo(i);
}
shell

$ zig test test_comknown.zig
test_comknown.zig:9:17: error: unable to resolve comptime value
 const r=max(if(cond) f32 else i32, 1,2);
                ^```
test_comknown.zig:9:17: note: condition in comptime branch must be comptime known

如果在解析函数参数的类型值时,进行非法操作也会出错。因为bool类型不能比较大小,所以下例编译时出错。

test_maxbool.zig

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);
}
shell

$ zig test test_compbool.zig
test_compbool.zig:6:17: error: operator > not allowed for type 'bool'
    return if (a>b) a else b;
               ~^~

上例的代码改进后,测试通过。

test_maxbool1.zig

const expect=@import("std").testing.expect;
fn max(comptime T:type,a:T,b:T) T {
    if(T==bool){
        return a or b;
    }else{
    return if (a>b) a else b;
    }
}
test "try to compare bools"{
    try expect(max(bool,true,false)==true);
}

if表达式的条件如果是编译期可知的,则编译时所有处理编译期可知的代码被删除,保证跳过未采用的分支。本例中max函数实际生成为:

fn max(a:bool,b:bool)bool{ return a or b;}

对switch表达式也是如此。

名词解释:

鸭子类型(:duck type) 英文俗语有:当一只鸟走路像鸭子,游水像鸭子,叫声像鸭子,那它就是鸭子。鸭子类型是可以简单理解为用各种语法把类型做为值,编译期动态确定运行逻辑;而不是用类继承体系来实现。

11.2. 编译期变量(compile-time variables)

变量标记为comptime,保证编译器在编译期对变量读写。例如可以编译一部分在编译期计算、一部分在运行期计算的函数。

test_comptime_var.zig

const expect=@import("std").testing.expect;
const CmdFn=struct{
    name: []const u8,
    func: fn(i32) i32,
};
const fnstr=[_]CmdFn{
    CmdFn{.name="one",.func=one},
    CmdFn{.name="two",.func=two},
    CmdFn{.name="three",.func=three},
};
fn one(v:i32) i32{return v+1;}
fn two(v:i32) i32{return v+2;}
fn three(v:i32) i32{return v+3;}
fn perform(comptime ch:u8,v:i32) i32{
    var r:i32=v;
    comptime var i=0;
    inline while (i<fnstr.len):(i+=1){
        if (fnstr[i].name[0]==ch) {
            r=fnstr[i].func(r);
        }
    }
    return r;
}

test "perform fn" {
    try expect(perform('o',2)==3);
    try expect(perform('t',2)==7);
    try expect(perform('w',2)==2);
}

这个例子主要展示编译期变量用法,全部在运行期也能正常运行。这个示例针对不同ch的值,会最终产生不同的函数:

// perform('o',2)

fn perform(v:i32) i32{

var r:i32=v;

r=one(r); return r;

}

//perform('t',2)

fn perform(v:i32) i32{

var r:i32=v;

r=two(r);

r=three(r); return r;

}

//perform('w',2)

fn perform(v:i32) i32{

var r:i32=v;

return r;

}

在Debug模式下构建也是这样,在release类的模式下构建这些生成的函数仍要通过严格的LLVM优化。

编译期计算的方法的主要目的不要用于代码优化,而应该用于确保编译期发生的确实在编译时计算。这样可以达到像其它用宏、预处理器等语言开发的效果,参看后面例子。

名词解释:

LLVM(low level virtual machine) 直译为底层虚拟机,实际上是目前主流的编译工具链之一,[LLVM主页](https://llvm.org/)

11.3. 编译期表达式(compile-time expressions)

用comptime表达式来保证在编译期计算。本例中,编译期调用exit函数(或任何其它外部函数)无意义,所以编译错误。

test_comptime_err.zig

extern fn exit() noreturn;
test "foo" {
    comptime {exit();
    }
}

test_comptime_err.zig:3:19: error: comptime call of extern function

在comptime表达式内:

所有变量是comtime变量;

所有if, while, for, switch表达式均在编译期计算;

所有函数调用是在编译期解析,如果函数试图运行有全局副作用影响的代码,则编译错误。

这样,可以创建同一个函数,即可在编译期被调用,也可在运行期被调用。

test_comptime_fib.zig

const expect = @import("std").testing.expect;
fn fib(i:u32) u32{
    if (i<2) return i;
    return fib(i-1)+fib(i-2);
}
test "fibonacci" {
    try expect(fib(7)==13);
    comptime {
        try expect(fib(7)==13);
    }
}

本例中省略fib函数中的初始值判断,编译时出错。

test_fib_err.zig

const expect = @import("std").testing.expect;
fn fib(i:u32) u32{
    //if (i<2) return i;
    return fib(i-1)+fib(i-2);
}
test "fibonacci" {
    comptime {
        try expect(fib(7)==13);
    }
}

test_fib_err.zig:4:17: error: overflow of integer type 'u32' with value '-1'

上例用的是无符号整数,这样试图0-1时,触发未定义行为,如果编译期可知,则是编译错误。

如果是下例中的有符号整数,编译器在编译期计算花费很多时间,则放弃并产生编译错误。

如果想增加编译预期时间,用

@setEvalBranchQuota函数将1000改为其它值。

test_fib_err1.zig

const expect = @import("std").testing.expect;
fn fib(i:i32) i32{
    //if (i<2) return i;
    return fib(i-1)+fib(i-2);
}
test "fibonacci" {
    comptime {
        try expect(fib(7)==13);
    }
}
shell

$ zig test test_fib_err1.zig
test_fib_err1.zig:4:15: error: evaluation exceeded 1000 backwards branches
    return fib(i-1)+fib(i-2);
           ``` ^``
test_fib_err1.zig:4:15: note: use @setEvalBranchQuota() to raise the branch limit from 1000
test_fib_err1.zig:4:15: note: called from here (999 times)
test_fib_err1.zig:8:23: note: called from here
        try expect(fib(7)==13);
                   ``` ^~~

如果fib函数正确,而expect函数中期望值错误,如下例:

test_fib_err2.zig

const expect = @import("std").testing.expect;
fn fib(i:i32) i32{
    if (i<2) return i;
    return fib(i-1)+fib(i-2);
}
test "fibonacci" {
    comptime {
        try expect(fib(7)==99);
    }
}
shell

zig test test_fib_err2.zig
Test [1/1] test.fibonacci... FAIL (TestUnexpectedResult)
C:/dev/zig/test_fib_err2.zig:8:23: 0x7ff70bbf100e in test.fibonacci (test.exe.obj)
        try expect(fib(7)==99);
                      ^
0 passed; 0 skipped; 1 failed.

容器级下(任何函数之外),任何表达式都是编译期表达式。这样可以用函数来初始化复杂的静态数据。

test_comptime_initstatic.zig

const expect = @import("std").testing.expect;
const f5=firstn(5);
const s=sum(&f5);
fn firstn(comptime n:u32) [n]u32{
    var pl:[n]u32=undefined;
    var i=0;
    while(i<n):(i+=1){
        pl[i]=i*2;
    }
    return pl;
}
fn sum(num: []const u32) u32{
    var r:u32=0;
    for(num) |x| {r+=x;}
    return r;
}
test "variable values"{
    try expect(s==20);
}

编译器会用事先计算好的结果来生成常量,下面是生成的LLVM IR中的代码行:


@0 = internal unnamed_addr constant [5 x i32] [i32 0, i32 2, i32 4, i32 6, i32 8]
@1 = internal unnamed_addr constant i32 20

这里,我们不需要对函数语法做任何特殊设置,就像调用参数是长度和值仅运行期可知的切片的sum函数。

11.4. 编译期指针(comptime pointer)

只要源代码不依赖于未定义的内存布局,指针也可以在编译时工作。

test_compile_pointer.zig

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);
    }
}

11.4.1. 编译期内存地址值(comptime address)

只要指针没有被解引用,在编译期代码中也可保存内存地址。

test_comptime_pointer_conversion.zig

const expect = @import("std").testing.expect;
test "comptime @intToPtr" {
    comptime {
        const ptr = @intToPtr(*i32, 0xdeadbee0);
        const addr = @ptrToInt(ptr);
        try expect(@TypeOf(addr) == usize);
        try expect(addr == 0xdeadbee0);
    }
}

11.5. 泛型数据结构(generic data structure)

基于编译期计算可实现泛型。为保持语言小巧和一致,所有聚合类型是匿名的。为了错误信息和调试,从创建匿名结构时调用的函数名和参数推断出名称 List(i32)。

把类型赋值给常量,常量名即为类型名,如本例中ListInt。

test_generic.zig

const expect = @import("std").testing.expect;
fn List(comptime T:type) type{
    return struct {
        pub const Node=struct{
            next: ?*Node,
            data: T,
        };
        head: ?*Node,
        len:   usize,
    };
}
const ListInt=List(i32);
test "generic"{
    var l=ListInt{.head=null,.len=0};
    try expect(@TypeOf(l)==List(i32));
    var n=ListInt.Node{.next=null,.data=10};
    l.head=&n; l.len=1;
    try expect(l.head.?.data==10);
}

名词解释:

泛型(:generic) 没有泛型功能的函数,其参数类型一经定义不可变。有泛型的函数,本质上是把参数的类型当成编译期可知的变量。当参数类型在调用函数的源代码中给定后,实例化到给定的类型。每个实例化都生成不同的目标代码。滥用泛型会导致代码膨胀。

11.6. @setEvalBranchQuota

@setEvalBranchQuota(comptime new_quota:u32)

说明:在放弃并产生编译错误之前,更改编译期代码执行可以使用的最大回退分支数。

如果 new_quota 小于默认值(1000)或先前明确设置,则忽略它。

示例

test_setevalbranchquota1.zig

test "not use setevalbranchquota" {
    comptime {
        var i = 0;
        while (i < 1001) : (i += 1) {}
    }
}
shell

$ zig test test_setevalbranchquota1.zig
error: evaluation exceeded 1000 backwards branches

用@setEvalBranchQuota后,则测试通过。

test_setevalbranchquota2.zig

test "use setevalbranchquota" {
    comptime {
        @setEvalBranchQuota(1001);
        var i = 0;
        while (i < 1001) : (i += 1) {}
    }
}

11.7. @compileError

@compileError(comptime msg:[]u8)

语义分析时,引发编译错误并显示 msg。

用条件表达式是编译期常量的if或switch语句,以及comptime函数,来避免对代码进行语义分析。

11.8. @compileLog

@compileLog(args:...)

显示输出在编译时传递给它的参数

为防止 @compileLog 遗留在代码库中,构建时 @compileLog引发编译错误,这样可以防止生成代码,但不影响解析。

test_compilelog.zig

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});
}
shell

$ zig test test_compilelog.zig
test_compilelog.zig:9:5: error: found compile log statement
    @compileLog("comptime in main");
    ^``` ```` ```` ```~~
test_compilelog.zig:4:5: note: also here
    @compileLog("comptime val1 = ",val1);
    ^``` ```` ```` ```` ````
Compile Log Output:
@as(*const [16:0]u8, "comptime in main")
@as(*const [16:0]u8, "comptime val1 = "), @as(i32, 99)

如果删除所有 @compileLog 调用,或者解析没有遇到这些调用,程序就会成功编译,并测试通过。

test_without_complog.zig

const print = @import("std").debug.print;
const num1 = blk: {
    var val1:i32=99;
    val1=val1+1;
    break :blk val1;
};
test "main" {
    print("Runtime in main, num1 = {}.\n",.{num1});
}

12. 反射(reflection)

反射是指在编译期或运行期,获取变量和类型的信息。

12.1. 函数反射(function reflection)

可得到函数定义的相关信息。

test_func_reflect.zig

const expect=@import("std").testing.expect;
test "fn reflection" {
    try expect(@typeInfo(@TypeOf(expect)).Fn.args[0].arg_type.? == bool);
    try expect(@typeInfo(@TypeOf(expect)).Fn.is_var_args == false);
}

12.1.1. @src

@src() std.builtin.SourceLocation

返回结构值,表示函数在源代码中的名称和位置。@src必须在函数中调用。

test_src.zig

const std=@import("std");
const expect=std.testing.expect;
test "@src" {
    try doTheTest();
}
fn doTheTest() !void {
    const src=@src();
    try expect(src.line==7);
    try expect(src.column==15);
    try expect(std.mem.endsWith(u8,src.fn_name,"doTheTest"));
    try expect(std.mem.endsWith(u8,src.file,"src.zig"));
}

12.2. 枚举反射(enum reflect)

可取得枚举属性个数、名字、标签类型等信息。

test_enum_reflect.zig

const expect=@import("std").testing.expect;
const mem = @import("std").mem;
const foo=enum{zero,one,two};
test "enum reflect"{
    try expect(@typeInfo(foo).Enum.tag_type==u2);
    try expect(@typeInfo(foo).Enum.fields.len==3);
    try expect(mem.eql(u8, @typeInfo(foo).Enum.fields[1].name, "one"));
    try expect(mem.eql(u8,@tagName(foo.two), "two"));
}

12.3. 标记联合反射(tagged union reflection)

用std.meta.Tag函数可得到标记联合的标记类型。

用@tagName函数可得到标记联合的标记名。

test_taged_union_reflect.zig

const std=@import("std");
const expect=@import("std").testing.expect;
const fooTag=enum{ok,notok};
const foo=union(fooTag){ok:u8,notok:void};
test "get tag type"{
    try expect(std.meta.Tag(foo)==fooTag);
}
test "get tag Name"{
    try expect(std.mem.eql(u8,@tagName(foo.ok), "ok"));
}

12.4. 可选类型反射(optional reflection)

可选类型可在编译期反射,得到其正常值类型。

test_opt_child.zig

const expect=@import("std").testing.expect;
test "optional child type"{
    const i: ?i32=5;
    comptime try expect(@typeInfo(@TypeOf(i)).Optional.child==i32);
}

12.5. 错误联合类型反射(error union reflection)

可在编译时得到错误联合类型的类型信息。

test_errunion_reflect.zig

const expect=@import("std").testing.expect;
test "error union reflection" {
    var foo: anyerror!i32 = undefined;
    foo = 1234;
    foo = error.SomeError;
    comptime try expect(@typeInfo(@TypeOf(foo)).ErrorUnion.payload == i32);
    comptime try expect(@typeInfo(@TypeOf(foo)).ErrorUnion.error_set == anyerror);
}

12.6. 指针反射(pointer reflection)

获知指针指向类型。

test_pointer_reflect.zig

const expect=@import("std").testing.expect;
test "pointer reflection" {
    try expect(u32==@typeInfo(*u32).Pointer.child); // u32
}

12.7. 类型反射(type reflection)

12.7.1. @Type

@Type(comptime info:std.builtin.Type) type

说明:是@typeInfo的逆操作,它将类型信息具体化为一个类型。

对函数,BoundFn无效。

示例:

test_type.zig

const expect=@import("std").testing.expect;
test "@Type" {
    try expect(i32==@Type(@typeInfo(i32)));
}

12.7.2. @typeInfo

@typeInfo(comptime T:type) std.builtin.Type

说明:提供类型反射。

结构、联合、枚举、错误集的类型信息的属性保证与源文件中的外观顺序相同。

示例:

test_typeInfo.zig

const print=@import("std").debug.print;
pub fn main() void{
    print("{}\n",.{@typeInfo(u32)});
}
shell

builtin.Type{ .Int = builtin.Type.Int{ .signedness = builtin.Signedness.unsigned, .bits = 32 } }

12.7.3. @TypeOf

@TypeOf(...) type

说明:这是一个特殊的内置函数,它接受任意(非零)数量的表达式作为参数,并使用对等类型解析返回结果的类型。

这些表达式是可以计算的,但是它们保证不会产生运行时副作用。

示例:

no_runtime_side_effects.zig

const expect=@import("std").testing.expect;
test "no runtime side effects" {
    var data: i32 = 0;
    const T = @TypeOf(foo(i32, &data));
    comptime try expect(T == i32);
    try expect(data == 0);
}
fn foo(comptime T: type, ptr: *T) T {
    ptr.* += 1;
    return ptr.*;
}

12.8. 名字反射(name reflection)

12.8.1. @typeName

@typeName(T:type) *const [N:0]u8

说明:以数组的形式返回类型的字符串表示。它等效于类型名称的字符串字面值。返回的类型名称是含有一系列点操作符的类型名。

run_typeName.zig

const print=@import("std").debug.print;
pub fn main() void{
    const foo=struct{x:i32,y:i32};
    print("{s}\n",.{@typeName(foo)});
    print("{s}\n",.{@typeName(i32)});
}
shell

run_typeName.main.foo
i32

12.8.2. @errorName

@errorName(err:anyerror) [:0]const u8

说明:返回错误的字符串表示。

如果整个程序没有调用@errorName,或者所有@errorName调用的 err 参数都是编译期可知的,则不会生成错误名称表。

示例:

run_errorName.zig

const print=@import("std").debug.print;
pub fn main() void{
    const e=error.OutOfMem;
    print("{s}\n",.{@errorName(e)}); // OutOfMem
}

12.8.3. @tagName

@tagName(value:anytype) [:0]const u8

说明:把枚举或联合的值转换为其名字的字符串字面值。

如果枚举是非穷举的,且标记值没有映射名字,则调用安全检查未定义行为。

示例:

test_tagName.zig

const print=@import("std").debug.print;
const foo=enum{one,two,three};
pub fn main() void{
    print("{s}\n",.{@tagName(foo.one)}); // one
}

12.8.4. @field

@field(lhs:anytype,comptime field_name:[]const u8) (field)

说明:通过编译时的字符串访问内部属性和内部定义变量。

test_field_decl_access_bystr.zig

const expect=@import("std").testing.expect;
const Point=struct {
    x:u32, y:u32,
    pub var z:u32=1;
};
test "field access by string" {
    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" {
    try expect(@field(Point,"z") == 1);
    @field(Point,"z")=2;
    try expect(@field(Point,"z") == 2);
}

12.8.5. @hasField

@hasField(comptime Container:type,comptime name:[]const u8) bool

说明:返回结构、枚举、联合内,是否有与 name 匹配的属性。

不检查是否匹配函数、变量或常量。

结果是编译期常数。

test_hasField.zig

const expect=@import("std").testing.expect;
const Foo = struct {
    nope:i32,
    pub var blah ="xxx";
    const hi=1;
};
test "@hasField" {
    try expect(!@hasField(Foo,"blah"));
    try expect(!@hasField(Foo,"hi"));
    try expect(@hasField(Foo,"nope"));
    try expect(!@hasDecl(Foo,"nope1234"));
}

12.8.6. @hasDecl

@hasDecl(comptime Container:type,comptime name:[]const u8) bool

说明:返回结构、枚举、联合内,是否有与 name 匹配的定义变量。

不检查是否匹配属性。

如果 @hasDecl 调用是在另一个文件,则返回 false。

示例:

test_hasdesl.zig

const expect=@import("std").testing.expect;
const Foo = struct {
    nope:i32,
    pub var blah ="xxx";
    const hi=1;
};
test "@hasDecl" {
    try expect(@hasDecl(Foo,"blah"));
    try expect(@hasDecl(Foo,"hi"));
    try expect(!@hasDecl(Foo,"nope"));
    try expect(!@hasDecl(Foo,"nope1234"));
}

12.9. 位长反射(size reflection)

12.9.1. @sizeOf

@sizeOf(comptime T:type) comptime_int

说明:返回在内存中保存 T 所需的字节数。结果是一个特定目标平台的编译期常数。

结果值可能包含填充字节。

如果内存中有两个连续的 T,结果值是位于索引0的元素和位于索引1的元素之间,以字节为单位的偏移量。

对于整数,分情况使用@sizeOf(T) 或 @typeInfo(T).Int.bits.

此函数在运行时测量字节位长。对于运行时不允许的类型,如 comptime_int 和 type,结果是0。

test_sizeOf.zig

const expect=@import("std").testing.expect;
test "@sizeOf" {
    try expect(@sizeOf(i32)==4);
    try expect(@sizeOf(type)==0);
}

12.9.2. @alignOf

@alignOf(comptime T:type) comptime_int

说明:返回当前目标平台的C语言ABI下,该类型应对齐的字节数。结果是特定目标平台的编译期常数,保证小于等于 @sizeOf(T)。

当指针指向数据的类型与此字节数相等时,可以在指针定义中省略对齐方式。

示例:

test_alignof.zig

const assert = @import("std").debug.assert;
test "@alignOf" {
    const i=@alignOf(u32);
    try expect(i==4);
    try expect(*u32 == *align(i) u32);
}

12.9.3. @offsetOf

@offsetOf(comptime T:type,comptime field_name:[]const u8) comptime_int

说明:返回结构内属性的字节偏移量。

示例:

test_offsetOf.zig

const expect=@import("std").testing.expect;
const foo=struct{x:i64,y:i32};
test "@sizeOf" {
    try expect(@offsetOf(foo,"x")==0);
    try expect(@offsetOf(foo,"y")==8);
}

12.9.4. @bitOffsetOf

@bitOffsetOf(comptime T:type,comptime field_name:[]const u8) comptime_int

说明:返回结构内属性的位偏移量。

对非压缩结构,总可以被8整除;对压缩结构,未按字节对齐的字段共享字节偏移量,但位偏移量不一样。

test_bitOffsetOf.zig

const expect=@import("std").testing.expect;
const foo=packed struct{w:u8,x:u3,y:u2,z:u3};
test "@sizeOf" {
    try expect(@offsetOf(foo,"y")==1);
    try expect(@bitOffsetOf(foo,"y")==11);
}

12.9.5. @bitSizeOf

@bitSizeOf(comptime T:type) comptime_int

对压缩结构或压缩联合的属性,则返回在内存中保存 T 所需的比特位数。结果是一个特定目标平台的编译期常数。

此函数在运行时测量比特位长,对于运行时不允许的类型,如 comptime_int 和 type,结果是0。

test_bitSizeOf.zig

const expect=@import("std").testing.expect;
const foo=packed struct{w:u8,x:u3,y:u2,z:u3};
const f:foo=undefined;
test "@sizeOf" {
    try expect(@bitSizeOf(i16)==16);
    try expect(@bitSizeOf(@TypeOf(f.w))==8);
    try expect(@bitSizeOf(@TypeOf(f.y))==2);
}

12.10. @This

@This() type

说明:返回此函数调用所在的最内层结构、枚举和联合。这对于需要引用自身的匿名结构非常有用。

test_this.zig

const expect=@import("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;
        }
    };
}

在文件作用域使用@This()时,返回对当前文件的结构的引用。

13. 未定义行为(undefined behavior)

未定义行为简称为UB,是指源代码符合语法规范,但真实运行逻辑有错,会产生不可预料的行为。

例如下面的代码均符合语法规范,但明显逻辑有错。


var i:i32=0; var j=5/i;
var a=[_]i32{1,2}; var i:usize=5; var j=a[i];

在编译期检测到未定义行为,会产生编译错误并退出编译。

当有安全检查时,大多数编译期检测不到的未定义行为,都可以在运行期检测到。

如果安全检查发现未定义行为,通常会崩溃退出,并产生栈追踪。

用@setRuntimeSafety可以在语句块禁用安全检查。

为了优化,ReleaseFast和ReleaseSmall构建模式禁用安全检查(内含@setRuntimeSafety的块除外)。

13.1. @setRuntimeSafety

@setRuntimeSafety(comptime safety_on:bool) void

设置是否对调用该函数的作用域启用运行时安全检查。

~run_setruntimesafety.zig

pub fn main() void{

//本块内启用安全检查,即使在 ReleaseFast 和 ReleaseSmall 模式下,也进行安全检查。

@setRuntimeSafety(true);

var y:u8=255;

y +=1;

{

//安全检查是否启用可以在任何作用域内被改写,所以本块关闭安全检查,整数溢出在任何模式下不会被捕获。

@setRuntimeSafety(false);

var z:u8=255;

z +=1;

}

}

shell

$ zig run run_setruntimesafety.zig -O Debug
thread 8180 panic: integer overflow
$ zig run run_setruntimesafety.zig -O ReleaseFast
thread 2392 panic: integer overflow

run_setruntimesafety1.zig

pub fn main() void{
    //本块内启用安全检查,即使在 ReleaseFast 和 ReleaseSmall 模式下,也进行安全检查。
    @setRuntimeSafety(true);
    //var y:u8=255;
    //y +=1;
    {
    //安全检查是否启用可以在任何作用域内被改写,所以本块关闭安全检查,整数溢出在任何模式下不会被捕获。
        @setRuntimeSafety(false);
        var z:u8=255;
        z +=1;
    }
}
shell

$ zig run run_setruntimesafety.zig -O Debug
$ zig run run_setruntimesafety.zig -O ReleaseFast

注意: 将来计划将 @setRuntimeSafety 替换为 @OptimizeFor

13.2. @panic

@panic(message:[]const u8) noreturn

调用崩溃处理函数。

崩溃处理函数默认调用根代码文件中公开的 panic 函数,如果没有指定,则从 std/builtin.zig 中调用 std.builtin.default_panic 函数。

通常建议使用@import("std").debug.panic。然而,@panic可以用于下列两种情况:

从库代码中,如果程序员的崩溃函数在根代码文件中是公开的,则调用@panic;

混合C语言源代码和Zig语言源代码时,跨多个.o文件的标准panic实现。

13.3.1. 运行unreachable代码(reaching unreachable code)

编译时:

test_cub1.zig

fn foo(b:bool) void{
    if(!b) unreachable;
}
comptime{
    foo(false);
}

error: reached unreachable code

运行时:

test_rub1.zig

fn foo(b:bool) void{
    if(!b) unreachable;
}
test "unreachable UB"{
    foo(false);
}

Test [1/1] test.unreachable UB... thread 2696 panic: reached unreachable code

13.3.2. 超索引范围(index out of bounds)

编译时:

test_cub2.zig

comptime {
    const a: [5]u8="hello".*;
    const g=a[5];
    _=g;
}

error: index 5 outside array of length 5

运行时:

test_rub2.zig

test "index out of bounds UB"{
    const a: [5]u8="hello".*;
    var i:usize=5;
    const g=a[i];
    _=g;
}

Test [1/1] test.index out of bounds UB... thread 8924 panic: index out of bounds: index 5, len 5

13.3.3. 有符号整数转换到无符号整数(cast negative number to unsigned number)

编译时:

test_cub3.zig

comptime{
    var v:i32=-1;
    const i=@intCast(u32,v);
    _=i;
}

error: type 'u32' cannot represent integer value '-1'

运行时:

test_rub3.zig

test "cast negative number to unsigned number"{
    var v:i32=-1;
    const i=@intCast(u32,v);
    _=i;
}

Test [1/1] test.cast negative number to unsigned number... thread 6620 panic: attempt to cast negative value to unsigned integer

13.3.4. 转换时截断数据(cast truncates data)

为了截断多余位,应该用@truncate函数。

编译时:

test_cub4.zig

comptime{
    var v:i32=300;
    const i=@intCast(i8,v);
    _=i;
}

error: type 'i8' cannot represent integer value '300'

运行时:

test_rub4.zig

test "cast truncates data"{
    var v:i32=300;
    const i=@intCast(i8,v);
    _=i;
}

Test [1/1] test.cast truncates data... thread 3200 panic: integer cast truncated bits

13.3.5. 整数溢出(integer overflow)

13.3.5.1. 默认运算(default opertions)

+ 加, - 减, - 取负值, * 乘,

/ @divTrunc @divFloor @divExact 除

上面的运算符都可以引发整数溢出。

编译时:

test_cub5.zig

comptime{
    var v:u8=255;
    v +=2;
}

error: overflow of integer type 'u8' with value '257'

运行时:

test_rub5.zig

test "integer overflow"{
    var v:u8=255;
    v +=2;
}

Test [1/1] test.integer overflow... thread 13532 panic: integer overflow

13.3.5.2. 标准库数学函数(standard library math functions)

标准库的下列函数可能会返回错误:

@import("std").math.add sub mul divTrunc divFloor divExact shl

本例抓取加法溢出。

run_rub52.zig

const math=@import("std").math;
const print=@import("std").debug.print;
pub fn main() !void{
    var v:u8=255;
    v=if(math.add(u8,v,2)) |r| r else |e|{
        print("unable to add two: {s}\n",.{@errorName(e)});
        return e;
    };
    print("result: {}\n",.{v});
}
shell

$ zig run run_rub52.zig
unable to add two: Overflow
error: Overflow
D:\zig\lib\std\math.zig:490:5: 0x7ff6dc0e1397 in add__anon_2564 (hello.exe.obj)
    return if (@addWithOverflow(T, a, b, &answer)) error.Overflow else answer;
    ^
D:/ziglearn/run_rub52.zig:10:3: 0x7ff6dc0e12a0 in main (run_rub52.exe.obj)
                return e;
  ^

13.3.5.3. 内置函数溢出(builtin overflow functions)

下列内置函数返回bool值表示是否有溢出,同时在输入参数以指针方式返回溢出后数值。

@addWithOverflow, @subWithOverflow, @mulWithOverflow, @shlWithOverflow

本例测试通过。

test_rub53.zig

const expect=@import("std").testing.expect;
fn add254(i:u8, ptr:*u8) bool{
    if(@addWithOverflow(u8,254,i,ptr)){
        return false;
    }else{
        return true;
    }
}
test "@addWithOverflow"{
    var r:u8=undefined;
    try expect(add254(1,&r)==true);
    try expect(r==255);
    try expect(add254(10,&r)==false);
    try expect(r==8);
}

13.3.5.4. 回绕运算(wrapping operations)

+% 回绕加, -% 回绕减, -% 回绕取负值, *% 回绕乘

上面的运算是回绕运算。

test_rub54.zig

const expect=@import("std").testing.expect;
const minInt = std.math.minInt;
const maxInt = std.math.maxInt;
test "wraparound addition and subtraction" {
    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));
}

13.3.6. 精确左移溢出(exact left shift overflow)

和普通左移溢出的区别是,只要移出的位有bit 1就是溢出。

编译时:

test_cub6.zig

comptime {
    const x =@shlExact(@as(u8,0b01010101),2);
    _ = x;
}

error: operation caused overflow

运行时

test_rub6.zig

test "exact left shift overflow"{
    var y:u8=0b01010101;
    var x=@shlExact(y,2);
    _ = x;
}

Test [1/1] test.exact left shift overflow... thread 14132 panic: left shift overflowed bits

13.3.7. 精确右移溢出(exact right shift overflow)

和普通右移溢出的区别是,只要移出的位有bit 1就是溢出。

编译时:

test_cub7.zig

comptime {
    const x=@shrExact(@as(u8,0b10101010),2);
    _ =x;
}

error: exact shift shifted out 1 bits

运行时:

test_rub7.zig

test "exact right shift overflow" {
    var y:u8=0b10101010;
    var x=@shrExact(@as(u8,y),2);
    _ =x;
}

Test [1/1] test.exact right shift overflow... thread 7584 panic: right shift overflowed bits

13.3.8. 除零(division by zero)

编译时:

test_cub8.zig

comptime {
    var a:u32=0;
    var b:u32=15;
    _=b/a;
}

error: division by zero here causes undefined behavior

运行时:

test_rub8.zig

test "division by zero" {
    var a:u32=0;
    var b:u32=15;
    _=b/a;
}

Test [1/1] test.division by zero... thread 10292 panic: division by zero

13.3.9. 精确除有余数(exact division remainder)

除法运算有余数则出错。

编译时:

test_cub9.zig

comptime {
    const a:u32=10;
    const b:u32=3;
    _=@divExact(a,b);
}

error: exact division produced remainder

运行时:

test_rub9.zig

test "exact divisioin reaminder" {
    var a:u32=10;
    var b:u32=3;
    _=@divExact(a,b);
}

Test [1/1] test.exact divisioin reaminder... thread 8568 panic: exact division produced remainder

13.3.10 试图解包裹null(attempt to unwrap null)

编译时:

test_cub10.zig

comptime {
    const i:?i32=null;
    _=i.?;
}

error: unable to unwrap null

运行时:

test_rub10.zig

test "attempt to unwrap null" {
    var i:?i32=null;
    _=i.?;
}

Test [1/1] test.attempt to unwrap null... thread 18204 panic: attempt to use null value

13.3.11. 试图解包裹错误(attempt to unwrap error)

编译时:

test_cub11.zig

fn geterr() !i32{
    return error.notint;
}
comptime {
    const i=geterr() catch unreachable;
    _=i;
}

error: caught unexpected error 'notint'

运行时:

test_rub11.zig

fn geterr() !i32{
    return error.notint;
}
test "attempt to unwrap error" {
    var i=geterr() catch unreachable;
    _=i;
}

Test [1/1] test.attempt to unwrap error... thread 3168 panic: attempt to unwrap error: notint

13.3.12. 无效错误码(invalid error code)

编译时:

test_cub12.zig

comptime {
    const e=error.eone;
    const i=@errorToInt(e)+10;
    _=@intToError(i);
}

error: integer value '11' represents no error

运行时:

test_rub12.zig

const print=@import("std").debug.print;
test "invalid error code" {
    var e=error.eone;
    var i=@errorToInt(e)+500;
    var j=@intToError(i);
    print("{}\n",.{j});
}

Test [1/1] test.invalid error code... thread 13012 panic: invalid error code

13.3.13. 无效枚举转换(invalid enum cast)

编译时:

test_cub13.zig

const foo=enum{a,b,c};
comptime {
    const a:u2=3;
    const b=@intToEnum(foo,a);
    _=b;
}

error: enum 'test_cub13.foo' has no tag with value '3'

运行时:

test_rub13.zig

const foo=enum{a,b,c};
test "invalid enum cast" {
    var a:u2=3;
    var b=@intToEnum(foo,a);
    _=b;
}

Test [1/1] test.invalid enum cast... thread 15908 panic: invalid enum value

13.3.14. 无效错误集转换(invalid error set cast)

编译时:

test_cub14.zig

const s1=error{A,B};
const s2=error{A,C};
comptime {
    _=@errSetCast(s2,s1.B);
}

error: 'error.B' not a member of error set 'error{A,C}'

运行时:

test_rub14.zig

const print=@import("std").debug.print;
const s1=error{A,B};
const s2=error{A,C};
test "invalid error set cast" {
    var i=s1.B;
    var x=@errSetCast(s2,i);
    print("{}\n",.{x});
}

Test [1/1] test.invalid error set cast... thread 7312 panic: invalid error code

13.3.15. 不正确的指针对齐(incorrect pointer alignment)

编译时:

test_cub15.zig

comptime {
    const ptr=@intToPtr(*align(1) i32,0x1);
    const a=@alignCast(4,ptr);
    _=a;
}

error: pointer address 0x1 is not aligned to 4 bytes

运行时:

test_rub15.zig

test "incorrect pointer alignment" {
    var i:i32=15;
    var ptr=&i;
    var ptr1=@ptrToInt(ptr);
    ptr1+=1;
    ptr=@intToPtr(*i32,ptr1);
    _=ptr.*;
}

Test [1/1] test.incorrect pointer alignment... thread 15972 panic: incorrect alignment

13.3.16. 联合属性访问错误(wrong union field access)

编译时:

test_cub16.zig

const foo=union{u:u64,i:i32};
comptime{
    var f=foo{.i=15};
    f.u=1;
}

error: access of union field 'u' while field 'i' is active

运行时:

test_rub16.zig

const foo=union{u:u64,i:i32};
test "wrong union field access" {
    var f=foo{.i=15};
    f.u=1;
}

Test [1/1] test.wrong union field access... thread 18668 panic: access of inactive union field

对extern union 和 packed union,这个安全检查无效。

改变union的激活属性可以用指针或undefined,下例可测试通过。

test_ub16.zig

const expect=@import("std").testing.expect;
const foo=union{u:u64,i:i32};
test "change active filed use pointer" {
    var f=foo{.i=15};
    var ptr=&f;
    ptr.*=foo{.u=1};
    try expect(f.u==1);
}
test "change active field use undefined" {
    var f=foo{.i=15};
    f=foo{.u=undefined};
    f.u=1;
    try expect(f.u==1);
}

13.3.17. 浮点转换到整数超范围(out of bounds float to integer cast)

编译时:

test_cub17.zig

comptime {
    var i:f32=2567.0;
    var j:u8=@floatToInt(u8,i);
    _=j;
}

error: float value '2567' cannot be stored in integer type 'u8'

运行时:

test_rub17.zig

test "out of bounds float to integer cast" {
    var i:f32=2567.0;
    var j:u8=@floatToInt(u8,i);
    _=j;
}

Test [1/1] test.out of bounds float to integer cast... thread 17024 panic: integer part of floating point value out of bounds

13.3.18. 指针转换无效null(pointer cast invalid null)

这个未定义行为发生在将地址值为0的指针类型转换为不可能地址值为0的指针时。

允许地址值为0的指针类型有,C指针,optional指针,和allowzero指针。

编译时:

test_cub17.zig

comptime {
    const p:?*i32=null;
    const ptr=@ptrCast(*i32,p);
    _=ptr;
}

error: null pointer casted to type *i32

运行时:

test_rub17.zig

test "pointer cast invalid null"{
    var p:?*i32=null;
    var ptr=@ptrCast(*i32,p);
    _=ptr;
}

Test [1/1] test.pointer cast invalid null... thread 1472 panic: cast causes pointer to be null

14. 内存分配器(allocator)

Zig语言没有运行时虚拟机和垃圾收集,不替程序员进行自动内存管理。这是Zig语言代码可以适应多种平台(例如包括实时软件、操作系统内核、和低延迟服务器等)的原因之一。

在Zig语言中通常不设默认内存分配器,每个需要内存的函数都要有Allocator参数来指定分配器。

14.1. 分配器种类(allocator class)

通常分配器需要调用init函数来初始化,然后调用allocator函数返回具体的分配器变量,再用分配器变量去申请释放管理内存。

14.1.1. 通用分配器(GeneralPurposeAllocator)

GeneralPurposeAllocator 是有许多内存安全保障措施的分配器。

test_GeneralPurposeAllocator.zig

const std=@import("std");
test "GeneralPurposeAllocator" {
    var gpa=std.heap.GeneralPurposeAllocator(.{}){};
    const al=gpa.allocator();
    defer if (gpa.deinit()) @panic("memory leak");
    const a=try al.alloc(u8,8);
    defer al.free(a);
}

deinit函数返回为true时,表示有内存泄漏。

14.1.2. 固定缓冲区分配器(FixedBufferAllocator)

FixedBufferAllocator 不涉及任何堆分配,是把内存某片固定区域用做内存分配。

test_FixedBufferAllocator.zig

const std=@import("std");
test "FixedBufferAllocator" {
    const le:usize=16;
    var buf: [le]u8 align(8)=undefined;
    var fba=std.heap.FixedBufferAllocator.init(buf[0..]);
    const al=fba.allocator();
    const a=try al.alloc(u8,8);
    defer al.free(a);
}

FixedBufferAllocator用完后,不需要deinit函数。

缓冲区根据分配用途,预估对齐值。

14.1.3. arena分配器(ArenaAllocator)

ArenaAllocator初始化参数,需要把另一种分配器做为该分配器的基层分配器。

用ArenaAllocator来分配内存,可以多次分配不用每次释放,待调用deinit函数时全部释放。

test_ArenaAllocator.zig

const std=@import("std");
test "ArenaAllocator" {
    var arena=std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const al=arena.allocator();
    const a1=try al.alloc(u8,8);
    const a2=try al.alloc(u32,18);
    const a3=try al.alloc(f64,12);
    _=a1; _=a2; _=a3;
}

14.1.4. 页面分配器(page_allocator)

page_allocator 每次内存分配均要进行系统调用(可能是mmap),实际上是以页面为单位分配。

page_allocator 是最基础的分配器,如不能深刻理解其用途,尽量不要用这个。

page_allocator 不用调用init函数,也不用调用deinit函数,也不用调用allocator函数,可直接使用。

test_page_allocator.zig

const std=@import("std");
test "page_allocator" {
    var al=std.heap.page_allocator;
    const a1=try al.alloc(u8,8);
    al.free(a1);
    const a2=try al.alloc(u32,18);
    al.free(a2);
}

14.1.5. libc分配器(c_allocator)

c_allocator 是libc库的分配器,相当于直接调用C语言库中的malloc函数来分配内存。用这个分配器须链接libc库。

c_allocator 不用调用init函数,也不用调用deinit函数,也不用调用allocator函数,可直接使用。

test_c_allocator.zig

const std=@import("std");
test "c_allocator" {
    var al=std.heap.c_allocator;
    const a1=try al.alloc(u8,8);
    al.free(a1);
    const a2=try al.alloc(u32,18);
    al.free(a2);
}

$ zig test test_c_allocator.zig -lc

14.1.6. 测试用分配器(testing.allocator)

测试用分配器只能在测试声明语句块内使用。

测试用分配器不用调用init函数,也不用调用deinit函数,也不用调用allocator函数,可直接使用。

const std=@import("std");

test "c_allocator" {

var al=std.testing.allocator;

const a1=try al.alloc(u8,8);

al.free(a1);

const a2=try al.alloc(u32,18);

al.free(a2);

}

如果需要正确处理OutOfMemory错误,可以用std.testing.FailingAllocator。

14.1.7. 分配器选择(choosing an allocator)

分配器的选用取决于许多因素。建议的选择流程如下:

A. 是否生成库?是的话,最好设1个Allocator参数,由库的使用者确定分配器。

B. 是否链接libc?是的话,应该选择std.heap.c_allocator。

C. 需要的最大字节数可由编译期可知的数字限定?是的话,用std.heap.FixedBufferAllocator或std.heap.ThreadSafeFixedBufferAllocator,第2个分配器是线程安全的。

D. 是否程序退出时一次释放所有内存?是的话,用std.heap.ArenaAllocator。

E. 内存分配是周期性模式(如视频游戏主循环或web服务请求)?如果在周期末尾可以一次释放所分配的内存(例如某次web服务请求已完成),则可以选择std.heap.ArenaAllocator。如果可确定内存使用上限,可以用std.heap.FixedBufferAllocator。

F. 是否在写测试,并希望正确处理error.OutOfMemory?是的话,用std.testing.FailingAllocator。

G. 是否在写测试?是的话,用std.testing.allocator。

H. 如果上面的场景都不适用,那就需要通用分配器。可在主函数中设置一个std.heap.GeneralPurposeAllocator,然后将其或子分配器传递到各个部分。

I. 还可以实现1个分配器。

可通过Allocator接口实现分配器。必须仔细阅读 std/mem.zig中的注释,提供allocFn和resizeFn。

有许多分配器例子可参考,如:std/heap.zig和std.heap.GeneralPurposAllocator。

14.2. 内存分配(memory alloc)

分配器上常用的内存分配函数如下,如果返回的是错误联合类型,调用时记得用 try :

test_memory_alloc.zig

const expect=@import("std").testing.expect;
var al=@import("std").testing.allocator;
test "alloc free" {
    var s=try al.alloc(i32,15);
    defer al.free(s);
    try expect(@TypeOf(s)==[]i32);
    try expect(s.len==15);
    s[3]=10;
    try expect(s[3]==10);
}
test "realloc free" {
    var s=try al.alloc(i32,4);
    defer al.free(s);
    s=try al.realloc(s,16);
    try expect(s.len==16);
}
test "create destroy" {
    var i=try al.create(i32);
    try expect(@TypeOf(i)==*i32);
    defer al.destroy(i);
    i.*=15;

}
test "dupe" {
    const a=[4]i32{11,22,33,44};
    var s=try al.dupe(i32,&a);
    defer al.free(s);
    try expect(@ptrToInt(s.ptr)!=@ptrToInt(&a));
    try expect(s.len==4);
    try expect(s[2]==a[2]);
}
test "dupeZ" {
    const a="hello";
    var s=try al.dupeZ(u8,a);
    defer al.free(s);
    try expect(@ptrToInt(s.ptr)!=@ptrToInt(&a));    
    try expect(s.len==5);
    try expect(s[1]=='e');

}

14.3. 堆分配失败(heap allocation failure)

许多编程语言用无条件崩溃来处理堆分配失败。Zig语言中,当堆分配失败时,Zig库返回error.OutOfMemory。

由于某些操作系统默认启用了内存overcommit,有些人认为处理堆分配失败毫无意义。这种想法存在许多问题:

只有一些操作系统具有overcommit。Linux默认启用且可配置;Windows不会overcommit;嵌入式系统不会overcommit;Hobby操作系统可能有也可能没有。

实时系统没有overcommit,且应用程序的最大内存量是提前确定的。

写库的主要目标之一是代码重用,正确处理分配失败,库就可能多的被重用。

overcommit是许多用户体验极差的原因。当启用overcommit(如Linux)接近内存耗尽时,OOM会不确定的杀掉某些进程,这就经常导致某个重要的进程被终止,并且无法使系统恢复到工作状态。

14.4. 内存相关内置函数(memory builtin function)

14.4.1. @memcpy

@memcpy(noalias dest:[*]u8,noalias source:[*]const u8,byte_count:usize)

从内存的一个区域复制字节到另一个。dest 和 source都是指针,且不能区域重叠。

这个函数是个低级别的内在函数,无安全机制。大多数代码不应该使用此函数,而应该使用类似下列语句:

for (source[0..byte_count]) |b, i| dest[i] = b;

优化器足够智能,可以将上面的代码段转换为memcpy。

还有一个类似的标准库函数:

const mem = @import("std").mem;

mem.copy(u8, dest[0..byte_count], source[0..byte_count]);

14.4.2. @memset

@memset(dest: [*]u8, c: u8, byte_count: usize)

用c 填充 dest 指向的内存区。

这个函数是个低级别的内在函数,无安全机制。大多数代码不应该使用此函数,而应该使用类似下列语句:

for (dest[0..byte_count]) |*b| b.* = c;

优化器足够智能,可以将上面的代码段转换为memcpy。

还有一个类似的标准库函数:

const mem = @import("std").mem;

mem.set(u8, dest, c);

14.4.3. @prefetch

@prefetch(ptr:anytype,comptime options:std.builtin.PrefetchOptions)

如果目标平台CPU支持,则告知编译器发出预取指令。不支持,则无操作。

此函数对程序的运行逻辑没有影响,只对性能有影响。

ptr 参数可以是任何指针类型,确定了要预取的内存地址。此函数不会对指针解引用,将指向无效内存的指针传给此函数是完全合法的,不会导致任何未定义行为。

options参数是一个struct,Zig语言代码生成的这个struct必须和编译器实现保持同步。

rw属性:明确是为了读还是为了写预取。

locality属性:为0,表示没有临时用处,数据可以在访问缓存后立即从缓存删除;为3,表示有高的时间局部性,数据应一直保存在缓存中,因为可能很快再次被访问。

cache属性:要执行预取缓存

builtin.zig

pub const PrefetchOptions = struct {
    rw: Rw = .read,
    locality: u2 = 3,
    cache: Cache = .data,
    pub const Rw = enum {
        read,
        write,
    };
    pub const Cache = enum {
        instruction,
        data,
    };
};

14.4.4. @wasmMemorySize

@wasmMemorySize(index:u32) u32

返回由index标识的Wasm内存大小,单位为Wasm页,每个Wasm页的大小为64KB。

这个函数是个低级别的内在函数,没有安全机制,通常对以Wasm为目标的分配器设计者有用。因此,除非是从头开始编写新的分配器,否则用@import("std").heap.WasmPageAllocator之类操作。

14.4.5. @wasmMemoryGrow

@wasmMemoryGrow(index:u32,delta:u32) i32

以无符号的Wasm页面数为单位,按 delta 递增 index 标识的Wasm内存大小,每个Wasm页的大小为64KB。

成功时,返回以前的内存大小;失败时,如果分配失败,则返回-1。

这个函数是个低级别的内在函数,没有安全机制,通常对以Wasm为目标的分配器设计者有用。因此,除非是从头开始编写新的分配器,否则用@import("std").heap.WasmPageAllocator之类操作。

test_wasmMemoryGrow.zig

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;
    var prev = @wasmMemorySize(0);
    try expect(prev == @wasmMemoryGrow(0, 1));
    try expect(prev + 1 == @wasmMemorySize(0));
}

15. 文件读写(file I/O)

std.fs下的Dir和File模块负责文件读写。

15.1. 目录操作(directory operator)

目录的主要操作如下,使用时,注意用 try 或 catch :

遍历捕获的Entry结构为:

const Entry={name:const []u8, kind:File.Kind};

示例

run_dir.zig

const print=@import("std").debug.print;
const std=@import("std");
pub fn main() !void{
    var home=std.fs.cwd();
    defer home.close();
    try home.makeDir("testdir");
    var dir=try home.openDir("testdir",.{});
    try dir.makeDir("d1");
    try dir.makeDir("d2");
    _=try dir.createFile("f1",.{});
    dir.close();
    var idir=try home.openIterableDir("testdir",.{});
    var iter=idir.iterate();
    while(try iter.next()) |e| {
        print("Entry: name={s} kind={any}\n",.{e.name,e.kind});
    }
    idir.close();
    try home.deleteTree("testdir");
}
shell

$ zig run run_dir.zig
Entry: name=d1 kind=fs.file.File.Kind.Directory
Entry: name=d2 kind=fs.file.File.Kind.Directory
Entry: name=f1 kind=fs.file.File.Kind.File

15.2. 文件操作(file operator)

文件的主要操作如下,使用时,注意用 try 或 catch :

run_file.zig

const print=@import("std").debug.print;
const std=@import("std");
pub fn main() !void{
    var gpa=std.heap.GeneralPurposeAllocator(.{}){};
    const al=gpa.allocator();
    defer if (gpa.deinit()) @panic("memory leak");
    var home=std.fs.cwd();
    defer home.close();
    try home.makeDir("testdir");
    var dir=try home.openDir("testdir",.{});
    var file=try dir.createFile("testfile",.{});
    file.close();
    file=try dir.openFile("testfile",.{.mode=.read_write});
    _=try file.writeAll("hello        world");
    try file.seekTo(6);
    _=try file.writeAll("wang");
    var buf:[100]u8=undefined;
    try file.seekTo(0);
    const len=try file.readAll(&buf);
    const s=buf[0..len];
    print("{s}\n",.{s});
    try file.seekTo(0);
    const buf1=try file.readToEndAlloc(al,1024);
    defer al.free(buf1);
    print("{} {s}\n",.{buf1.len,buf1});
    file.close();
    try dir.deleteFile("testfile");
    dir.close();
    try home.deleteTree("testdir");
}
shell

$ zig run run_file.zig
hello wang   world
18 hello wang   world

15.2.1. @embedFile

@embedFile(comptime path:[]const u8) *const [N:0]u8

返回编译期常量指针,指向以null结尾固定长度的数组,长度等于path对应文件的字节数,数组内容是文件内容。

相当于是包含文件内容的字符串字面值。

path是当前文件的绝对或相对路径,类似于@import。

示例:

在当前目录上,建立文件,文件名:embed.txt 文件内容:hello

run_embedFile.zig

const print=@import("std").debug.print;
pub fn main() !void{
    const rbuf=@embedFile("embed.txt");
    print("{s}\n",.{rbuf});
}

hello

15.3. 其它操作(other operator)

这里只介绍改名和取文件信息操作如下,其它操作详看标准库的相关文档。

Stat 结构 主要属性有 .size .atime .mtime .ctime

run_dir_other.zig

const print=@import("std").debug.print;
const std=@import("std");
pub fn main() !void{
    var home=std.fs.cwd();
    defer home.close();
    try home.makeDir("testdir");
    var dir=try home.openDir("testdir",.{});
    var s=try dir.stat();
    print("testdir Stat:{any}\n",.{s});
    _=try dir.createFile("file11",.{});
    try dir.rename("file11","file2");
    s=try dir.statFile("file2");
    print("file2 Stat:{any}\n",.{s});
    dir.close();
    try home.deleteTree("testdir");
}
shell

$ zig run run_dir_other.zig
testdir Stat:fs.file.File.Stat{ .inode = 12103423999086002, .size = 0, .mode = 0, .kind = fs.file.File.Kind.Directory, .atime = 1667742517815319100, .mtime = 1667742517815319100, .ctime = 1667742517815319100 }
file2 Stat:fs.file.File.Stat{ .inode = 7036874418294610, .size = 0, .mode = 0, .kind = fs.file.File.Kind.File, .atime = 1667742517815319100, .mtime = 1667742517815319100, .ctime = 1667742517815319100 }

16. 格式化输出(format print)

16.1. 标准输入输出(stdin and stdout)

在C语言标准库和unix类操作系统中:

本例在屏幕上键入名字,并输出显示

run_stdio.zig

const std=@import("std");
fn getline(r:anytype,buf:[]u8) !?[]const u8 {
    var line=(try r.readUntilDelimiterOrEof(
    buf,'\n')) orelse return null;
    if (@import("builtin").os.tag==.windows){
        return std.mem.trimRight(u8,line,"\r");
    }else{
        return line;
    }
}
pub fn main() !void{
    const stdout=std.io.getStdOut();
    const stdin=std.io.getStdIn();
    try stdout.writeAll("Enter your name: ");
    var buf:[100]u8=undefined;
    const input=(try getline(stdin.reader(),&buf)).?;
    try stdout.writer().print(
    "Your name is: {s}\n",.{input});
}
shell

$ zig run run_stdio.zig
Enter your name: wang
Your name is: wang

16.2. 格式化输入(format input)

std库中提供了从字符串中获得整数和浮点数的函数,如下:

radix的参数含义为:10是分析十进制数字符串,0是分析0b 0o 0x 开头的其它进制字符串。

test_fmt_input.zig

const expect=@import("std").testing.expect;
const std=@import("std");
test "parseFloat and parseInt" {
	const i=try std.fmt.parseFloat(f32,"13.5");
	try expect(i==13.5);
	const j=try std.fmt.parseInt(i64,"0x13AC",0);
	try expect(j==0x13AC);
	const k=try std.fmt.parseInt(i32,"-123",10);
	try expect(k==-123);
}

16.2. 格式化输出(format output)

在格式串中,用{}表示需要格式化输出参数,格式串形式是:

{[参数序号][输出类型]:[填充字符][对齐方式][总宽度].[小数位数]}

其中,

输出类型是:

格式字符 输出类型
d 十进制(整数默认值)
b 二进制
o 八进制
x X 十六进制,x是输出小写字母,X是输出大小字母
e 科学计数法浮点数
c 把整数做为ASCII字符输出,整数最大8位长
u 把整数做为UTF-8序列输出,整数最大必须有21位
s 字符串,对应参数是0结尾的多项指针或u8类型c指针,或u8类型切片
* 值的地址
any 默认格式输出,参数可以是任何类型
? 解包裹后的值或null,和上列类型符组合
! 解包裹后的值或错误值,和上列类型符组合

要输出 { 在格式串中用 {{}}}


const print=@import("std").debug.print;
pub fn main() !void{
	const i:i16=0x1A2B;
	const j:u8=65;
	print("{1} {0b} {0o} {0x} {0X}\n",.{i,j});
	print("={:-<10}= ={0x:*>10}= ={0d:^10}=\n",.{j});
	const k:f32=1666.15345;
	print("={e:6.2}= ={0:7.3}= ={0d:7.3}= ={0d:8.2}=\n",.{k});
	const m:u21=0x8BED; // 语 UTF-8: 0xE8AFAD
	print("={1u}= ={0c}=\n",.{j,m});
	const str="hello world";
	const slice1=str[0..4];
	print("={s}= ={s}=\n",.{str,slice1});
	print("{1*} {2*} {0*}\n",.{&m,&str,&slice1});
	print("{any}",.{@TypeOf(str)});
	const p:?i32=15;
	const p1:?i32=null;
	print("{?x} {0?} {1?}\n",.{p,p1});
	const q:anyerror!i32=14;
	const q1:anyerror!i32=error.eone;
	print("{!x} {0!} {1!}\n",.{q,q1});
	print("{{ end }}\n",.{});
}
shell

$ zig run test_fmt.zig
65 1101000101011 15053 1a2b 1A2B
=65--------= =********41= =    65    =
=1.67e+03= =1.666e+03= =1666.153= = 1666.15=
=语= =A=
=hello world= =hell=
*const [11:0]u8@7ff7be2ec100 *const [4]u8@7ff7be2ec1a8 u21@7ff7be2ec240
*const [11:0]u8f 15 null
e 14 error.eone
{ end }

17. 异步函数(async function)

随着计算机速度越来越快,核心数越来越多,为了最大限度压榨硬件性能,近些年来编程语言在多进程、多线程、协程(可视做用户级多线程)的相关功能方面越来越丰富。

这就需要引入

17.1. 同步和异步函数(sync and async function)

17.1.1. 同步函数(sync function)

普通函数是不能在运行中途跳出,运行其它函数中语句后,再回来继续运行的。所以普通函数又称为同步函数。

例如下面的伪代码,在main调用fa时中途返回再中途进入,在绝大多数编程语言(可以说是所有)中是不能正常编译或运行的。


fn fa() void{
    x+=1;
    goto g11;
g12:
    x+=1;}
fn main() void {
    x=0;
    fa();
g11:    expect(x==1);
    goto g12;
    expect(x==2);}

17.1.2. 异步函数栈帧(async function frame)

函数运行中间可以挂起,条件满足时可以在挂起处继续恢复运行,这种函数称之为异步函数。

在普通函数调用期间,如果中途退出回来继续运行,则栈帧会乱掉,程序会跑飞。普通函数调用过程基本上流程是:

异步函数调用流程基本是:

17.2. 挂起(suspend)

在函数的内部用 suspend 关键字来修饰语句块,表示在此处挂起。

以异步方式调用函数时,须用 async 关键字修饰,返回类型是本函数栈帧,返回值是本函数的挂起栈帧值。

test_suspend.zig

const expect=@import("std").testing.expect;
var x: i32 = 1;
test "suspend with no resume" {
    var fr = async foo();
    try expect(x == 2);
    try expect(@TypeOf(fr)==@Frame(foo));
}
fn foo() void {
    x += 1;
    suspend {}
    x += 1;
}
shell

$ zig test test_suspend.zig -fstage1

本例中,异步调用foo函数,x加1变为2后,到suspend 挂起点,程序中途退出,异步调用的退出值是fr,其类型是 foo 函数栈帧。

所以此时x==2成立。

注意:foo函数栈帧是类型,因为可能在不同处调用foo函数,则foo函数的栈帧的具体值是不一定相同的。

另异步函数编译需要加 -fstage1 参数

名词解释:

进程调度(:process scheduling) 操作系统中进程通常有新建(:create)、就绪(:ready)、阻塞(:wait)、运行(:run)、终止(:exit)五个状态。从运行到就绪为挂起,从就绪到运行为恢复。协程相关方面概念与此类似。

17.3. 恢复(resume)

在异步函数内或调用异步函数的流程上,用 resume 关键字带一个栈帧类型的操作数,表示恢复到此栈帧对应的挂起点处,继续运行。

test_resume.zig

const expect=@import("std").testing.expect;
var r:i32=0;
test "resume" {
    var fr=async foo();
    try expect(r==1);
    r+=1;
    try expect(r==2);
    resume fr;
    try expect(r==12);
}
fn foo() void{
    r+=1;
    suspend{}
    r+=10;
}
shell

$ zig test test_suspend.zig -fstage1

本例中,异步调用foo函数,r加1,函数挂起,此时r值为1,返回挂起处栈帧值fr。

主流程r加1后,r值为2。

resume 的操作数是fr,表示从foo函数挂起处继续执行后正常返回,此时r值为12。

17.3.1. 从挂起块处恢复(resume from suspend block)

可以在suspend语句块中赋挂起栈帧指针,用resume恢复。

本例中,从挂起块中用@frame取得挂起处栈帧指针。

通过输出显示内容,可以直观的看到挂起和恢复的运行流程。

注意一点是挂起块不是在@frame函数调用处挂起的,因为语句块也是表达式,所以是在挂起块运行完后(即语句块表达式计算完后)才挂起的,可以从 ffff3 即可得知。

run_resume_block.zig

const print=@import("std").debug.print;
var r:i32=0;
var fr:anyframe=undefined;
pub fn main() void{
    _=async foo();
    print("ffff{}\n",.{r});
    r+=1;
    print("gggg{}\n",.{r});
    resume fr;
    print("hhhh{}\n",.{r});
}
fn foo() !void{
    print("aaaa{}\n",.{r});
    r+=1;
    suspend{
        r+=1;
        print("bbbb{}\n",.{r});
        comptime try expect(@TypeOf(@frame())==*@Frame(foo));
        fr=@frame();
        r+=1;
        print("cccc{}\n",.{r});
    }
    r+=10;
    print("dddd{}\n",.{r});
}
shell

$ zig run run_resume_block.zig -fstage1
aaaa0
bbbb2
cccc3
ffff3
gggg4
dddd14
hhhh14

17.3.2. 在挂起块内恢复(resume in suspend)

可以在挂起块内用resume恢复挂起。

test_resume_from_suspend.zig

const expect=@import("std").testing.expect;
test "resume from suspend" {
    var r:i32=1;
    _ = async foo(&r);
    try expect(r==2);
}
fn foo(r: *i32) void {
    suspend {
        resume @frame();
    }
    r.* +=1;
    suspend {}
    r.* +=1;
}
shell

$ zig test test_resume_from_suspend.zig -fstage1

17.4. nosuspend

如果想把内有 suspend 块的异步函数当普通函数使用,则调用时加 nosuspend 关键字。

test_nosuspend.zig

const print=@import("std").debug.print;
var i:i32=10;
pub fn main() void{
    const j=nosuspend foo(false);
    print("{} {}\n",.{i,j}); // 30 20
}
fn foo(su:bool) i32{
if(su){
    suspend {}
}
    i+=20;
    return 20;
}
shell

$ zig run run_nosuspend.zig -fstage1
30 20

此外,该异步函数须保证当普通函数使用时,运行流程不会进入挂起点。假如把本例中

const j=nosuspend foo(false);

改为

const j=nosuspend foo(true);

则程序运行崩溃,输出信息为:

thread 17440 panic: async function called in nosuspend scope suspended

17.5. await

通常异步函数由 async 启动,其 async 的返回值做为 await 表达式的操作数。

await 的操作数是 anyframe->T 类型,T是异步函数的返回类型。

await 表达式一直等到对应的 async 函数运行结束,将 async 函数的返回值做为表达式的值。

然后程序继续执行。

请仔细阅读本示例中的输出信息,借以理解 await 的运行流程。

run_await.zig

const print=@import("std").debug.print;
pub fn main() void{
    _=async amain();
    resume frfoo1;
    resume frfoo2;
}
fn amain() void{
    var fr1=async foo1();
    print("eeeeee\n",.{});
    var fr2=async foo2();
    print("ffffff\n",.{});
    const r1=await fr1;
    print("hhhhhh {}\n",.{r1});
    const r2=await fr2;
    print("iiiiii {}\n",.{r2});
}
var frfoo1:anyframe=undefined;
fn foo1() i32{
    print("aaaa\n",.{});
    suspend{frfoo1=@frame();}
    print("bbbb\n",.{});
    return 10;
}
var frfoo2:anyframe=undefined;
fn foo2() i32{
    print("cccc\n",.{});
    suspend{frfoo2=@frame();}
    print("dddd\n",.{});
    return 20;
}
zig run run_await.zig

aaaa
eeeeee
cccc
ffffff
bbbb
hhhhhh 10
dddd
iiiiii 20

将 run_await.zig中 的 main 函数改为如下,交换一下resume frfoo1 和 frfoo2的顺序,其它行不变。


pub fn main() void{
    _=async amain();
    resume frfoo2;
    resume frfoo1;
}

则程序输出变为:

zig run run_await.zig

aaaa
eeeeee
cccc
ffffff
dddd
bbbb
hhhhhh 10
iiiiii 20

因为await fr1表达式在前,所以尽管frfoo2先恢复,即先输出 dddd ,也要等到 frfoo1 运行完能够得到返回值后,程序才继续运行。

一般来说,大多数应用程序代码将只使用 async 和 await ,事件循环等偏底层应用将在内部使用 suspend 。

17.6 异步和栈帧相关内置函数(async and frame builtin function)

17.6.1. @frame

@frame() *@Frame(func)

返回指向给定函数栈帧的指针。其类型可强转为 anyframe->T 或 anyframe ,T是作用域中函数的返回类型。

这个函数不标记挂起点,但确实会使作用域内的函数变成异步函数。

17.6.2. @Frame

@Frame(func:anytype) type

返回 func 的栈帧类型,适用于异步函数和没有特定调用约定的函数。

此类型适合用作async的返回类型。例如,它允许堆分配一个async函数帧:

test_heap_allocated_frame.zig

const std = @import("std");
test "heap allocated frame" {
    const frame = try std.heap.page_allocator.create(@Frame(func));
    frame.* = async func();
}
fn func() void {
    suspend {}
}
shell

$ zig test test_heap_allocated_frame.zig -fstage1
1/1 test "heap allocated frame"... OK
All 1 tests passed.

17.6.3. @frameAddress

@frameAddress() usize

返回当前栈帧的基指针。

由目标平台确定,并不是所有目标平台都一致,由于优化,栈帧地址在release模式下可能不可用。

此函数仅在函数作用域内有效。

17.6.4. @frameSize

@frameSize(func:anytype) usize

等同于@sizeOf(@Frame(func)),fnuc可能是运行期可知。

此函数通常与@asyncCall结合使用。

17.6.5. @asyncCall

@asyncCall(frame_buffer:[]align(@alignOf(@Frame(anyAsyncFunction))) u8, result_ptr,function_ptr,args:anytype) anyframe->T

在一个不确定是异步函数的函数指针上,执行一个async调用。

frame_buffer须容纳整个函数帧,函数帧大小用@frameSize 确定。对太小的缓冲区,要调用安全检查未定义行为。

result_ptr是optional类型(可能是null)。如果非null,则把函数调用结果直接写到result_ptr,在await结束后可读取。await的结果定位是从result_ptr中复制结果。

async_struct_field_fn_pointer.zig

const std = @import("std");
const expect = std.testing.expect;
test "async fn pointer in a struct field" {
    var data: i32 = 1;
    const Foo = struct {
        bar: fn (*i32) callconv(.Async) void,
    };
    var foo = Foo{ .bar = func };
    var bytes: [64]u8 align(@alignOf(@Frame(func))) = undefined;
    const f = @asyncCall(&bytes, {}, foo.bar, .{&data});
    try expect(data == 2);
    resume f;
    try expect(data == 4);
}
fn func(y: *i32) void {
    defer y.* += 2;
    y.* += 1;
    suspend {}
}
Shell

$ zig test async_struct_field_fn_pointer.zig -fstage1
1/1 test "async fn pointer in a struct field"... OK
All 1 tests passed.

17.6.6. @returnAddress

@returnAddress() usize

返回当前函数返回时将执行的下一个机器代码指令地址。

这个结果是针对特定目标平台,并不是所有目标平台都一致。

此函数仅在函数范围内有效。如果函数内联到调用函数中,则返回的地址将应用于调用函数。

17.7. 原子操作(atomic)

多进程、多线程、协程间的数据同步,需要共享锁。

传统的共享锁实现复杂,运行代价重,适合于严格数据同步。

现代大部分桌面和服务器CPU,支持原子操作,可以实现轻量级的数据同步,俗称无锁数据结构。

因Zig语言这部分还在继续完善,此节仅罗列内置函数,其余暂时不写了。

17.7.1. 原子操作内置函数(atomic builtin function)

17.7.1.1. @atomicLoad

@atomicLoad(comptime T:type,ptr:*const T,comptime ordering:builtin.AtomicOrder) T

原子解引用并返回值。

T 必须是指针, bool, 浮点数,整数,enum。

17.7.1.2. @atomicRmw

@atomicRmw(comptime T:type,ptr:*T,comptime op:builtin.AtomicRmwOp,operand:T,comptime ordering:builtin.AtomicOrder) T

原子修改内存,返回旧值。

T 必须是指针, bool, 浮点数,整数,enum。

支持的运算:

.Xchg 保存未修改的operand。支持enum,整数,浮点数。

.Add 对整数,是回绕加法,也支持浮点数。

.Sub 对整数,是回绕减法,也支持浮点数。

.And 比特与

.Nand 比特非

.Or 比特或

.Xor 比特异或

.Max 如果operand大,则保存它。支持整数和浮点数。

.Min 如果operand小,则保存它。支持整数和浮点数。

17.7.1.3. @atomicStore

@atomicStore(comptime T:type,ptr:*T,value:T,comptime ordering:builtin.AtomicOrder) void

原子保存值。

17.7.1.4. @cmpxchgStrong

@cmpxchgStrong(comptime T:type,ptr:*T,expected_value:T,new_value:T,success_order:AtomicOrder,fail_order:AtomicOrder) ?T

运行强原子比较交换操作,运行逻辑相当于下面没有原子操作的代码:

cmpxchgStrongButNotAtomic.zig

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, 浮点数,整数,enum。

@typeInfo(@TypeOf(ptr)).Pointer.alignment must be >= @sizeOf(T).

17.7.1.5. @cmpxchgWeak

@cmpxchgWeak(comptime T:type, ptr:*T,expected_value:T,new_value:T,success_order:AtomicOrder,fail_order:AtomicOrder) ?T

运行弱引用原子比较交换操作,运行逻辑相当于下面没有原子操作的代码:

cmpxchgWeakButNotAtomic

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, 浮点数,整数,enum。

@typeInfo(@TypeOf(ptr)).Pointer.alignment must be >= @sizeOf(T).

17.7.1.6. @fence

@fence(order:AtomicOrder)

用于在两个操作的边界使用happens-before内存屏障规则。

AtomicOrder参见 @import("std").builtin.AtomicOrder

名词解释:

内存屏障(:memory barrier) 多核CPU中多个核同时读写内存时,为了数据和运行逻辑的正确,需要内存屏障。

happens-before: 如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但保证a操作将对b操作可见。

18. Zig构建系统(Zig build system)

18.1. 静态语言构建流程(static language build process)

静态语言的程序构建流程基本是:

对于有宏功能的程序设计语言,需要先在预处理器把宏文本替换,再开始正式编译。比如gcc可以在宏文本替换后生成 .i 文本文件。

编译过程通常以中间代码为界,又可分为前端和后端。前端是指源代码文件经过词法、语法、语义等分析,生成中间代码格式的文本或二进制格式文件,例如在LLVM中文件名后缀是 .ll 。

后端是指中间代码文件经过代码优化和链接优化,最终生成指定平台(通常是当前平台)的可执行程序或库。

18.2. 构建模式(build mode)

共有4种构建模式: Debug (默认值),用于调试; ReleaseFast 目的是运行速度更快;ReleaseSafe 目的是更安全;ReleaseSmall 目的是生成二进制程序更小。

使用时在命令行输入与下面类似的命令,在 -O 选项后面输入构建模式:

$ zig build-exe exam.zig -O ReleaseFast

4种构建模式比较:

编译模式 编译速度 安全检查 生成程序大小 生成程序运行速度
Debug
ReleaseFast
ReleaseSafe 适中
ReleaseSmall 适中

用 zig build --help来查看构建系统命令行选项帮助。

18.3. 构建可执行程序(building an executable)

$ zig init-exe

自动生成build.zig文件

文件中standardTargetOptions是构建目标平台选项,默认值是本机;

standardReleaseOptions是构建模式选项。

build.zig

const std = @import("std");

pub fn build(b: *std.build.Builder) void {
    // Standard target options allows the person running `zig build` to choose
    // what target to build for. Here we do not override the defaults, which
    // means any target is allowed, and the default is native. Other options
    // for restricting supported target set are available.
    const target = b.standardTargetOptions(.{});

    // Standard release options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
    const mode = b.standardReleaseOptions();

    const exe = b.addExecutable("zig", "src/main.zig");
    exe.setTarget(target);
    exe.setBuildMode(mode);
    exe.install();

    const run_cmd = exe.run();
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    const exe_tests = b.addTest("src/main.zig");
    exe_tests.setTarget(target);
    exe_tests.setBuildMode(mode);

    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&exe_tests.step);
}

18.4. 构建库(building a library)

$ zig init-lib

自动生成build.zig文件

build.zig

const std = @import("std");

pub fn build(b: *std.build.Builder) void {
    // Standard release options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
    const mode = b.standardReleaseOptions();

    const lib = b.addStaticLibrary("zig", "src/main.zig");
    lib.setBuildMode(mode);
    lib.install();

    const main_tests = b.addTest("src/main.zig");
    main_tests.setBuildMode(mode);

    const test_step = b.step("test", "Run library tests");
    test_step.dependOn(&main_tests.step);
}

18.4.1. @export

@export(declaration, comptime options: std.builtin.ExportOptions) void

declaration 必须是:函数或变量的标识符 (x) ;函数或变量的属性标识符 (x.y)

从 copmtime 块中调用 @export ,以有条件的导出符号。

当 declaration 是 C语言调用约定的函数,且 options.linkage 是 strong ,相当于在函数前用 export 关键字:

export1.zig

comptime {
    @export(internalName, .{ .name = "foo", .linkage = .Strong });
}
fn internalName() callconv(.C) void {}

即export1.zig中代码等同于:

export fn foo() void {}

也可使用标识符语法 @"foo" 语法,为符号名称选择任何字符串:

export fn @"A function name that is a complete sentence."() void {}

上一行源代码生成的目标文件中,可以想看到符号被原封不动使用:

00000000000001f0 T A function name that is a complete sentence.

18.4.2. @extern

@extern(T: type, comptime options: std.builtin.ExternOptions) *T

在输出目标文件中创建对外部符号的引用。

18.4.3. @import

@import(comptime path:[]u8) type

查找与path对应的zig源代码文件,如果还未添加则将其添加到构建中。

@import返回与文件对应的结构类型,其名称等于不包含扩展名的文件名称。

带pub关键字的定义可以从不同于其定义的源文件中引用。

path可以是相对路径,也可以是包的名称。如果是相对路径,则它相对于包含@import函数调用的文件。

18.5. 编译C源代码(compiling c source code)

在build.zig文件中,增加类似于本例的源代码。


lib.addCSourceFile("src/lib.c", &[_][]const u8{
    "-Wall",
    "-Wextra",
    "-Werror",
});

18.6. 单线程构建(single threaded build)

编译选项可输入 --single-threaded,单线程构建是指:

所有线程本地变量当做静态变量处理;

异步函数的开销等同于普通函数调用开销;

@import("builtin").single_threaded 为 true,因此读这个变量的各种用户态API更高效。例如std.Mutex变成1个空数据结构,所有相关函数是空操作。

18.7. 目标平台(targets)

Zig能在LLVM支持的所有目标平台上生成二进制代码。

$ zig targets

命令输出显示支持的目标平台。

Zig标准库(@import("std"))抽象了体系结构、环境和操作系统,所以需要做更多的工作来支持更多平台。

并非所有标准库代码需要操作系统抽象,所以泛型等数据结构可在上述所有目标平台工作。

Zig标准库支持的当前目标平台为:Linux x86_64, Windows x86_64, macOS x86_64

名词解释:

目标平台三元组(:target triple) 包括CPU架构、供应商、操作系统和ABI信息。例如:x86_64-windows-gnu 表示目标平台是64位x86 CPU,windows 操作系统,glibc 库 ABI 接口。

18.8. 编译期变量(compile variables)

可导入 builtin 包访问编译期变量,其中包含了编译期常量,如当前平台、大小端模式等。

19. C语言相关(C)

虽然Zig独立于C语言,且与大多数语言不同,不依赖于libc,但Zig语言与现有C代码交互还是很重要的。

19.1. C基本类型(c type primitives)

2.1.5. C语言ABI兼容类型(c ABI compatible)保证与C语言ABI兼容,可以像其它类型一样在Zig源代码中使用。

与C语言void类型互操作,用 anyopaque 。

19.2. 导入C头文件(import from c header file)

@cImport函数可直接从 .h 文件中导入符号。

run_importc.zig

const c = @cImport({
    // See https://github.com/ziglang/zig/issues/515
    @cDefine("_NO_CRT_STDIO_INLINE", "1");
    @cInclude("stdio.h");
});
pub fn main() void {
    _ = c.printf("hello\n");
}
shell

$ zig build-exe run_importc.zig -lc
$ run_importc
hello

@cImport函数将编译期求值的表达式作为参数,用于控制预处理器指令或包括多个 .h文件:

@cImport Expression

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");
});

19.3. C代码转译Zig代码(c translation CLI)

$ zig translate-c

把C语言源代码转译为Zig语言源代码。转译后的文件写入stdout。

19.3.1. 命令行选项(command line flags)

-I 指定头文件的搜索目录,可多次使用。等同于clang的[-I选项](https://releases.llvm.org/12.0.0/tools/clang/docs/ClangCommandLineReference.html#cmdoption-clang-i-dir),默认不包括当前目录,用 -I. 来包括当前目录。

-D 定义预处理宏。等同于clang的[-D选项](https://releases.llvm.org/12.0.0/tools/clang/docs/ClangCommandLineReference.html#cmdoption-clang-d-macro)。

-cflags [flags] --: 把附加的[命令行选项](https://releases.llvm.org/12.0.0/tools/clang/docs/ClangCommandLineReference.html)传给clang。

-target: 转译成Zig代码的平台三元组信息,如未指定平台,则用当前主机平台信息。

19.3.2. -target和-cflags(using -target and -cflags)

用zig translate-c转译C语言代码时,必须和转译后的Zig代码的 -target 三元组相同。还必须确保使用的 -cflags 与目标平台上代码使用的 cflags相匹配。

用不正确的-target或-cflags可能会转译失败,或者在与C语言代码链接时会有ABI不兼容。

varytarget.h

long FOO = __LONG_MAX__;
Shell

$ 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;

varycflags.h

enum FOO { BAR };
int do_something(enum FOO foo);
Shell

$ 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;

19.3.3. @cImport和translate-c(@cImport and translate-c)

@cImport和zig translate-c使用相同的C语言转译方法,所以在技术层面上它们是等价的。

@cImport可以快速方便地访问数值常量、类型定义,不需要任何多余设定来记录类型,非常有用。

如果需要将cflags传递给clang,或者想编辑转译后的代码,建议使用zig translate-c,并将结果保存到文件中。

编辑转译后代码的常见原因有:将宏函数中的anytype类型参数更改为更具体的类型;将指针类型由 [*c]T 改为 [*]T 或 *T ,以提高类型安全性;在指定函数内用@setRuntimeSafety启用或禁用运行时安全。

19.4. C转译缓存(c translation caching)

C语言转译功能集成了Zig缓存系统,用相同源代码文件、cflags和目标平台的再次转译,将用缓存而不是重新转译。

使用 --verbose cImport 选项,可查看编译 @cImport 用到的缓存文件存储位置。

verbose.zig

const c = @cImport({
    @cDefine("_NO_CRT_STDIO_INLINE", "1");
    @cInclude("stdio.h");
});
pub fn main() void {
    _ = c;
}
shell

$ zig build-exe verbose.zig -lc --verbose-cimport
info(compilation): C import source: zig-cache\o\303680b98edc23012687f366ec52c995\cimport.h
info(compilation): C import .d file: zig-cache\o\303680b98edc23012687f366ec52c995\cimport.h.d
info(compilation): C import output: zig-cache\o\36a5287201d0d08c8a3814158ddb246e\cimport.zig

cimport.h包含要转译的文件(用@cInclude, @cDefine, @cUndef构造), cimport.h.d 是文件依赖项列表,cimport.zig是转译输出的Zig代码。

19.4.1. @cDefine

@cDefine(comptime name:[]u8,value)

仅在 @cImport内使用。

在 @cImport临时缓冲区追加:

#define $name $value

默认是不用value,类似于:

#define _GNU_SOURCE

用void,类似于:

@cDefine("_GNU_SOURCE",{})

19.4.2. @cImport

@cImport(expression) type

解析C语言代码,并将函数、类型、变量和兼容宏定义,导入到1个新的空struct type中,并返回该type。

expression在编译时计算。内置函数 @cInclude, @cDefine, @cUndef在这个expression内运行,并追加输出到临时缓冲区。

通常在整个应用程序中应该只有一个@cImport,因为可以避免编译器多次调用clang,并防止重复内联函数。

有多个@cImport的理由为:

为了避免符号冲突,例如,如果 foo.h 和 bar.h 都

#define CONNECTION_COUNT

用不同的预处理器定义解析C语言源代码

19.4.3. @cInclude

@cInclude(comptime path: []u8)

仅在 @cImport内使用。

在 c_import 临时缓冲区追加:

#include <$path>\n

19.4.4. @cUndef

@cUndef(comptime name: []u8)

仅在 @cImport内使用。

在 c_import 临时缓冲区追加:

#undef $name

19.5. 转译失败(translation failures)

有些C语言构造无法转译为Zig代码,如goto, 位域, 粘贴宏。Zig用降级,来继续翻译。

降级有三种类型:-opaque, extern 和 @compileError。

不能正确转换的C struct 和union 被转译为 opaque{}。

包含不能转译的代码构造和 opaque 类型的函数,被降级为extern 定义。

因此,不可转译类型仍然可以用作指针,只要链接器清楚已编译的函数,就可以调用不可转译函数。

当顶级定义(全局变量,函数原型,宏)无法降级时,使用@compileError。

由于Zig对顶级定义使用延迟解析,所以不可转译的实体不会在代码中导致编译错误,除非实际使用它们。

19.6. C语言宏(c macros)

无法转译的宏被降级为@compileError。使用宏的C语言代码可以正常转译,只有宏本身无法转译为Zig语言。

下例中,尽管无法转译宏,但代码还是被正确转译。因为MAKELOCAL宏不能转译为Zig语言函数,所以被降级为 @compileError。

macro.c

#define MAKELOCAL(NAME, INIT) int NAME = INIT
int foo(void) {
   MAKELOCAL(a, 1);
   MAKELOCAL(b, 2);
   return a + b;
}
shell

$ zig translate-c macro.c>macro.zig
macro.zig

pub export fn foo() c_int {
    var a: c_int = 1;
    var b: c_int = 2;
    return a + b;
}
pub const MAKELOCAL = @compileError("unable to translate C expr: unexpected token '='"); // macro.c:1:9

19.7. C指针(c pointers)

仅在转译自动生成的代码中使用C指针,除此之外尽量避免使用。

导入C头文件时,C指针往往不能明确转换为单项指针(*T)还是多项指针([*]T)。所以C指针是一种折衷方案,Zig代码可以用转译过的头文件。

[*c]T C指针。

C指针指向单个struct(而不是array)时,解引用来访问结构属性的语法为:

ptr_to_struct.*.struct_member

这与在C语言中 -> 运算符类似。

当C指针指向结构数组时,语法将恢复为:

ptr_to_struct_arry[index].struct_member

19.8. 导出C库(exporting a c library)

Zig语言的一个主要用途就是导出C语言ABI接口的库,供其它编程语言调用。

库API接口由带export关键字前缀的函数、变量和类型组成。

构建静态库命令行:

$ zig build-lib mathtest.zig

构建动态库命令行:

$ zig build-lib mathtest.zig -dynamic

下面用同一个目录下mathtest.zig, test.c, build.zig这3个文件,演示导出C库的方法。

mathtest.zig

export fn add(a: i32, b: i32) i32 {
    return a + b;
}
test.c

// This header is generated by zig from mathtest.zig
#include "mathtest.h"
#include <stdio.h>
int main(int argc, char **argv) {
    int result = add(42, 1337);
    printf("%d\n", result);
    return 0;
}
build.zig

const Builder = @import("std").build.Builder;
pub fn build(b: *Builder) void {
    const lib = b.addSharedLibrary("mathtest", "mathtest.zig", b.version(1, 0, 0));
    const exe = b.addExecutable("test", null);
    exe.addCSourceFile("test.c", &[_][]const u8{"-std=c99"});
    exe.linkLibrary(lib);
    exe.linkSystemLibrary("c");
    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);
}
shell

$ zig build test
1379

实际不行,fatal error: 'mathtest.h' file not found

必须手工添加:

mathtest.h

extern int add(int a,int b);

才可以。

可将Zig目标文件与其它遵循C语言ABI接口的目标文件混合,例如:

base64.zig

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;
}
test.c

// 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;
}
build.zig

const Builder = @import("std").build.Builder;
pub fn build(b: *Builder) void {
    const obj = b.addObject("base64", "base64.zig");
    const exe = b.addExecutable("test", null);
    exe.addCSourceFile("test.c", &[_][]const u8{"-std=c99"});
    exe.addObject(obj);
    exe.linkSystemLibrary("c");
    exe.install();
}
shell

$ zig build
$ ./zig-out/bin/test
all your base are belong to us

###头文件不能自动生成。。

19.9. 内嵌汇编(assembly)

汇编语言与具体的CPU密切相关,除了裸机开发,绝大部分场景极少用到。如有需要,可参看:

Zig语言文档assembly部分

gcc扩展文档assembly部分

20. 测试(test)

1个文件中可以写1个或多个测试定义。

zig test命令构建并运行源代码中的测试定义,测试功能的源代码在lib/test_runner.zig中。

expect函数的参数如果为 false 则返回错误,为 true 则继续执行,测试结果输出到标准输出(stderr)。

test_introducing_zig_test.zig

const expect = @import("std").testing.expect;
test "expect addOne adds one to 41" {
    try expect(addOne(41) == 42);
}
fn addOne(number: i32) i32 {
    return number + 1;
}
shell

$ zig test test_introducing_zig_test.zig
1/1 test.expect addOne adds one to 41... OK
All 1 tests passed.

输出的 1/1 中的第一个1表示的是第1个测试,第二个1表示总共有1个测试。如果某条测试定义OK,则会立即清除该行显示。最终显示测试通过总数。

TODO 是否加个选项,测试通过显示后该行不清除。

20.1. 测试定义(test declaration)

测试定义形式为:test testname {block}

测试定义的返回类型是anyerror!void。

测试定义是文件作用域级变量,所以与顺序无关,可以写在被测代码之前或之后。

如果不用zig test命令构建,则测试定义在构建时被忽略,不包括在构建最终结果中(编译生成的obj文件或可执行文件)。

匿名测试只能用于运行其它测试,且不能被过滤。

20.2. 嵌套测试(nested container test)

zig测试构建时,只有已确定的测试定义被构建,除了文件作用域级的测试定义外,没有被引用的嵌套的测试定义不会被构建。

下例中refAllDecls函数表明执行文件中所有文件作用域级的测试定义,@this内置函数返回文件本身,_=S; 表示计算S的值,但丢弃计算结果。

test_testdecl_container_top_level.zig

const expect = @import("std").testing.expect;
test {
    std.testing.refAllDecls(@This());
    _ = S;
    _ = U;
    _ = @import("introducing_zig_test.zig");
}
const S = struct {
    test "S demo test" {
        try expect(true);
    }
    const SE = enum {
        V,
        test "This Test Won't Run" {
            try expect(false);
        }
    };
};
const U = union { 
    s: US,
    const US = struct {
        test "U.US demo test" {
            try expect(false);
        }
    };

    test "U demo test" {
        try expect(true);
    }
};
shell

$ zig test test_testdecl_container_top_level.zig
1/4 test_0... OK
2/4 test.S demo test... OK
3/4 test.U demo test... OK
4/4 test.expect addOne adds one to 41... OK
All 4 tests passed.

20.3. 测试失败(test failure)

当测试返回错误时,其错误返回追踪(error return trace)输出到标准错误,并输出失败总个数。

test_testfail.zig

const expect = @import("std").testing.expect;
test "expect this to fail" {
    try expect(false);
}
test "expect this to succeed" {
    try expect(true);
}
Shell

$ zig test test_testfail.zig
Test [1/2] test.expect this to fail... FAIL (TestUnexpectedResult)
D:\zig\lib\std\testing.zig:351:14: 0x7ff62ed21020 in expect (test.exe.obj)
    if (!ok) return error.TestUnexpectedResult;
             ^
D:/ziglearn/test_testfail.zig:5:5: 0x7ff62ed21137 in test.expect this to fail (test.exe.obj)
    try expect(false);
    ^
Test [2/2] test.expect this to succeed... OK
1 passed; 0 skipped; 1 failed.
error: the following test command failed with exit code 1:
zig-cache\o\e310f8b27e356549cf3756ed70739ebb\test.exe D:\zig\zig.exe

20.4. 跳过测试(skip test)

跳过测试的一种方法是用命令行选项过滤,zig test --test-filter testname,使测试只包括 testname。匿名测试定义无法过滤。

另一种方法是测试定义返回error.SkipZigTest,则该测试被跳过,并输出被跳过的总数。

test_skip.zig

const expect = @import("std").testing.expect;
test "this will be skipped" {
    return error.SkipZigTest;
}
Shell

$ zig test test_skip.zig
1/1 test.this will be skipped... SKIP
0 passed; 1 skipped; 0 failed.

20.4.1. 跳过挂起点测试(skip test suspend point)

当测试在默认的I/O阻塞模式下运行时,会跳过内有挂起(suspend)点的测试定义。(可使用--test-evented-io命令行参数改为I/O事件模式)

本示例中,如果使用nosuspend关键字,在I/O阻塞模式下,该测试不会被跳过。(参见: ###Async和Await)

test_async_skip.zig

const expect = @import("std").testing.expect;
test "async skip test" {
    var frame = async func();
    const result = await frame;
    try std.testing.expect(result == 1);
}
fn func() i32 {
    suspend {
        resume @frame();
    }
    return 1;
}
Shell

$ zig test test_async_skip.zig -fstage1
1/1 test "async skip test"... SKIP (async test)
0 passed; 1 skipped; 0 failed.

20.4.2. 报告内存泄漏(report memory leak)

如果用std.testing.allocator来分配内存,则测试时会报告发现的内存泄漏。例如本示例中,定义了list数组后,测试结束后没有释放内存(deinit函数),发生内存泄漏,测试时输出此错误。

test_memory_leak1.zig

const expect = @import("std").testing.expect;
test "detect leak" {
    var list = std.ArrayList(u21).init(std.testing.allocator);
    // defer list.deinit();
    try list.append('t');
    try expect(list.items.len == 1);
}
shell

$ zig test test_memory_leak1.zig
Test [1/1] test.detect leak... [gpa] (err): memory address 0x21083840000 leaked:
All 1 tests passed.
1 errors were logged.
1 tests leaked memory.
error: the following test command failed with exit code 1:
zig-cache\o\f1f05799e0ade565f4b6efa5c900da23\test.exe C:\zig\zig.exe

把defer处的注释符去掉,则无内存泄漏,测试正常。

test_memory_leak2.zig

const expect = @import("std").testing.expect;
test "detect leak" {
    var list = std.ArrayList(u21).init(std.testing.allocator);
    defer list.deinit();
    try list.append('t');
    try expect(list.items.len == 1);
}
shell

$ zig test test_memory_leak2.zig
Test [1/1] test.detect leak...OK
All 1 tests passed.

20.5. 检测是否处于测试状态(detecting test build)

可用编译期变量@import("builtin").is_test检测是否处于测试构建状态。

istest.zig

const std = @import("std");
const expect = std.testing.expect;
const print = std.debug.print;
const t1=@import("builtin").is_test;
test "builtin.is_test" {
    print("\ntest status,is_test:{}\n",.{t1});
    try expect(t1);
}
pub fn main() void{
    print("run status,is_test:{}\n",.{t1});
}
shell

$zig test istest.zig
Test [1/1] test.builtin.is_test... OK
test status,is_test:true
All 1 tests passed.
$zig build-exe istest.zig
$istest
run status,is_test:false

20.6. 测试名字空间(the testing namespace)

除了expect函数外,测试名字空间里还有其它的函数用于测试,例如:

try std.testing.expectEqual(expected, actual);

expected是预期结果,actual是待测试表达式,actual的计算结果会强制变换为expected的类型,如果值相等则测试通过。

try std.testing.expectError(expected_error, actual_error);

与expectEqual类似,expected_error是预期错误,actual_error是待测表达式,如果actual_error的计算结果与expected_error相等则测试通过。

除此之外,测试名字空间还有比较切片、字符串及其它的一些函数,具体详见标准库的testing名字空间。

20.7. @breakpoint

@breakpoint()

插入一个特定平台的调试陷阱指令,会引发调试器在那里中断。

此函数仅在函数作用域内有效。

21. 附录

21.1. 运算符索引(operator reference)

运算符 所属章节
a+b a+=b 5.3.1.1. 普通加法(normal addition)
a+%b a+%=b 5.3.1.2. 回绕加法(Wrapping Addition)
a+|b a+|=b 5.3.1.3. 饱和加法(Saturating Addition)
a-b a-=b 5.3.2. 减法(Subtraction)
a-%b a-%=b 5.3.2. 减法(Subtraction)
a-|b a-|=b 5.3.2. 减法(Subtraction)
-a 5.3.3.1. 普通取相反数(negation)
-%a 5.3.3.2. 回绕取反(wrapping negation)
a*b a*=b 5.3.4. 乘法(multiplication)
a*%b a*%=b 5.3.4. 乘法(multiplication)
a*|b a*|=b 5.3.4. 乘法(multiplication)
a/b a/=b 5.3.5. 除法(division)
a%b a%=b 5.3.6. 取余(remainder division)
a<<b a<<=b 5.4.1.1. 普通左移(Bit Shift Left)
a<<|b a<<|=b 5.4.1.2. 饱和左移(saturating bit shift left)
a>>b a>>=b 5.4.2. 右移(bit shift right)
a&b a&=b 5.4.3. 按位与(bitwise AND)
a|b a|=b 5.4.4. 按位或(bitwise OR)
a^b a^=b 5.4.5. 按位异或(bitwise XOR)
~a 5.4.6. 按位非(bitwise NOT)
a==b 5.5.1. 等于eq(equal to)
a!=b 5.5.2. 不等于ne(not equal to)
a>b 5.5.3. 大于gt(greater than)
a>=b 5.5.4. 大于等于ge(greater than or equal to)
a<b 5.5.5. 小于lt(less than)
a<=b 5.5.6. 小于等于le(less than or equal to)
a and b 5.8.1. 逻辑与(boolean and)
a or b 5.8.2. 逻辑或(boolean or)
!a 5.8.3. 逻辑非(boolean NOT)
a[i] 6.1. 数组(array)
a++b 6.1.4.1. 数组粘接(array concat)
a**b 6.1.4.2. 数组重复(array repeat)
?T 7.1. 可选类型(optional)
a.? 7.1.3. 取可选类型载荷(get optional payload)
a==null a!=null 7.1.2.1. 空值比较运算(null comparison operator)
a orelse b 7.1.6. orelse运算(orelse operator)
a||b 7.2.1.3. 合并错误集(merging error set)
e!T 7.2.2. 错误联合类型(error union type)
a catch b a catch |err| b 7.2.2.2. 抓取(catch)
try a 7.2.2.3. 尝试(try)
a:T 3.2.3. 变量类型(variable type)
*T 8.1.1. 指针定义(pointer declaration)
&a 8.1.2. 取地址(address of)
a.* 8.1.3. 解引用(dereference)
|v| 10.3. 捕获(capture)
x() 3.6. 函数(function)
x.y 6.5. 结构(struct)
x{} 6.5. 结构(struct)
"a" 2.2.4. 字符串字面值(string literal)
'a' 2.2.3. 字符字面值(char literal)
// 1.5. 注释(comment)
a1_a2 2.2.1. 整数字面值(integer literal)
_
\ 2.2.4.3. 逃逸序列(escape sequence)
[N]T 6.1. 数组(array)
[]T 6.3. 切片(slice)
{} 3.5. 块(block)
; 3.4. 表达式和语句(expression and statement)
a..b 6.3. 切片(slice)
=> 10.2.4. switch
a...b 10.2.4. switch

21.2. 关键字索引(keyword reference)

关键字 简要说明
align 对齐,指定指针的对齐方式
allowzero 带allowzero属性的指针允许地址值为0
and 逻辑或运算符
anyframe 保存指向函数帧指针的变量类型
anytype 在函数调用时推导出参数具体类型
asm 内联汇编
async 异步函数调用方式
await 等待操作数(栈帧)运行结束,复制返回值。
break break从循环中退出,或从标签块中返回值。
catch 抓取错误值
comptime 确保表达式在编译期计算
const 定义只读变量
continue 在循环中跳回到开始处继续
defer 控制流离开当前块时执行表达式
else if,switch,while,for表达式子句
enum 定义枚举类型
errdefer 如果函数返回错误,则在控制流离开当前块时执行errdefer表达式,errdefer表达式可以捕获未包裹的值
error 定义错误类型
export 使生成目标文件中的函数或变量在外部可见。导出的函数默认采用C调用约定
extern 用于定义将在链接时(静态链接时)或运行时(动态链接时)解析的函数或变量
fn 定义一个函数
for 可用于遍历切片、数组或元组的元素。
if if表达式
inline 在编译时展开内联表达式
noalias
nosuspend 标记没有达到挂起点的区域
or 逻辑或
orelse 如果null则返回关键字后的值
packed 改变结构或联合的内存布局为压缩布局
pub 可以从其它文件引用pub 定义的符号
resume 在挂起点之后继续运行函数帧
return 带返回值退出函数
linksection
struct 定义结构
suspend 挂起当前函数
switch 分支选择表达式
test 测试声明
threadlocal 将变量指定为线程本地变量
try 取出载荷值或退出函数返回错误
union 定义联合
unreachable 断言控制不会流经此处
usingnamespace 导入操作数所有公开符号
var 定义可以修改的变量
volatile 易变性
while 条件循环语句
isize 有符号平台相关整数类型
usize 无符号平台相关整数类型
comptime_int 整数字面值类型
comptime_float 浮点数字面值类型
bool 布尔类型
anyopaque 用于类型擦除的类型
void 零位长类型
noreturn 无返回的类型
type 编译期可知的类型值的类型
anyerror 全局错误集
undefined 未定义值
true
false
opaque 不透明类型
noreturn 无返回类型
void 类型

21.3. 内置函数索引(builtin function reference)

内置函数由编译器提供,可直接使用,其函数名前缀是 @ 。

内置函数 所属章节
@addrSpaceCast 9.2.4.5. @addrSpaceCast
@addWithOverflow 5.3.1.4. @addWithOverflow
@alignCast 4.4.3.3. @alignCast
@alignOf 12.9.2. @alignOf
@as 9.1.1. @as
@asyncCall 17.6.5. @asyncCall
@atomicLoad 17.7.1.1. @atomicLoad
@atomicRmw 17.7.1.2. @atomicRmw
@atomicStore 17.7.1.3. @atomicStore
@bitCast 9.2.5. @bitCast
@bitOffsetOf 12.9.4. @bitOffsetOf
@bitReverse 5.4.11. @bitReverse
@bitSizeOf 12.9.5. @bitSizeOf
@boolToInt 9.2.2.1. @boolToInt
@breakpoint 20.7. @breakpoint
@byteSwap 5.4.10. @byteSwap
@call 3.6.4. @call
@cDefine 19.4.1. @cDefine
@cImport 19.4.2. @cImport
@cInclude 19.4.3. @cInclude
@clz 5.4.7. @clz
@cmpxchgStrong 17.7.1.4. @cmpxchgStrong
@cmpxchgWeak 17.7.1.5. @cmpxchgWeak
@compileError 11.7. @compileError
@compileLog 11.8. @compileLog
@ctz 5.4.8. @ctz
@cUndef 19.4.4. @cUndef
@divExact 5.3.5.1. @divExact
@divFloor 5.3.5.2. @divFloor
@divTrunc 5.3.5.3. @divTrunc
@embedFile 15.2.1. @embedFile
@enumToInt 9.2.2.2. @enumToInt
@errorName 12.8.2. @errorName
@errorReturnTrace 7.2.3.2.1. @errorReturnTrace
@errorToInt 9.2.3.2. @errorToInt
@errSetCast 9.2.3.1. @errSetCast
@exp,@exp2 5.3.7.2.2. 指数函数(exponential function)
@export 18.4.1. @export
@extern 18.4.2. @extern
@fence 17.7.1.6. @fence
@field 12.8.4. @field
@fieldParentPtr 6.5.7.1. @fieldParentPtr
@floatCast 9.2.1.3. @floatCast
@floatToInt 9.2.1.4. @floatToInt
@floor,@ceil,@trunc,@round 5.3.7.3. 舍入函数(rounding function)
@frame 17.6.1. @frame
@Frame 17.6.2. @Frame
@frameAddress 17.6.3. @frameAddress
@frameSize 17.6.4. @frameSize
@hasDecl 12.8.6. @hasDecl
@hasField 12.8.5. @hasField
@import 18.4.3. @import
@intCast 9.2.1.1. @intCast
@intToEmum 9.2.2.3. @intToEnum
@intToError 9.2.3.3. @intToError
@intToFloat 9.2.1.5. @intToFloat
@intToPtr 9.2.4.3. @intToPtr
@log,@log2,@log10 5.3.7.2.3. 对数函数(logarithmic function)
@max 5.5.8. @max
@memcpy 14.4.1. @memcpy
@memset 14.4.2. @memset
@min 5.5.7. @min
@mod 5.3.6.2. @mod
@mulWithOverflow 5.3.4.1. @mulWithOverflow
@offsetOf 12.9.3. @offsetOf
@panic 13.2. @panic
@popCount 5.4.9. @popCount
@prefetch 14.4.3. @prefetch
@ptrCast 9.2.4.1. @ptrCast
@ptrToInt 9.2.4.2. @ptrToInt
@reduce 6.2.2.2. @reduce
@rem 5.3.6.1. @rem
@returnAddress 17.6.6. @returnAddress
@select 6.2.2.3. @select
@setAlignStack 8.4.4. @setAlignStack
@setCold 3.6.5. @setCold
@setEvalBranchQuota 11.6. @setEvalBranchQuota
@setFloatMode 5.7.1. @setFloatMode
@setRuntimeSafety 13.1. @setRuntimeSafety
@shlExact 5.4.1.3. @shlExact
@shlWithOverflow 5.4.1.4. @shlWithOverflow
@shrExact 5.4.2.1. @shrExact
@shuffle 6.2.2.4. @shuffle
@sin,@cos,@tan 5.3.7.2.1. 三角函数(trigonometric function)
@sizeOf 12.9.1. @sizeOf
@splat 6.2.2.1. @splat
@sqrt,@fabs,@mulAdd 5.3.7.1 代数函数(algebraic function)
@src 12.1.1. @src
@subWithOverflow 5.3.2.1. @subWithOverflow
@tagName 12.8.3. @tagName
@This 12.10. @This
@truncate 9.2.1.2. @truncate
@Type 12.7.1. @Type
@typeInfo 12.7.2. @typeInfo
@typeName 12.8.1. @typeName
@TypeOf 12.7.3. @TypeOf
@unionInit 6.5.1. @unionInit
@Vector 6.2.1.1. @Vector
@wasmMemoryGrow 14.4.5. @wasmMemoryGrow
@wasmMemorySize 14.4.4. @wasmMemorySize

21.4. Zig语言语法规范(Grammar)

参见Zig语言文档Grammer部分

21.5. 风格指南(style guide)

编译器不强制执行这些编码约定,建议参考。

21.5.1. 空白(whitespace)

4个空格缩进;

'{' 在同一行,除非需要换行;

如果列表的内容大于2,将每一项单独一行,并在末尾加 ',' ;

行长小于100。

21.5.2. 命名(name)

根据情况分别使用这3种风格:camelCaseFunctionName, TitleCaseTypeName, snake_case_variable_name。

type 用 TitleCase。如果是名字空间,即0个属性的struct且从不实例化,用snake_case;

可被调用且返回type,用TitleCase;

否则,用snake_case。

在书面英语中,大写字母缩写词、专有名词或任何其他有大写规则的单词,也受命名约定约束。

有顶级属性的名字空间文件(内含struct),使用TitleCase;否则用snake_case。目录名用snake_case。

21.5.3. 示例(examples)

style_example.zig

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 {}

21.5.4. 文档注释指导(doc comment guidance)

省略任何基于被记录事物名称的冗余信息。

鼓励将信息复制到多个相似的函数上,因为这有助于IDE和其他工具提供更好的帮助文本。

用 assume 来表示引发未定义行为的不变量。

用 assert 表示引发安全检查未定义行为的不变量。

21.6. 源文件编码(source encoding)

Zig源代码是UTF-8编码,无效的UTF-8字节序列会导致编译错误,不允许下列码点(包括注释):

除了U+000a (LF), U+000d (CR), and U+0009 (HT)之外的Ascii控制字符: 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风格的行结尾,但不鼓励这样做。不允许在任何其他情况下使用CR。

HT硬制表符(字节值0x09,代码点U+0009, '\t')可与SP空间(字节值0x20,代码点U+0020, ' ')互换作为token分隔符,但不鼓励使用硬制表符。

在源文件上运行zig fmt 将实现这里提到的所有建议。另外stage1编译器还不支持CR或HT控制字符。

注意,如果假设源代码是正确的Zig代码,那么读取Zig源代码的工具可以做出假设。例如,在识别行尾时,工具可以使用简单搜索,如/\n/,或高级搜索,如/\r\n?|[\n\u0085\u2028\u2029]/,在这两种情况下,行结束符将被正确识别。

另一个例子是,当识别一行上第一个标记之前的空白时,工具可以使用简单搜索,比如/[\t]/,或者使用高级搜索,比如/\s/,在这两种情况下,空白都可以被正确识别。

21.7. 汉英对照表(chinese-english table)

这是我在本手册中用到的英文,我基本按个人理解进行翻译,有些可能与主流翻译不一致。

汉语 英文
C语言编译器 clang
unicode码点 unicode code point
package
饱和加法 saturating addition
崩溃 panic
比较 comparison
比特 bit
编译期 compile time
编译期 comptime
编译期可知 comptime-known
变量 variable
变量隐藏 shadow
遍历 iterate
遍历器 iterator
标签 label
标识符 identifier
标准错误 stderr
标准库 std
标准输出 stdout
标准输入 stdin
表达式 expression
补码 complement
捕获 capture
不等于ne not equal to
不可达 unreachable
不是数 nan not a number
不透明 opaque
布尔 bool
参数 parameter
操作数 operand
操作系统 OS
插入 insert
常量 const
超额申请 overcommit
乘法 multiplication
程序 application
抽象 abstraction
除法 division
大端序 big-endian
大于gt greater than
大于等于ge greater than or equal to
单位长 size
单指令流多数据流 SIMD(Single Instruction Multiple Data)
导出 export
导入 import
地址 address
等于eq equal to
递归 recursion
调用 call
定义 declaration
动态 dynamic
section
断言 assert
heap
对齐 alignment
多维数组 multidimensional array
反射 reflection
泛型 generic
方法 method
not
分配器 allocator
分支 branch
分支 prong
浮点数 float
符号 symbol
符号位 symbol bit
负值 negation
赋值 assignment
概念 concept
工件,人工制品,编译各阶段的产出 artifact
公开 public
构建 build
构造 construct
挂起 suspend
函数 function
函数 function
macro
后续 epilogue
忽略 ignore
互操作 interop
环境 environment
缓冲区 buffer
恢复 resume
回绕加法 wrapping addition
汇编 assembly
汇集 mix
or
基准测试 benchmark
继续 continue
加法 addition
兼容 compatible
减法 subtraction
简略方式 shortcut
降级 demotion
交叉编译 cross-compilation
交换 swap
结构 struct
解包裹 unwrap
解引用 dereference
进入下一个分支 fallthrough
精确 exact
局部 local
聚合类型 aggregate type
开关 switch
可选 optional
null
block
扩大 widen
类型 type
类型强转 type coercion
联合 union
枚举 enum
名字空间 namespace
明确 explicit
默认 default
目标obj文件 object file
目标平台三元组 target triple
内存垃圾收集 gc(garbage collection)
内存屏障 memory barrier
内存泄漏 memory leak
内联 inline
内置 builtin
排除故障 debug
前导 prologue
前缀 prefix
强转 coerce
切片 slice
取余 remainder
全局 global
容量 capacity
删除 remove
舍入 round
生命周期 lifetime
失败 failure
实例 case
示例 example
属性 field
数组 array
说明 description
所有权 ownership
索引 index
逃逸序列 escape sequence
体系结构 architecture
条目 entry
同步 sync
推导 infer
退出 exit
外部 extern
唯一,去重 unique
未定义行为 undefined behavior
无穷大 inf(infinity)
下溢 underflow
向量 vector
向下 floor
小端序 little-endian
小于lt less than
小于等于le less than or equal to
修饰符 specifier
鸭子类型 duck type
严格 strict
页面 page
异步 async
异或 xor
易变性 volatile
溢出 overflow
引发 cause
应用程序二进制接口 ABI(application binary interface)
应用程序接口 API
硬编码 hard code
用户态 userland
优化器 optimizer
语法 syntax
语句 statement
预处理器 perprocessor
元组 tuple
约定 convention
运算符 operator
运行期 run time
运行期 runtime
运行期可知 runtime-known
载荷 payload
stack
frame
整数 integer
直译为底层虚拟机,是编译工具链 LLVM(low level virtual machine)
value
指定值结尾 sentinel-terminated
指令 instruction
指针 pointer
中断退出 break
重载 overload
注释 comment
抓取 catch
转换 cast
转换 cast
转译 translation
追加 append
追踪 trace
子句 clause
字节 byte
字节序 endianness
字面值 literal
作用域 scope

22. 编写工具及版权声明

本文档用纯文本按progdoc格式编辑,用progdoc程序生成单一html文件。

progdoc格式解析程序是用zig语言开发的。 :)

本文未经许可严禁转载。

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。