Zig语言中文手册
Zig语言是强类型的、无内存垃圾收集的静态语言,具有独一无二的强大的编译期计算和编译期类型反射功能,并且紧跟时代发展,有切片、可选类型、错误联合类型等现代特性。Zig语言编译工具链可直接编译C语言,跨平台的交叉编译强大且易用。编译期运算更易于性能优化,交叉编译更易于Zig源代码跨平台使用。
可以大致理解为:
- Zig = c + 编译期运算 + 现代语言特性;
- Zig编译工具链 = Zig编译 + c编译 + 交叉编译。
如果有基本的C语言和计算机组成原理方面的基础,更易于学习使用Zig语言。
本文档参考下列文档和源代码编写:
名词解释:
内存垃圾收集(:gc garbage collection)
用alloc是在堆上申请的内存,堆内存由语言虚拟机自动管理、自动释放的特性称之为内存垃圾收集。
编译期(:compile time)
源代码须经过编译生成目标文件,经链接后生成可执行程序。编译期是指从源代码到生成目标文件期间。
运行期(:run time)
运行期是指可执行程序从程序加载到内存中,到程序运行结束期间。
交叉编译(:cross-compilation)
交叉编译是指在某平台上,源代码可以编译生成另一平台的可执行文件。例如:在Windows下生成linux可执行程序。
本文档重点是理解和使用Zig语言本身。
由于阅读时大段文本理解费劲,另外为了便于在手机和PAD上阅读,所以:
- 对与Zig语言本身无关内容不做过多阐述
- 尽量用短句
- 示例程序只针对当前章节知识点,尽量短小
- 能用中文尽量用中文,文后会给出英汉对照表
- 为了便利于母语非英语的中文程序员,用到英文时基本上是小写和单数,请忽略英文语法错误
- 本手册是单一文件,方便离线保存和文档内检索
安装包下载网址:
https://ziglang.org/download/
下载后解压,将Zig目录添加到PATH(或path)环境变量。
表明安装成功。
示例程序尽可能不超过半个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)。
如果没有额外的编译测试选项,或者运行无错或测试符合预期,则文档中不再描述命令行情况。
示例中, fn 是函数定义关键字, @ 开头的一般是Zig语言的内置函数, @import 函数引入标准库。
Zig 程序的运行起点是 main 函数。如果是程序库则不需要 main 函数。
print函数中的第2个参数 .{"world"} ,是匿名列表类型。
shell表示命令行交互情况。$ 后面是命令行输入,其它是信息输出行。在windows环境下, $ 可能是 c:\zig>
在 windows 的 cmd 命令行窗口下,zig 程序使用非 ascii 字符(如汉字)可能会输出乱码。
一个简便的方法是用 chcp 命令。 chcp 不带参数显示当前代码页(即输入输出编码方式),带参数改变当前代码页。常用的参数有:
参数 |
编码方式 |
说明 |
936 |
GBK |
简体中文 |
65001 |
UTF-8 |
|
437 |
IBM437 |
美国编码 |
1252 |
latin-1 |
0x80-0xff中间有些是西欧字母的编码 |
Zig源代码的注释以 // 开头到行尾。例如上例中的//this is comm
Zig没有多行注释。
文档注释行主要用于自动生成API接口文档。
文档注释行以 /// 开始。
文档注释最好是单独的行,否则编译可能出错(比如在表达式中间时)。
整个文件的注释以 //! 开始。文件注释行只能放在文件的最前面。
Zig语言中的值必须要有确切的类型。
Zig语言中没有专门设立字符类型和字符串类型。字符字面值的类型是 comptime_int ,字符串字面值的类型是 *const [:0]u8。
名字解释:
字面值(:literal) 是指在源代码中用数字、字符或字符串直接写出的值,。比如: x=12;
12 就是数字字面值。
整数类型名中 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 类型,均是字节。
浮点数类型有: f16 f32 f64 f80 f128
浮点数类型须符合IEEE-754标准,f后面的数字是比特位长。
f32可对应C语言的float类型,f64可对应C语言的double类型。
comptime_int 编译期可知的整数字面值的类型。
comptime_float 编译期可知的浮点数字面值的类型。
- bool 布尔类型,只有2个值,true false
- anyopaque 用于类型擦除指针,类似于C语言中的void
- void 零位长的类型,
- noreturn 后面表达式或语句值的类型: break, continue, return, unreachable, while(true){}
- type 编译期可知的类型值的类型
- anyerror 错误代码值的类型
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) 描述了目标平台、应用程序之间的数据类型、调用约定、二进制格式等接口规范。
字面值是指写在源代码中的,由数字、字母和其它字符组成符合语法的具体值。
整数字面值的类型是 comptime_int。
整数类型字面值可转换为能容纳其值的任意整数类型。
不加前缀表示是十进制数,加前缀 0x 表示十六进制,前缀 0o 表示八进制,前缀 0b 表示二进制。
字面值数字字符间可以用 _
隔开,以更易于查看。
浮点数字面值的类型是comptime_float,可以最大保证与f128有同样的精度。
浮点数字面值可强制转换为任意浮点数类型,如果没有小数则可强制转换为整数类型。
字面值中有 e 的是十进制的科学计数法格式,基数是10;有 p 的是二进制的科学计数法格式,基数是2。
字符字面值是指以单引号包围的单个字符,类型是 comptime_int ,值是 unicode 码点。
如下例中,字符 e 的值是 0x65 ,也是它的 unicode 码点,类型是 comptime_int。
汉字 语 的值是 0x8BED ,也是它的 unicode 码点,类型也是 comptime_int。
字符字面值不能为空(即''),如果是''则编译出错。如果想表示编码为0的字符,可以用'\x00'。
名词解释:
unicode 全球统一的,可容纳各种语言的,字符集、编码方案方面的国际标准。https://home.unicode.org/
unicode码点(:unicode code point) 字符对应的unicode 32位编码称之为码点,不同字符的码点也不同。编码范围是从 0 到 0x10FFFF 。
字符串字面值是指以双引号包围的零到多个字符,类型是 *const [x:0]u8
,是指向以 '\x00' (字节值为0)结尾的,长度为 x(不含结尾0)的,元素类型为 u8 的常量指针。
字符串字面值是UTF-8编码的。
如本例中,"hello"的类型是 *const [5:0]u8
, "" 的类型是 *const [0:0]u8
。
把 "hello" 赋值给 bytes 常量,则 bytes 可以转换为切片使用,取得长度值,根据索引访问元素。
名词解释:
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 |
字符字面值是Unicode码点,字符串字面值则是UTF-8编码。如本例和上例中,
'语'是字符,值是0x8BED,是字符的Unicode码点,类型是comptime_int;
"语"是字符串,值是E8 AF AD 0
,是字符的UTF-8编码,类型是*const [3:0]u8
。
"语"实质上等同于"\xE8\xAF\xAD"
。
以十六进制方式查看本例源代码文件,'语' 实际保存为27 E8 AF AD 27
。
' 的UTF-8编码为0x27, 语 的UTF-8编码为0xE8AFAD。而本例i的值是0x8BED,这是 语 的unicode码点。
这说明使用字符字面值时,Zig语言编译时把源代码中UTF-8编码自动变成 unicode 码点。
对于ASCII字符来说,字符等于元素,因为其unicode码点值和UTF-8编码值相同;
对于非ASCII字符来说,字符不等于元素,因为其unicode码点值和UTF-8编码值不同。
名词解释:
ASCII码(american standard code for information interchange) 美国信息交换标准代码是最早最通用的字符编码,编码范围0到127(0x7F)。
以 \ 开头的表示逃逸序列,具体如下:
名词解释:
逃逸序列(escape sequence) 为了能在字符串中表示制表符、换行等不能直观输入和显示的字符,用 \ 和其后的字符组合,转义为对应的控制字符。
多行字符串字面值的每一行用 \\ 开头,且多行字符串内的逃逸序列无效。
TODO: 如果按Windows文本规范的结尾,其源代码的行以\r\n结尾,则多行字符串字面值编译时失败,行必须以\n结尾才能正确编译,需要打补丁。
可以用数组字面值给数组或切片类型赋初始值。
语法有两种如下,T是元素的类型,如果数组的长度是N,大括号中元素的数量也必须是N个。
[_]T{ e1, e2, ... en}
[N]T{ e1, e2, ... en}
当变量定义了具体的数组类型时,数组字面值可省略类型前缀,语法为:
.{ e1, e2, ... en}
可用结构字面值给结构类型的变量赋初始值。
结构字面值语法如下,其中sname是结构类型的名字,field 是属性的名字,v是初始值,
sname{.field1=v1, .field2=v2, ... .fieldn=vn}
当变量定义了具体的结构类型时,结构字面值可省略类型前缀,其语法为:
.{.field1=v1, .field2=v2, ... .fieldn=vn}
赋初始值时,如果有些属性值不立即赋具体值,可以设为undefined,稍后再赋值,如本例中的f3.y。
注意:结构体字面值中,须包括结构的所有没有默认值的属性(但是不能包含结构内定义的静态变量),否则编译错误。
如果待赋值的变量没有定义类型,或者待输入的函数参数定义是 anytype, 那么不带结构类型的结构字面值称为匿名列表字面值(anonymous list literal),又称为元组。
元组中各元素必须设定类型,元素间的类型可以相同,也可以不同,这点与结构类似。
因没有定义属性名字,所以元组的属性名是从零开始的数字序号,这点与数组的索引类似。
因为元组是编译期可知的常量,所以可以用 inline for 遍历。
可用枚举字面值给枚举类型的变量赋初始值。
枚举字面值语法为 ename.fieldx
,其中 ename 是枚举类型的名字,fieldx是 ename 中某个属性的名字。
当变量定义了具体类型时,枚举字面值可省略类型前缀,其语法为 .fieldx
可用联合字面值给联合类型的变量赋初始值。
联合字面值语法为 uname{.fieldx=init}
,其中uname是联合类型的名字,fieldx是 uname 中某个属性的名字,init 是初始值。
当变量定义了具体类型时,联合字面值可省略类型前缀,语法为 .{.fieldx=init}
变量和运算符构成表达式,表达式和运算符构成语句,语句构成语句块,语句块加参数列表加返回值类型构成函数,语句、函数构成源代码文件。
变量名、类型名、属性名、函数名等可统称为符号。
名字空间可分为:全局名字空间、文件名字空间、函数名字空间、语句块名字空间、聚合类型名字空间。
聚合类型名字空间是指在结构、枚举、联合类型内定义的符号。
其中,全局名字空间是最上级,文件名字空间是次一级,这两者是其余3种名字空间的上级。
标识符不能是Zig语言关键字。
普通标识符必须以ASCII英文字母或下划线开始,后面可以有任意数量的ASCII英文字母、ASCII数字、下划线字符。
标识符中不能直接使用非ASCII字符。如果要用不符合要求的变量名,例如与外部库链接或使用 Unicode 字符,用@""
语法。
本例中,C语言error函数的名字是Zig语言的关键字,最大月份值不是ASCII英文字母,所以要用@""
语法。
软件通常是由多个源代码文件构建而成,多个源代码文件的 pub 符号构成全局名字空间。
当前文件的符号定义时加 pub 修饰符,表示其它文件可以用 @import导入使用。
本例中,pubfile.zig中的foo.x, fn2可供test_pub.zig使用。
把最后3行的注释去掉任意1行,编译出现如下类似错误并中止。
error: 'y' is not marked 'pub'
与C语言交互时, extern 引入外部符号, export 导出本文件符号供其它文件使用。
直接在源代码文件中定义,没有在函数、类型和语句块内部定义的所有符号,称为文件名字空间。本例中,s1, i, foo, 属于文件名字空间。 x, y, k, j不属于。
本函数内不属于全局名字空间和文件名字空间的符号,且不是聚合类型中的属性名的符号,称为函数名字空间。
函数名字空间包括其中所有下级聚合类型名字空间、块名字空间的符号。
本例中,i, j, s, s1, z 均是foo函数名字空间的符号, x, y是 s 的属性,不属于foo函数名字空间;
本语句块内不属于全局名字空间和文件名字空间的符号,且不是聚合类型中的属性名的符号,称为块名字空间。块名字空间包括所有下级块的符号。
本例中,if(j>0)
语句块名字空间包含有 k, if(i)
语句块名字空间包含有属于自己的符号 s, j, 和下级语句块的符号 k
在结构、枚举、联合等聚合类型内定义的变量名,和其自身属性名,属于聚合类型名字空间。
本例中,s 名字空间下有 x,y,z 符号; s1 名字空间下有 x,y 符号。
因s 和 s1 没有上下级关系,所以可以重复使用 x,y 符号。
Zig语言不允许符号名隐藏,即同一个名字空间内的符号不能一样,不同名字空间的上下级符号名不能一样。
本例中,test语句块名字空间的 i 与 文件名字空间 i 属于上下级关系,所以编译出错中止。
不允许符号隐藏,这样更易于理解和分析代码,减少bug产生。
如果名字空间没有上下级关系,则符号名可以重复。
usingnamespace 将其操作数所有公开的定义,汇集到当前名字空间中。操作数类型必须是结构、枚举、联合和opaque 。
可以用 pub 修饰 usingnamespace 。
usingnamespace在组织文件或包公开的API有重要作用。例如,可以用下面代码来导入用到的C语言符号。
变量实质上是1块符合定义类型的内存单元。
所有的变量有6个要素:名字、类型、值、地址、占用内存字节长度、对齐。
类型确定了变量的运算规则、占用内存字节长度、默认对齐方式,所以类型是程序设计语言的核心内容之一。
赋给变量的值必须能转换为该变量的类型。
变量的地址不可修改。
变量名字须符合3.1.1. 标识符(identifier)要求,且同一名字空间、上下级名字空间的变量名字不能一样,即不能3.1.5. 隐藏(shadow)。
变量定义赋初始值后,其值不能修改的称之为常量 const ,可以修改的称之为变量 var 。
定义变量时,尽可能用 const , 这样不容易有 bug ,且易于优化。
如果改变常量的值,则编译出错中止。
本例编译出错中止,并输出如下信息。
error: cannot assign to constant
变量必须有类型,不存在没有类型的变量。通常在定义时须获知变量的类型,否则编译出错中止。
类型定义语法为:
const name:T=v; var name:T=v;
const name=vT; var name=vT;
定义变量时:
- 必须要用 const 或 var ,声明常量还是变量
- 类型和变量名用 : 隔开
- 类型在变量名之后
- 如果当初始值可以推导出准确类型时,可省略类型,以初始值的类型为准。
本例中,变量y的类型可以根据x推导出是i32,所以y的类型定义可以省略。
本例中,去掉注释,则var k没有明确的类型,会编译出错中止,输出信息为:
error: variable of type 'comptime_int' must be const or comptime
- 常量定义时必须赋初始值,没有初始值则编译出错中止
- 如果定义时不赋初始值,稍后再赋值,则须设变量值为undefined
- undefined可赋值给任何类型的变量
- 准备使用时则须给该变量赋有效值
本例中,如果把注释去掉,则编译出错中止,输出信息为:
error: variables must be initialized
调试模式下,用字节值0xAA填充到初始值为 undefined 变量的对应内存,这样有助于调试器检测未定义内存。
变量定义后必须要使用,如果始终不使用则编译出错中止。
可以用 _ = expr;
忽略表达式 expr 的运算结果。
_ 字符可以理解为垃圾筒变量,类似于/dev/null
。
本例编译时出错中止,错误信息为:
error: unused local constant
把注释符删掉,则测试通过。
运算符根据需要操作数的数量,可分为单目运算符、双目运算符。
例如:-a 对a取相反数运算,其中 - 是单目运算符; a-b 是减法运算,其中 - 是双目运算符。
Zig语言中运算符没有重载功能。
名词解释:
重载(:overload) 指多个函数名字一样(或是同一个运算符),但参数的数量或类型不一样,叫重载。
运算符有优先级,优先级高的先运算。例如,乘法优先级比加法高,所以:
1+5*3==1+15==16
而不是
1+5*3!=6*3!=18
用 ()
包围的表达式被看作是一个表达式,所以可以改变优先级:
(1+5)*3==6*3==18
运算符的优先级见下表,第1行优先级最高,第12行优先级最低。同一行的优先级一样。
表达式是符合语法规范的符号和运算符的组合式子,表达式有结果值。
语句是符合语法规范的运行单元,语句没有结果值。
定义语句、赋值语句等语句,行尾需要加 ; 。
以 }
结尾的流程控制语句和语句块行尾不加 ; 。
下例中,if(i>10)
是if表达式,其结果值赋给常量i;if(j)
是语句,语句块内运行expect函数语句。
内有0条或多条语句组成,用{ }
括起来的语法单元,称为语句块。
语句块可以嵌套,即内部还可以有语句块。
语句块也有值,普通语句块的值是 void,通常可以用break语句跳出带标签的语句块,给语句块赋值。
把语句块的值赋给其它变量,语句块的 } 后要加 ;
语句块内的语句,按书写的先后顺序运行,如果有break等跳转语句,按语句定义运行。
函数由函数名name、参数列表varlist、返回值类型result、函数体body、修饰符specifier组成。
specifier fn name(varlist) result body
- 修饰符、参数列表可以为空。
- 参数列表是用 , 隔开的,带类型声明的参数名列表。
- 函数名、返回值类型、函数体不能为空。
- 如果没有返回值,则返回值类型处要用void填写。
- 函数体是语句块,也可以是空语句块。
- 不支持函数嵌套定义,即不能在函数内再定义函数。
整数和浮点数等基本类型做参数时,在函数体内使用的是值的副本。这种情况基本只涉及到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接口标准,按值传递结构和联合。
注意:不管按值传递还是按引用传递,在函数体内均不能改变参数的值。
函数参数用anytype类型定义时,在函数被调用时根据输入变量的类型,来推导出参数的类型。
export修饰符使函数在生成的obj文件中外部可见,并且用C语言ABI接口,参看本例中sub函数;
extern修饰符定义该函数是外部定义,定义在链接时静态库里,或运行时动态库里,参看ExitProcess和atan2函数;
pub修饰符允许函数外部可见,其它文件能用@import函数导入并调用,参看sub2函数;
callconv修饰符改变函数的调用约定:
.Naked 使函数没有附加的前导和后续,通常用于与汇编集成,参看_start函数;
.Inline 强制在该函数的所有调用处内联,如果函数不能内联,则产生编译错误,参看shiftl1函数;
@setCold 告知优化器该函数很少被调用,参看abort函数。
@call(options:std.builtin.CallOptions,function:anytype,args:anytype) anytype
调用函数,与使用括号调用表达式的方式相同。
通过设置参数CallOptions,可以比普通函数调用有更多的灵活性。
stack: 仅当modifier==Modifier.async_kw时有效;
auto: 等效于普通函数调用
async_kw: 等效于用async关键字的函数调用
never_tail: 防止尾调用优化,这样保证返回地址指向调用点,而不是调用点的调用点。如果是尾调用优化或内联,则产生编译错误
never_inline: 保证调用不会内联。如果是内联,则产生编译错误
no_async: 断言函数调用不会挂起。这样允许非异步函数调用异步函数
always_tail: 保证调用是尾优化的,如果不能则产生编译错误
always_inline: 保证在调用点内联函数,如果不能则产生编译错误
compile-time: 在编译时计算调用。如果不能则产生编译错误
@setCold(comptime is_cold:bool)
告知编译器本函数很少被调用。
名字空间、作用域、生命周期、所有权这四者决定了变量是否有效。
作用域是指符号(包括普通变量、类型定义、函数定义等)在程序运行期间的有效使用区域。
不能在某作用域之外,使用此作用域的符号。
注意:离开作用域不绝对等同于变量已失效,只是不能使用。
作用域与名字空间类似,可分为全局作用域、文件作用域、函数作用域、块作用域。
结构等聚合类型名字空间不是作用域。
全局作用域是指符号在整个应用程序范围内有效,类似于C语言中的全局变量。
文件作用域是指符号在本源代码文件内有效,类似于在C语言文件下直接用static 声明的变量和函数。
函数作用域是指符号在本函数内有效,块作用域是指符号在本块内有效。
与名字空间类似,有上下级关系的作用域,下级作用域可以使用上级作用域符号,上级作用域不可以使用下级作用域符号。
如下例中,x在下级(也就是y所在的)语句块有效。
x在上级语句块无效,即 最后一行语句x=5;
编译出错。
当程序运行流是离开当前作用域(通常是代码块或函数)时,如果有defer语句,则运行该语句。
同一个作用域有多个defer语句时,按定义的反向顺序运行,先定义的后运行,后定义的先运行。
不运行的语句块内的defer语句,不会运行。见例子中的defer3。
defer语句块中不能有return语句,否则编译出错。
当离开当前作用域出错时,如果有errdefer语句,则运行该语句,主要用于清理错误现场,类似于C语言中的goto语言。
通常内存分配代码后,紧跟用含有内存释放代码的errdefer语句。这样错误处理更健壮,而不必为了确保覆盖每个退出路径编写冗长的代码。
errdefer还支持捕获操作,可以在清理错误现场过程中,记录错误信息。
errdefer语句只持续在块末尾,如果在块外返回错误,则不会执行errdefer。
下面当if语句返回错误时,已不在errdefer定义的块范围内,所以errdefer不会执行,有泄漏风险。
上例中,把if语句前的注释去掉,则测试通过。
可以保留变量值有效的时间称为变量的生命周期。
通常变量的生命周期全过程有定义、使用、失效:
- 定义是指把某块内存单元与变量的名字关联起来,可以用变量名来访问该块内存单元;
- 使用期间,可以读取变量的要素(值、地址、类型等),也可以更改变量的部分要素(值等);
- 失效是指在程序运行期间变量名变得永远不再可访问,这样就再也不能通过该变量名访问到其对应的内存单元。
生命周期可分为静态生命周期和动态生命周期。静态生命周期是指在程序运行期间一直有效的变量,动态生命周期是在程序运行期间不一定一直有效的变量。
局部变量是指生命周期仅在本函数或本语句块或 @import 块内有效的变量。局部变量是动态生命周期。
局部变量的名字空间、作用域、生存周期这三者是一致的。
本例中,j离开语句块后,不可再使用,所以编译出错中止。
局部变量可以定义在comptime块内,或用comptime关键字来修饰。这样该变量的值是编译期可知的,并且该变量的所有读取和写入都发生在程序编译过程中,而不是在运行时。在comptime表达式中定义的所有变量都是comptime变量。
本例中,因为y是编译期变量,y!=2 也是1个编译期变量,其值为false,所以编译时解析if(y!=2)不会运行,就不会触发@compileError,则测试通过。
本例中,因x的值是运行期可知,编译期不可知,编译期不能求得 x!=2 的值,不能解析出 if(x!=2) 肯定不会运行,所以需要解析到if语句内,导致触发@compileError,编译中止。
编译时出错,输出信息为:
error: wrong x value
静态变量是指在程序运行期间一直有效的变量,静态变量编译时可以延迟解析,所以使用和定义前后顺序无关,即使用语句可以写在定义语句之前。在Zig语言中,静态变量又被称为容器级变量(container level variable)。
全局名字空间的符号(即定义时带 pub 或export 的)是全局变量。
全局变量有静态生命周期。
本例中,i在j之后定义,但j仍可以使用i,因为i是静态变量。
直接在源代码文件中定义的,没有在函数定义、类型定义、语句块内部的所有不带 pub 或 export 的变量,称之为文件级变量。
文件级变量有静态生命周期,属于文件名字空间和文件级作用域。
本例中,i, add1, add2的定义顺序不影响正常使用。且变量 x 在运行期间一直有效,值由1变为2再变为4。
结构、枚举、联合内定义的变量是聚合类型变量。
聚合类型变量有静态生命周期,属于聚合类型名字空间。
其作用域归属于聚合类型定义语句所在位置,不在函数定义、语句块内的属于文件级作用域;否则属于函数或块作用域。
文件级作用域的聚合类型内变量见本例。
函数或块作用域的聚合类型变量又称之为静态局部变量。
静态局部变量有静态生命周期,其作用域属于函数作用域或块作用域。
请仔细比较本例中foo和foo1的不同,可以深刻理解静态生命周期。
foo调用后,x的修改后的值被保持,说明foo函数中的x是在程序运行期一直有效,是静态生命周期;
每次调用foo1,返回值总是x+1==1235,说明foo函数中的x是局部变量,退出函数作用域则x失效。
两次调用foo1,运行流程相当于:
两次调用foo,运行流程相当于:
extern 关键字或 @extern 内置函数,可以链接从其它文件或库导出的变量。
extern 变量的类型必须是C语言ABI兼容类型。
用threadlocal关键字可定义线程局部变量。线程局部变量不能用const定义。
如果用单线程方式构建,所有线程局部变量视为普通静态变量。
字符串字面值存储在全局常量数据段,所以可转换为不可变切片,但不能转换为可变切片。
const定义的变量如果值是编译期可知,以及编译期可知的变量,也存储在全局常量数据段中。
函数中用var定义的变量存储在函数的栈帧中。当函数返回时,任何指向栈帧中变量的指针会变成无效引用,且此时解引用是未定义行为。
用var定义的静态变量,存储在全局数据段。
用allocator.alloc或allocator.create分配的内存位置,由分配器的实现确定。
变量定义是指把某块内存单元与变量的名字关联起来,也就是说,这块内存单元目前归该变量所有。
变量失效是指再也不能通过该变量名访问到其对应的内存单元,也就是说,这块内存单元此时已不归该变量所有。
在生命周期之外,该块内存单元的所有权回归程序后,或空闲或再次分配给其它变量。
如果没有指针的话(这从本质上说,如果变量没有地址要素),则在变量生命周期内,该块内存单元的所有权只有1个,就是这个变量。
是这样的话,那一切都是这么简单轻松和美好。
但这是白日做梦。
引入指针后,如果把变量的地址赋给指针变量,则该指针变量也可以修改该块内存单元的值,也可以通过free等调用释放该块内存单元所有权。此时,该块内存单元的所有权到底是属于原变量呢?还是属于指针变量呢?这是巨多烦恼和BUG的根源。
有3种应对方法如下,总而言之,鱼与熊掌不可兼得,这也是有巨多程序设计语言的主要原因之一:
- 一种就是java, erlang, 等语言,或lua,python, javascript等脚本语言,取消指针类型。好处是编程容易,不足之处是不够贴近硬件平台,运行效率相对低。
- 一种就是C++, rust, 等语言,引入所有权移动语义和各种智能指针,好处是这些语义提供了更多的手段,另外保留指针相对运行效率较高。不足之处是这些语法语义太烧脑了,结合异步、多线程等复杂度爆炸。但能有什么办法呢?现实世界本来就这么复杂。
- 一种就是C, Zig 等语言,有指针类型,但语言本身没有其它所有权移动等语义。好处是有指针相对运行效率高,语言本身知识点少相对容易掌握。不足之处是程序太容易有BUG和漏洞了,程序员水平再高也难以避免。
如果有兴趣,或许可以通过读Zig语言源代码和标准库中的源代码,来深入理解所有权和生命周期。
程序员的责任是确保不访问指向内存不可用时的指针,切片是指针因为引用其它内存。
为防止bug,在处理指针时需要遵循一些有用的约定。通常当一个函数返回指针时,文档应该说明谁“拥有”这个指针。这有助程序员决定什么时候释放指针是合适的。
例如,函数文档可能会注明“调用方拥有返回的内存”,这时调用函数的代码必须有释放内存的代码。在这种情况下,函数可能会有Allocator参数。
有时指针的生存期可能更复杂。例如,在下一次调整列表大小(如追加新元素)之前,std.ArrayList(T).items切片生命周期保持有效。
函数和数据结构的API文档应该非常注意解释指针的所有权和生命周期语义。所有权决定了谁负责释放指针引用的内存,生命周期决定了内存无法访问的时间点,以免出现未定义行为。
内存的基本单位是为字节,1个字节共8个比特位。
通常可以理解为,当变量的内存地址可以被该变量所属类型的字节长整除时,则该变量是对齐的。
基本类型变量如果对齐的话,可以在一个CPU的读周期就可以读出;不对齐的话,可能就得两个CPU的读周期,极端的CPU会异常退出。这就是对齐也是变量基本属性的原因之一。
对齐值与CPU体系结构有关。
对齐值始终是2的次方,且小于0x1000_0000(1<<29)。
64位平台的指针对齐值为8,32位平台的指针对齐值为4。
变量的对齐值等于对应类型的字节长。
可以用 align(N) 指定变量和函数的对齐值,指向这些变量指针的对齐值属性与指定值相同。
可设置结构属性的对齐值。
当指针指向类型的对齐值等于该类型的对齐值时,则在指针类型声明里可以省略对齐值。
可以将对齐值较大的指针隐式转换为对齐值较小的指针,反之则不行。
本例中,ptr的类型是 *align(4) u8
,可隐式转换为ptr1的类型 *u8
。
@alignCast(comptime alignment:u29,ptr:anytype) anytype
操作数类型:指针或切片
说明:ptr可以是 *T, ?*T, []T
。返回值和ptr相同类型,返回值的对齐值调整到新值。
如果指针指向类型(或切片元素类型)的对齐值较小,但可以安全的转换为对齐值更大的类型,那么可用@alignCast可将指针或切片安全的改为对齐值更大的指针或切片。
这个步骤运行时是无额外操作的,但会有安全检查,如不通过则产生未定义行为。
示例:
本例可通过,是因为此时a是文件级静态变量,而上例中的a是局部变量。
@setAlignStack(comptime alignment:u29)
说明:确保函数的栈对齐至少为alignment个字节。
整数类型参见2.1.1. 整数(integer)
浮点数类型参见2.1.2. 浮点数(float)
可以用 std.math.floatMax, floatMin, maxInt, minInt函数,取得不同类型整数和浮点数的最大值最小值。
也可以直接用std.math.f16_max f16_min f32_max f32_min f64_max f64_min fl128_max, fl128_min 常量的值,取得相应类型的浮点数的最大最小值。
在计算机中,零和正数用原码表示,负数用补码表示,所以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
N进制通俗讲就是逢N进1。 设N进制的1位数字为x,则x的范围是0<=x<=(N-1);两数相加,当和等于N时,向上1位进1,本位变为0,例如:
计算机中开发最常用的是十进制、十六进制、二进制。
十进制-十六进制-二进制对照表
十进制 |
十六进制 |
二进制 |
|
十进制 |
十六进制 |
二进制 |
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,按倒序记下余数,就是十六进制,例如:
所以十进制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
十进制和二进制之间互相转换,可以中间用十六进制为桥梁,更简便些。
算术运算包括有加、减、乘、除、取余、取相反数。
语法(:syntax):a+b a+=b
操作数类型(:operand type): 整数(integer),浮点数(float)
说明(:description):整数加法可能会产生溢出;为2个操作数调用成对类型解析。
示例(:example):
语法:a+%b a+%=b
操作数类型:整数(integer)
说明:确保结果回绕。回绕是指运算结果仅保留操作数类型的比特位长度,溢出比特位舍弃。为2个操作数调用成对类型解析。
示例:
本例中,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。
语法:a+|b a+|=b
操作数类型:整数(integer)
说明:如果运算结果超出范围,则运算结果为该范围的极限值(最大值或最小值);为2个操作数调用成对类型解析。
示例:
@addWithOverflow(comptime T:type,a:T,b:T,result:*T) bool
操作数类型:整数(integer)
说明:result.* = a+%b
,如果发生溢出或下溢,返回 true,否则返回false。
示例:
本例中,127+1=0b1000_0000,在i8中对应的是-128,发生上溢;
-128+-1=0b1_0111_1111,发生下溢,截断溢出比特位后为0b0111_1111,在i8中对应的是127。
浮点数只有普通减法,整数减法也分为普通减法(subtraction)、回绕减法(wrapping subtraction)、饱和减法(saturating subtraction),整数类减法的说明基本与加法一样,参见5.3.1. 加法(addition)。
语法:
普通减法:a-b a-=b
回绕减法:a-%b a-%=b
饱和减法:a-|b a-|=b
示例:
@subWithOverflow(comptime T:type,a:T,b:T,result:*T) bool
操作数类型:整数(integer)
说明:result.* = a-%b
,如果发生溢出或下溢,如果发生溢出或下溢,返回 true,否则返回false。
示例:
本例中,-128-1=0b1_0111_1111,发生上溢,截断溢出比特位后为0b0111_1111,在i8中对应的是127;
127-(-1)=0b1_0111_1111,为0b1000_0000,在i8中对应的是-128,发生下溢。
语法:-a
操作数类型:整数(integer),浮点数(float)
说明:整数取反可能会引起溢出,例如i8类型的整数的范围是从-128到127,对-128取反可能会溢出。
示例:
Test [1/1] test.negation overflow... thread 14856 panic: integer overflow
语法:-%a
操作数类型:整数(integer)
说明:确保结果回绕。
示例:
参见5.3.1.2. 回绕加法(wrapping addition)
浮点数只有普通乘法,整数减法也分为普通减法(multiplication)、回绕减法(wrapping Multiplication)、饱和减法(saturating multiplication),整数类乘法的说明基本与加法一样,参见5.3.1. 加法(addition)。
语法:
普通乘法:a*b a*=b
回绕乘法:a*%b a*%=b
饱和乘法:a*|b a*|=b
示例:
示例中,因130*2=260,260是0x1_04,u8类型仅有1字节长,所以回绕后 130*%2==4;
因-70*2=-140,-140是0xFF_74,i8类型仅有1字节长,所以回绕后的结果为0x74,所以-70*%2==0x74==116。
@mulWithOverflow(comptime T:type,a:T,b:T,result:*T) bool
操作数类型:整数(integer)
说明:result.* = a*%b
,如果发生溢出或下溢,如果发生溢出或下溢,返回 true,否则返回false。
示例:
本例中,70*2=0b1000_1100,在i8中对应的是-116,发生上溢;
-70*2=0b1_0111_0110,发生下溢,截断溢出比特位后为0b0111_0110,在i8中对应的是116,发生下溢。
语法:a/b a/=b
操作数类型:整数(integer),浮点数(float)
说明:整数除法可能会引发溢出、除零错;
浮点数除法在Optimized模式时也可能会引发除零错;
有符号整数操作数必须是编译期可知的确定值,否则只用@divTrunc, @divFloor, 或 @divExact来相除;
为2个操作数调用成对类型解析。
示例:
本例中,i是编译期可知的确定值是-6的有符号整数,所以允许进行除法运算;m是运行期可知而不是编译期可知的值,所以不允许直接进行除法运算。
@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 。
示例:
本例中,@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 在编译期发现参数不符合要求,则编译出错中止;在运行期发现参数不符合要求,则返回错误。
@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 可能返回错误。
示例:
@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 可能返回错误。
示例:
语法:a%b a%=b
操作数类型:整数(integer),浮点数(float)
说明:整数取余可能会引发除零错;浮点数取余在Optimized模式时也可能会引发除零错;有符号整数操作数必须是编译期可知的确定值,否则用@rem, @mod来取余。为2个操作数调用成对类型解析。
示例:
@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 可能返回错误。
示例:
@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 可能返回错误。
示例:
本节下各小节函数的操作数均是浮点数或浮点数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,是乘和加的整合运算,只计算一次,因此更快。
@sin(value:anytype) @TypeOf(value)
说明:求正弦值。
@cos(value:anytype) @TypeOf(value)
说明:求余弦值。
@tan(value:anytype) @TypeOf(value)
说明:求正切值。
@exp(value:anytype) @TypeOf(value)
说明:求以e(自然对数)为底的指数。
@exp2(value:anytype) @TypeOf(value)
说明:求以 2 为底的指数。
@log(value:anytype) @TypeOf(value)
说明:求自然对数。
@log2(value:anytype) @TypeOf(value)
说明:求以 2 为底的对数。
@log10(value:anytype) @TypeOf(value)
说明:求以 10 为底的对数。
@floor(value:anytype) @TypeOf(value)
说明:求不大于给定浮点数的最大整数值。
@ceil(value:anytype) @TypeOf(value)
说明:求不小于给定浮点数的最小整数值。
@trunc(value:anytype) @TypeOf(value)
说明:浮点数舍入为整数,舍入方向是零。
@round(value:anytype) @TypeOf(value)
说明:浮点数舍入为整数,舍入方向是远离零。
位运算包括左移、右移、位与、位或、位异或、位非,还有求高位比特0个数、低位比特0个数、比特1个数、高低比特位交换和取反等内置函数。
语法:a<<b a<<=b
操作数:整数(integer)
说明:a向左移动b个比特位,用比特0填充右侧的移出位。操作数b必须是编译期可知的值,或者是比特位长等于a的类型比特位长取2对数值的整数类型。
示例:
本例中,j的类型是u8,log2(8)==3,所以左移右边的操作数必须是u3类型。
本例中,100的二进制为0110_0100,左移两位后变为01_1001_0000,因j的类型比特位长为8,所以截断多出的比特位后为1001_0000,即为144。
语法:a<<|b a<<|=b
操作数:整数(integer)
说明:左移后超出类型最大值,则为该类型的最大值。
示例:
@shlExact(value:T,shift_amt:Log2T) T
操作数:整数(integer)
说明:运行比特位左移。对于无符号整数,如果移出的比特里有1,则结果是未定义行为;对于有符号整数,如果移出的比特里有与最高符号位不一致的,则结果是未定义行为。
shitf_amt 的类型是无符号整数,比特位长是 log2(@typeInfo(T).Int.bits)
。
当 shift_amt >= @typeInfo(T).Int.bits
时是未定义行为。
示例:
@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 时是未定义行为。
示例:
本例中,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。
语法:a>>b a>>=b
操作数类型:整数(integer)
说明:a向右移动b个比特位,a是无符号整数时,用0填充左侧的移出位;a是有符号整数时,用最高比特位的值(也就是符号位)填充左侧的移出位。
操作数b必须是编译期可知的值,或者是比特位长等于a的类型比特位长取2对数值的整数类型。
示例:
@shrExact(value:T,shift_amt:Log2T) T
说明:
运行比特位左移,调用保证移出的比特里没有1,如果有则是未定义行为。
shitf_amt 的类型是无符号整数,比特位长是 log2(@typeInfo(T).Int.bits)
。
当 shift_amt >= @typeInfo(T).Int.bits
时是未定义行为。
示例:
语法:a&b a&=b
操作数类型:整数(integer)
说明:为2个操作数调用成对类型解析。
0&0==0; 0&1==0; 1&0==0; 1&1==1.
示例:
语法:a|b a|=b
操作数类型:整数(integer)
说明:为2个操作数调用成对类型解析。
0|0==0; 0|1==1; 1|0==1; 1|1==1.
示例:
语法:a^b a^=b
操作数类型:整数(integer)
说明:为2个操作数调用成对类型解析。
0^0==0; 0^1==1; 1^0==1; 1^1==0.
示例:
语法:~a
操作数类型:整数(integer)
说明:按位非必须指定具体类型。
~0==1; ~1==0.
示例:直接用整数字面值 ~0b0101
,则编译出错中止。
@clz(operand:anytype)
操作数类型:整数(integer),整数vector(intger vector)
说明:计算从最高比特位开始,连续为比特0的个数。如果operand为零,@clz 返回整数类型 T 的位宽。
如果operand是编译期可知的整数,则返回类型为 comptime_int;
否则,返回类型为无符号整数或无符号整数vector,该类型的最小比特位数可以表示operand类型的比特位数。例如,operand类型为u8,则返回类型至少为u3以上。
@ctz(operand:anytype)
操作数类型:整数(integer),整数vector(intger vector)
说明:计算从最低比特位开始,连续为比特0的个数。如果operand为零,@clz 返回整数类型 T 的位宽。
如果operand是编译期可知的整数,则返回类型为 comptime_int;
否则,返回类型为无符号整数或无符号整数vector,该类型的最小比特位数可以表示operand类型的比特位数。例如,operand类型为u8,则返回类型至少为u3以上。
@popCount(operand:anytype)
操作数类型:整数(integer),整数vector(intger vector)
说明:统计整数中比特1的数量。
如果operand是编译期可知的整数,则返回类型为 comptime_int;
否则,返回类型为无符号整数或无符号整数vector,该类型的最小比特位数可以表示operand类型的比特位数。例如,operand类型为u8,则返回类型至少为u3以上。
@byteSwap(operand:anytype) T
操作数类型:整数(integer),整数vector(intger vector)。
说明:交换整数的字节顺序,这将整数的字节序从大端变为小端,或从小端变为大端。
注意,整数类型布局应该与@sizeOf得到的size相关。如对u24,@sizeOf(u24)==4,这表明虽然u24只需要24位即可,但在内存中是按4个字节保存的。所以如果T被指定是u24,只有3个字节被反转。
示例:
@bitReverse(integer:anytype) T
操作数类型:整数(integer),整数vector(intger vector)。
说明:按比特位反转整数值,包括最高符号位。
示例:
比较包括等于、不等于、大于、大于等于、小于、小于等于,还有求最大值、最小值内置函数。
语法:a==b
操作数类型:整数(integer),浮点数(float),布尔类型(bool),类型值的类型(type)
说明:如果a和b相等返回true,否则返回false;为2个操作数调用成对类型解析。
语法:a!=b
操作数类型:整数(integer),浮点数(float),布尔类型(bool),类型值的类型(type)
说明:如果a和b相等返回false,否则返回true;为2个操作数调用成对类型解析。
语法:a>b
操作数类型:整数(integer),浮点数(float)
说明:如果a大于b返回true,否则返回false;为2个操作数调用成对类型解析。
语法:a>=b
操作数类型:整数(integer),浮点数(float)
说明:如果a大于或等于b返回true,否则返回false;为2个操作数调用成对类型解析。
语法:a<b
操作数类型:整数(integer),浮点数(float)
说明:如果a大于b返回true,否则返回false;为2个操作数调用成对类型解析。
语法:a<=b
操作数类型:整数(integer),浮点数(float)
说明:如果a小于或等于b返回true,否则返回false;为2个操作数调用成对类型解析。
@min(a:T,b:T) T
操作数类型:整数(integer),浮点数(float)
说明:如果a和b中有1个是NaN,则返回另1个;如果都是NaN,则返回NaN。
@max(a: T, b: T) T
操作数类型:整数(integer),浮点数(float)
说明:如果a和b中有1个是NaN,则返回另1个;如果都是NaN,则返回NaN。
浮点数中,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 函数。
浮点数运算默认是Strict模式,可以在当前语句块中切换到Optimized模式,并仅在当前语句块有效。
下面的示例,须将代码分成两个目标文件,否则在编译时会直接计算出所有浮点数值,而编译时的操作是Strict模式。
注意:在linux环境下,应该用foo.o而不是foo.obj
语法:@setFloatMode(comptime mode: @import("std").builtin.FloatMode)
设置当前作用域的浮点数模式,FloatMode是:
默认值是 Strict, 此时浮点数运算操作遵循严格的 IEEE 规范。
设为 Oprimized 后,浮点运算可以执行以下操作:
- 假设参数和结果不是 NaN。需要进行优化以保留 NaN 上已定义的行为,但是结果值是未定义的;
- 认定参数和结果不是 +/-Inf。需要进行优化以保留 +/-Inf 上已定义的行为,但结果值是未定义的;
- 参数或结果是零时,零的正负符号视为不重要;
- 使用参数的倒数而不是除法;
- 执行浮点数收缩(例如,将一个乘法和一个加法融合为一步运算);
- 执行可能改变浮点数结果的代数等效转换(例如重新关联)。
Oprimized 相当于 GCC 中的 -ffast-math 选项。
浮点模式可被子作用域继承,并且可以在任何作用域中重写。
布尔类型只有两个值,true(真) 和 false(假)。
比较运算的结果值是布尔类型。
布尔运算有逻辑与、逻辑或、逻辑非。
语法:a and b
操作数类型:布尔类型(bool)
说明:如果a是false,则返回false不再计算b(即逻辑与短路操作)。否则返回b的值。
示例:
本例中:statA语句运行后x==1,说明当第1个操作数是false时,没有计算第2个操作数add5(&x)的值,直接返回false,这就是逻辑与短路;
statB语句后,其结果==false,x==6,说明当第1个操作数是true时,再计算第2个操作数add5(&x)的值,add5(&x)改变x值为6,并且返回false。
语法:a or b
操作数类型:布尔类型(bool)
说明:如果a是true,则返回true不再计算b(即逻辑或短路操作)。否则返回b的值。
逻辑或短路与逻辑与短路类似,具体可参看上一节。
语法:!a
操作数类型:布尔类型(bool)
说明:!true==false; !false==true;
聚合类型是一个约定俗成的概念。
广义的聚合类型是指数组、结构、枚举、联合等类型。
狭义的聚合类型是指结构、枚举、联合类型。
语法:
定义 [N]T
或 [_]T
; 索引 a[i]
; 取长度 a.len
说明:数组是由若干个元素组成的,这些元素类型相同,在内存中按前后顺序依次存储。数组的索引从0开始,可以根据索引值得到元素。
数组的长度是在编译期可知的,且数组的长度一经确定不能更改。
需要能改变长度的动态数组,可以用std.ArrayList,参见6.4. 动态数组(dynamic array)
可以用2.3. 数组字面值(array literal)给数组或切片类型赋初始值。
可以用函数或其它方法对数组进行初始化赋值。
元素是数组的数组,称为多维数组。
多维数组的索引:a[i][j]
用for语句遍历数组时,需要修改数组值,则用|*item|捕获;仅仅读取,可以用|item|捕获;需要用到索引,则用|item,i|捕获,具体见示例。
也可用while语句来遍历数组。
对编译期可知的数组,可以使用粘接和重复。
语法: a++b
操作数类型:数组(array)
说明:操作数a和b必须是编译期可知的。
示例:
语法:a**b
操作数类型:数组(array)
说明:操作数a和b必须是编译期可知的。
示例:
向量是一种特殊的数组,数组元素由逻辑值、整数、浮点数或指针组成。对向量运算在编译时,会尽可能使用SIMD指令实现。
名词解释:
SIMD(Single Instruction Multiple Data)是单指令流多数据流,可以在一条CPU指令周期内同时计算多个数据的值。比如支持AVX2指令集的CPU平台上,下条指令就是把256位长的寄存器a和寄存器b,当成8个i32整数组成的数组,同时相加。
__m256i _mm256_add_epi32(__m256i a, __m256i b)
用内置函数@Vector创建向量。向量没有len属性。
- 向量的总位长小于目标平台的本机SIMD位长时,其运算会编译为单个SIMD指令;
- 大于本机SIMD位长时,则编译为多个SIMD指令;
- 如果目标主机CPU无SIMD指令支持,则编译器实现对每个向量元素运算。
- Zig语言支持编译期长度为0xFFFF_FFFF(2^32-1)的向量,但通常是使用2的次方(指数从2到64)。
注意:向量长度过长(如2^20),可能会导致当前版本的编译器崩溃。
@Vector(len:comptime_int,Element:type) type
说明:建立长度为len,元素类型为type的向量。
示例:
向量逐元素运算,运算结果是和输入向量长度相同、类型相同的向量,向量的运算符包括:
- 算术运算(Arithmetic):
+ - / * @divFloor @sqrt @ceil @log 等。
- 位运算(Bitwise Operators):
>> << & | ~ 等。
- 比较运算(Comparison Operators):
< > == 等。
不能在标量(单个数值)和向量之间运算,可以用内置函数@splat把标量转换为向量,用@reduce或数组索引语法把向量转换为标量,用@shuffle和@select实现在向量内部或向量间重新排列元素。
@splat(comptime len:u32,scalar:anytype) @Vector(len,@TypeOf(scalar))
说明:生成一个长度为 len 的 vector ,其中每个元素的值都是 scalar 。
@reduce(comptime op:std.builtin.ReduceOp,value:anytype) E
说明:使用指定的运算符 op 对value(类型是vector) 的元素执行连续的水平约简,将vector转换为类型是 E 的标量值。
对于整数vector,每个运算符都是可用的。
.And, .Or, .Xor 可用于 bool vector。
.Min, .Max, .Add, .Mul 可用于浮点数 vector。
整数类型的.Add 和 .Mul 是回绕的;保留浮点数类型操作结合性,除非将浮点模式设置为 Optimized。
@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]。
示例:
@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 ,则该函数执行逻辑相当于:
对于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 可以是任意长度,这决定了结果的长度。
向量可以和编译期长度已知的固定长度数组之间互相赋值。
也可以把编译期长度已知的切片赋值给向量。
TODO vector类型的切片
语法: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] |
切片本质上是1个胖指针,由1个多项指针(属性名是ptr)和长度(属性名是len)组成。
切片的索引也是从0开始。
运行时取切片,类型是[]T
,编译时取切片,类型是*[N]T
。
数组的类型是[N]T
,长度是类型的一部分,是编译期可知。例如:[4]i32
和[8]i32
并不是同一种类型的数组。
切片的类型是[]T
,类型与长度无关,其长度是运行期可知。
更建议用切片而不是指针,因为切片有边界检查。本例中,把注释符删掉,则编译出错中止,输出信息为:
error: index 5 outside array of length 4
多项指针没有边界检查。
本例中,s.len可以被修改,s.ptr也可以被修改,运行后显示的第2个数每次都不一样。
这时,s.ptr指针已经指到乱七八糟的地址,通常被称之为野指针,此时处于运行失序状态,不知道会发生什么事。
数组的长度是在编译期可知的,一经确定不能改变数组的长度。
如果数组的长度需要增加或减少,可以用std.ArrayList。
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 。
ArrayList的常用属性有:
- allocator 分配器,ArrayList使用的内存分配器
- capacity 容量,ArrayList实际占用内存大小,单位是元素个数
- items 动态数组,可以和数组一样索引、切片等操作。
- items.len 动态数组长度,也就是实际使用的数量,单元是元素个数。items.len<=capacity
追加是指在现有列表的尾部新增元素,追加函数如下,调用时记得用 try 或 catch:
-
append(item:T) !void
追加1个元素
-
appendSlice(items:[]T) !void
追加1个切片
-
appendUnalignedSlice(items:[]align(1)T !void
追加1个未对齐的切片
-
appendNTimes(value:T,n:usize) !void
追加n个元素,其值为value
appendAssumeCapcacity, appendSliceAssumeCapcacity, appendUnalignedSliceAssumeCapcacity, appendNTimesAssumeCapacity
表示调用时保证空余容量大于等于追加元素的个数。这样就不返回错误了,调用时不用 try 或 catch 。
插入是指把输入值插入索引n处,[n..]的元素向尾部移动。插入函数如下,记得调用时用 try 或 catch:
-
insert(n:usize,item:T) !void
在n处插入元素item
-
insertSlice(n:usize,items:[]const T) !void
在n处插入切片items
把指定位置的元素删除,删除函数如下,因不返回错误,所以调用时不用 try :
-
orderedRemove(i:usize) T
删除索引i处的元素,后面的元素前移。返回删除元素的值
-
swapRemove(i:usize) T
交换索引i处和最尾部len-1处的元素后,数组长度减一。返回原索引i处的元素值
-
popOrNull() ?T
删除最尾部len-1处元素,并返回该值。如果长度为0则返回null
尽可能的用 swapRemove 而不是 orderedRemove ,因为 swapRemove 运行效率更高。
可以用一个切片,来替换 ArrayList 中部分元素。
调用方式: replaceRange(start:usize,l:usize,new_items:[]const T) !void
等同于从 ArrayList 的索引 start 处开始,先删掉 l 个元素,然后再插入 new_items。
如果 start+l > list.items.len ,则产生越界的未定义行为。把本例中最后一行的注释符去掉,测试中崩溃。
直接改变 items.len 的长度是危险的操作,用 resize 主要是增加长度,用 shrinkAndFree 主要是减少长度。
-
resize(new_len:usize) !void
设 items.len 为 new_len 。如果当前容量小于new_len,则重新分配内存扩容。
-
shrinkAndFree(new_len:usize) void
保证 new_len 小于 items.len ,用 realloc 缩小容量,缩小后将容量和长度设为 new_len 。
ArrayList 还可以当做字符串输出缓冲区。见本例。
如果元素字节位长是0,则 ArrayList 并不用分配内存。
本例中,FailingAllocator.init的参数为0,表示如果有任何内存分配则出错。
结构由0到多个属性(field)、0到多个方法(method)、0到多个变量(const/var)组成。
定义语法:
使用属性时,用结构变量名.属性名,如下一节示例中的 f.x ;
使用方法和静态变量时,用结构类型名.方法名(或静态变量名),如下一节示例中的 foo.nop() 和 foo.z 。
方法和静态变量并不在结构变量对应的内存单元中,放在结构内仅为了使用名字空间。
定义结构变量时,用2.4. 结构字面值(struct literal)赋初始值,或者用 undefined 稍后再赋值。
属性可以用在编译期执行的表达式来设默认初始值,这些属性在变量定义时可以省略。
结构的类型可以根据匿名结构字面值推导出来。
本例中的匿名结构字面值是有属性名的,而4.5. 元组(tuple)中的示例是
.{@as(u32,1234),@as(f64,12.34),true,"hi"}
没有属性名,所以其属性名是数字0,1,2。
结构定义内可以嵌套子结构定义。
所有的结构都是匿名的。根据定义方式,可以用变量表达式、返回函数值、匿名结构字面值来命名结构类型名称。
本例中,foo是变量表达式定义的,其中y是foo的子结构;
list(i32)是函数返回值定义的,名字中的参数i32是编译期确定的;这种方式通常用于泛型实现。
struct{}是匿名结构,名字是struct_2576,2576是程序随机分配的数字。
结构类型对应的内存单元(即比特位长)仅包括其属性,不包括结构内定义的静态变量和函数。
出于优化需要,普通结构不保证结构比特位长等于属性比特位长之和,也不保证内存中属性顺序和定义一致,但保证属性是基于C语言ABI对齐的。
结构内定义的函数称为方法,使用structname.method(a,b)
来调用。
当方法的第1个参数是本结构自身类型,则可以用简略调用方式:varname.method(b)
来调用。
本例中,f是foo类型变量,则 f.plus(15)
等同于 foo.plus(f,15)
结构内的定义语句、变量不占用结构空间,如果结构的属性数量为0,则结构的比特位长为0。
此种结构常用于名字空间和静态变量。
结构属性的顺序由编译器根据优化需要而定,所以须使用@fieldParentPtr函数,根据属性指针得到结构的指针。
@fieldParentPtr(comptime ParentType:type,comptime field_name:[]const u8,field_ptr:*T) *ParentType
说明:根据属性指针 filed_ptr,返回 struct 指针。
示例:
在 struct 前加 packed 定义压缩结构。
与普通结构不同,压缩结构内存布局保证如下:
- 按属性定义顺序;
- 属性间没有填充字节;
- Zig语言中小于8位的整数,在普通结构中实际保存为1个字节位长,在压缩结构中,按实际比特位宽保存;
- bool类型的属性按1个比特1位保存;
- 枚举类型的属性用对应的整数类型比特位长;
- 联合类型的属性用其最大比特位长属性的比特位长;
- 非ABI对齐属性根据目标字节序,打包成最短的ABI对齐整数。
把本例中的packed去掉,再运行输出如下,其中aa是填充字节,bool和u3 u4类型均按1个字节存储:
注意:用易变压缩结构可能会编译错误,后续会改进。详见[issue 1761](https://github.com/ziglang/zig/issues/1761)。
压缩结构可以用@bitCast或@ptrCast重新解读内存信息,在编译期也适用。
下例中用@bitCast把Full中的x,重新解读为Divided中的half, i1, i2。
对结构的非字节对齐属性(类似于C语言中的位域),可正常读写,也可以用@bitOffsetOf和@offsetOf来查看其在结构中的位置。
可以把非字节对齐属性取址赋值给指针。但这个指针不能像普通指针一样做函数参数。
指向非字节对齐属性的指针与其宿主整数中的其他字段共享相同的地址:
外部结构的内存布局保证匹配目标的C语言ABI接口。外部结构只适用于与C语言ABI接口兼容。
定义外部结构是在类型定义中的 struct 前加 extern 。
类似于C语言中的枚举,枚举是由0到多个属性组成,属性对应的整数值默认从0开始,逐个加1。
类似于结构定义,枚举中也可以定义函数和静态变量。
枚举赋值可以用2.6. 枚举字面值(enum literal)或其它枚举类型变量。
如果变量类型已确定,赋值时可以省略枚举类型。
通常用switch语句处理枚举类型,在分支判断处可省略枚举类型。
枚举的默认标记类型是能容纳属性的最大整数值的无符号整数类型,比如上一节示例中对应的 u1 类型。
也可以指定枚举的标记类型,语法为:enum(T)
可设定某属性的标记为特定整数值,设定后其后的属性对应的整数值在此基础上逐个加1。语法为: filedname=value
与结构类似,枚举内也可以定义函数做为枚举的方法,并且也可以用简略调用方式。
最后属性为 _ 的是非穷尽枚举,非穷尽枚举必须指定标记类型。
switch处理非穷尽枚举,可以用'_'分支替代else分支,此时switch须分析所有已知属性,否则会编译错误。
在非穷尽枚举上使用 @intToEmum ,会影响 @intCast 整数标记类型的安全语义。
枚举不保证与C语言ABI接口兼容。
明确枚举的标签类型,可以与C语言ABI接口兼容,如下例明确为c_int。
联合是指对1块内存区域用不同类型表示。联合的属性可以是不同类型。
联合的语法和结构枚举类似,需要用 union 关键字。
联合也可以定义静态变量,也可以定义函数做为方法,方法也可使用简略调用方式。
@unionInit(comptime Union:type,comptime active_field_name:[]const u8,init_expr) Union
说明:等同于联合初始化语法相同,只不过属性名参数是编译期可知的字符串,而不是标识符。
普通联合只能使用第一次赋值的属性,也就是激活属性。
本例中 .i是激活的属性,所以不能使用 .f属性。
可给变量重新赋值改变激活属性。
也可以使用@ptrCast、外部联合或压缩联合,来访问非激活属性。
标记联合是用枚举类型来定义联合的标记。语法是:union(enumT)
用作标记的枚举类型和联合类型的属性名须完全一致。
普通联合不能用switch语句处理,标记联合可以。
可用switch语句处理标记联合,分支判断部分可以省略类型名称。
用switch语句处理标记联合,捕获时用指针才可以改写其值。
可以根据联合属性名,自动推导对应的枚举标记,来生成标记联合。语法是:union(enum)
标记联合类型可用@as函数转换为标记的枚举类型。
标记联合类型值可以强制转换为标记值。
extern union 可与目标C语言ABI接口兼容。
packed union 可以定义在 packed struct 中。
其它类型还有可选类型、错误联合类型、特定值结尾类型,以及 void 等类型。
狭义的NULL引用
意思是当指针值为 null
时,仍然按指向正常地址的逻辑运行;
广义的NULL引用
意思是某个变量的值处于无意义状态,仍然按正常值的逻辑运行。
这是引起许多诡异和严重的运行异常原因之一。
根源是把变量的正常值和无意义状态(也就是空值)混在一起处理。所以Zig语言设计了可选类型。
可选类型的语法: ?T
。T称为载荷类型。
可选类型的计算操作可以是赋值、比较、取载荷。
可选类型不能和其它类型直接进行加法等其它计算操作。
error: invalid operands to binary expression: 'Optional' and 'Int'
可选类型之间也不能直接进行加法等其它计算操作。
error: invalid operands to binary expression: 'Optional' and 'Optional'
名词解释:
载荷(:payload) 可选类型、错误联合类型等组合类型中的,其有效值的类型。
可以给可选类型变量赋值为载荷类型的值,也可以赋值为 null ,也可赋值为另1个可选类型值。
不能把可选类型的值直接赋给普通变量。
error: expected type 'i32', found '?i32'
不能把 null 赋给普通类型变量。
error: expected type 'i32', found '@TypeOf(null)'
相同的可选类型之间可以互相比较。
可选类型可以和其载荷类型互相比较。
语法: a==null a!=null
操作数类型:可选类型(optional)
说明:如果a是null返回true,否则返回false。
null只能和可选类型比较,不可以和载荷类型比较。
示例:
语法: a.?
操作数类型:可选类型(optional)
说明:等同于 a orelse unreachable,如果a是载荷值则取出,如果a是 null ,则崩溃。
可选类型载荷取出后,可以像正常值或变量一样运算。
示例:
当可选类型是 null 时取载荷,发生未定义行为。
Test [1/1] test.get null payload... thread 11472 panic: attempt to use null value
- 可选类型和载荷类型的比特位长是一样的。
- 可选类型中的null实质上是0。
- 但是在编译时会检查代码,确保可选类型的 null 值不会等同于普通类型的零,也不会把普通类型的零等同于 null。
- 这样在编译期严格的类型安全检查,最大程度的避免了NULL引用的错误。
不用 null 会使源代码变冗长,这里比较一下C语言和Zig语言代码见下面例子。
任务: 调用malloc函数的结果如果为null,则返回null。
这表明,Zig语言至少和C语言一样方便。 orelse 解包裹可选类型取出其载荷类型,所以ptr在函数中保证是非null。
另一种处理NULL的方式是:
在if语句中,捕获f的值为ptr时,不再是可选类型指针,而是不能为 null 的普通指针。
这样有指针参数的函数,在GCC中可以用 __attribute__((nonnull)) 属性进行注释,编译时可更好优化。
语法: a orelse b
操作数类型:可选类型(optional)
说明:如果a是null,则返回b;否则返回a的载荷值。b也可以是类型为noreturn的值。
示例:
主流的错误处理有返回错误码、异常处理两种方式。返回错误码实现简单效率高,但把正常值和错误码混到一起,易出错;异常处理实现复杂功能丰富,但效率略低且错误处理流程不易直观阅读和分析。
Zig等现代程序设计语言往往使用错误联合类型的方式来处理错误,尽可能兼顾错误码和异常处理的优点。
错误用标识符命名,名字相同则是相同的错误,其对应整数编码也相同。
错误的类型是其所属的错误集。参看下一节。
整个编译期间,所有不同的错误名统一被编码为大于0的无符号整数。现阶段被硬编码为 u16 类型,计划将来改为由不重复错误值数量来确定错误编码类型。参见issues #786。
从错误集中取错误,简略方式是用 setname.errorname
语法,标准方式是(error{a,b}).a
,还可以用error.errorname
如果可以根据初始值推导出来错误集类型,则变量定义时可省略类型。
错误只能进行赋值和比较运算。
语法: error{a,b,...}
由1到多个错误名组成的集合,称为错误集类型。
可以同时存在多个错误集,不同的错误集可以包含同样的错误名。如上一节示例中,err1错误集和err2错误集均包含erra0错误。
如果某错误集内所有错误名都包含在另一个错误集内,则称这个错误集为子集,另一个错误集为超集。
可以将错误从子集类型转换为超集类型,不可将超集类型转换为子集类型。
本例中,AllocationError是子集,FileOpenError是超集,可将错误类型由前者转换为后者。
本例中,把错误从超集类型转换为子集类型,则编译出错中止。
anyerror 关键字是全局错误集,包含所有错误,是所有错误集的超集。
可将任何错误集转换为全局错误集。也可将全局错误集明确转换为其它错误集,这将插入语言级断言,以确保错误值实际存在于转换后的错误集中。
使用全局错误集,在编译时无法获知会出现哪些error。而在编译时能获知错误集,可有助于生成文档和有用错误信息,所以应避免使用全局错误集。
名词解释:
断言(:assert) 断言是指如果表达式值为假,则用调试模式生成的二进制程序会运行中止,发布模式生成的通常不会中止。
语法: a||b
操作数类型:错误集合类型(error set)
说明:用 || 可将两个错误集合并,结果包含了两个错误集的错误。
这对根据编译期分支返回不同错误集的函数特别有用,例如Zig std打开文件的错误集是 LinuxFileOpenError || WindowsFileOpenError。
示例:
本例也演示了if和switch组合起来,处理error。
语法: errset ! T
或 ! T
错误集类型和普通类型组合成错误联合类型,表示要么是1个普通类型,要么是1个错误集类型。
错误集类型可以省略,表示由值来推导出错误集类型。
通常错误联合类型比错误集类型更常用。
当返回值的错误联合类型中省略错误集定义,则会转换为类型 anyerror!T,此时会尽可能推导出具体的错误集类型。
在获取函数指针、不同编译构建目标之间错误集保持一致等场景下,错误集推导不太友好。另外错误集推导不兼容递归。所以建议明确定义错误集。
语法:a catch b a catch |err| b
操作数类型:错误联合类型(error union)
说明:如果a是错误,则返回b,否则返回a的载荷值。注意b也可以是类型为noreturn的值。
err是捕获到的错误,其作用域是在表达式b范围内。
示例:
语法:try a
说明:变量为正常值时继续,错误时返回。
const b=try a;
等同于:
const b=a catch |e| return e;
函数内有try,则函数的返回值必须是错误联合类型。
如果函数没必要返回错误联合类型,则用catch关键字。
比如,如果确定表达式不会出错,则可以:
const b=a catch unreachable;
错误返回追踪是在函数中用try后,当有错误发生时,显示代码中返回错误的所有函数调用点。
仔细分析上面例子,当foo(12)时,其函数调用栈和return error过程如下,try语句返回error的步骤用 # 标记。
带 # 的行和执行程序时屏幕显示内容顺序一样。
虽然程序最后返回的错误是ETWO,但第一个返回的错误是EONE,在bar函数中,switch返回ETWO。错误返回追踪清楚的表明了函数间返回错误的全过程,使程序的错误处理更易于解读、分析和调试。
错误返回追踪在Debug 和 ReleaseSafe 构建模式下默认启用,在ReleaseFast 和 ReleaseSmall 构建模式下默认禁用。
激活错误返回追踪的方式有:
- 从main函数返回error;
- 执行进入catch unreachable,且没有覆盖默认panic处理器;
- 用@errorReturnTrace函数访问当前返回追踪,显示这些信息用std.debug.dumpStackTrace。如果构建时没有选择错误返回追踪,那么这个函数返回编译期null。
@errorReturnTrace() ?*builtin.StackTrace
如果程序是用错误返回追踪构建的,且在1个函数(其中调用了返回值是错误或错误联合类型的函数)中@errorReturnTrace被调用,则返回栈追踪对象,否则返回null。
本例把所有的错误处理(try和!)等都去掉。程序执行崩溃后栈追踪。
分析输出行可看出,栈追踪没有显示baz函数在中间的作用。
如果要debug,必须打开调试器或分析测试代码,这样就不如错误返回追踪。
名词解释
栈(:stack) 栈是一种先进后出的队列,始终在尾部写入和弹出。栈是实现函数调用的核心数据结构。函数调用栈中有函数参数、栈帧指针、返回地址、局部变量等重要数据。栈追踪是重要的故障检查排除手段之一。
从两种情况来分析性价比:无错误返回;有错误返回。
当无错误返回时,是1个单一内存写。比如,1个返回void的函数调用1个返回错误函数。在栈内存初始化结构 StrackTrace :
其中 N 是通过调用图分析确定的最大函数调用深度,递归被忽略且计为2。
StrackTrace指针做为隐藏参数,放在参数表的第1位,传递给每个可以返回错误的函数。
所以当无错误返回时,没有性能开销。
为返回错误的函数生成代码时,在返回错误的return语句之前,生成对这个函数的调用:
开销是2个算术运算和一些内存读写。这些内存受限,且在错误冒泡返回期间保持缓存。
至于生成代码大小,在返回语句前调用1个函数问题不大。尽管这样,仍有个计划,把 __zig_return_error 函数设计成尾调用,这样可将代码大小开销降到零。
当生成代码有错误返回追踪时,没有错误返回追踪的return语句生成跳转指令。
指定值结尾类型包括指定值结尾数组、切片和指针。
C语言字符串就是以数字 0 结尾的数组。
名词解释
指定值(:sentinel) 用指定值来表明元素有特殊含义,如列表头、数组结尾等。通常直译为哨兵,但我觉得指定值更易理解。
语法:[N:x]T
或 [_:x]T
说明,数组元素的类型是T,长度是N,在其长度值(len)对应的索引位置的元素值,是指定值x。
示例:
指定值结尾切片的类型是[:x]T
,该类型的长度值已知,长度值对应索引的值等于指定值x。
这种切片并不保证切片中间没有指定值x。
可以使用a[start..end:x]
来创建指定值结尾切片,a可以是多项指针、数组或切片。
本例中,s1的长度len已知等于5,且s1[5]==0
,等于指定值。
从s2中可看出,s2[0]==0
,指定值结尾切片并不保证切片中没有指定值。
指定值结尾的切片,可以确定结尾处的元素值是指定值,否则运行时会崩溃。
Test [1/1] test.nullterm not 0... thread 18708 panic: sentinel mismatch: expected 0, found 1
字符串字面值的类型是*const [len:0]u8,len是字符串长度,字符串是UTF-8编码。下面是用切片来拼接字符串的示例。
语法[*:x]T
描述了一个指针,其长度由一个指定值x决定,具有防止缓冲区溢出和读出边界的保护。
用 opaque {} 定义1个未知位长(肯定不为0)和对齐的类型。通常用于与不公开结构细节C语言代码交互时的类型安全。
noreturn是下列语句或表达式的类型:
break, continue, return, unreachable, while(true){}
解析类型时,如if子句或switch分支时,noreturn 类型与其他所有类型兼容。例如:
下面是 exit函数使用noreturn的实例。
以下类型的的比特位长是零,称为零比特类型:
示例如下:
这些类型仅有一个值,可以等同于用零位来表示。
使用这些类型的源代码不包括在最终生成的机器代码中。
即使在Debug模式下,上面的源代码函数内的语句也不会生成对应的机器代码,比如在x86_64上,生成:
这些汇编指令没有任何与void相关联的代码,只执行函数的前导和后续。
名词解释:
汇编(:assembly) 与CPU的二进制指令对应的英文文本语句,汇编语句与特定CPU和汇编器密切相关。
void主要用于实例化泛型。
例如,给定 Map(Key,Value),设Value的类型是void,则可变成Set(Key)。
值类型用void而不是使用虚拟值,hashmap条目类型中没有值属性,因此hashmap内存空间更少,且所有读写值的代码都会被删除。
void 与 anyopaque 不一样,void 的字节位长是已知的0字节长,而 anyopaque 的字节位长是未知的非0值。
只有void类型的表达式才可被忽略。变量或函数返回值也可用赋值给 _ 明确表示忽略。
追求速度的程序设计语言都有指针类型,追求易用性的都没有。
指针能提高运行速度的本质原因是可以最大程度的减少内存复制,高效率的同时带来了巨多的复杂性。
指针变量也是类型的一种,也有6个要素:名字、类型、值、地址、占用内存字节长度、对齐。
设变量a的类型为T,值为value,内存地址为addr,把变量a的内存地址,保存到另一个变量b,则:
- 变量b的类型,是指向类型T的指针;
- 变量b的自身值,是变量a的内存地址addr,可以理解为是usize类型的值;
- 还可以把变量b的内存地址addr1赋值给变量c,则变量c可以称之为二级指针,依此类推可以有三级或更高层级指针;
- 指针类型变量b的占用内存字节长度,通常和目标平台和可执行程序位长有关,32位程序的是4字节,64位程序的是8字节;
- 指针类型变量b的对齐值与变量a的对齐值互不相关,指针类型的对齐值通常是4或8;
- const指针类型,是指不能修改指针变量的自身值,与能不能解引用后修改指向变量值互不相关;
- 根据变量b的自身值,也就是addr,访问到value,称为解引用操作;
- 在变量b解引用时运算,等同于对变量a直接运算;
- 把变量a的内存地址,赋给变量b,称为取址操作;
- 指向同一个地址的指针,如果类型不同,则运算结果可能不同。
语法: *T
说明:T可以是绝大多数类型。
输出数字是i的自身值,因为i没有被赋值,所以这个数是个无意义的野指针。
语法: &a
操作数类型:所有类型(all type)
说明:取变量a的地址。
示例:
a[2]的地址是52a0,a[1]的地址是529c,25a0-529c==4,正好是i32类型的字节长度。
语法:a.*
操作数类型:指针(pointer)
说明:解引用时运算,等同于对指针指向变量的运算。
因为函数参数在函数体内只能读不能改,所以想在函数内部修改外部变量值,只能使用指针。这也是指针最普遍的用途之一。
示例:
设变量b为指向变量a的指针,类型为T,解引用后修改变量a的值。则根据指针b的类型不同,分别如下:
语法 |
指针b的自身值 |
变量a的值 |
*T |
可以修改 |
可以间接修改 |
*const T |
可以修改 |
不能修改 |
const *T |
不能修改 |
可以间接修改 |
const *const T |
不能修改 |
不能修改 |
把本例中任意一行的注释符去掉,都编译出错中止。
类型名:*T
单项指针仅只向1个单独的变量。
支持的操作有:解引用,重新赋值;不能进行指针加减、索引和取切片运算。
把本例中任意一行的注释符去掉,都编译出错中止。
指向数组某个元素的指针是单项指针。
类型名:[*]T
多项指针是指向未知个数元素的指针,元素的类型是T。
T必须是已知位长的类型,不能是anyopaque或其它不透明类型(opaque type)。
支持的操作有:指针加减整数、索引和取切片运算,不支持取长度、解引用操作。
可把数组的地址赋值给多项指针,赋值时多项指针类型声明必不可少。
可把编译期切片和运行期切片赋给多项指针。
把单项指针的自身值赋值给多项指针,也就是单项指针转换为多项指针,可以用 @ptrCast函数。
还可以把单项指针转换为有1个元素的数组指针,再把数组指针转换成多项指针。
通常不建议这么做,因为多项指针没有边界检查,如本例中的ptr[3]是未知用途的。
多项指针可以索引,也可以加减整数值。
多项指针互相不能加减
可以从多项指针上取切片,取出后的类型为 *[N]T
,是数组指针。
类型名:*[N]T
指向N个元素的指针,元素的类型是T。
支持的操作有:取长度、解引用、索引和取切片运算,不支持指针加减整数操作。
可以把数组的地址赋值给数组指针,赋值时可以省略类型。
可以把切片赋值给数组指针。
数组指针可以索引和取长度。
数组指针解引用后,类型为[N]T
,是数组。
可以从数组指针上取切片,取出后的类型为 *[N]T
,是数组指针。
运行期取切片,类型是[]T
,编译期取切片,类型是*[N]T
。
因为运行时切片的长度编译时不可知,所以运行时取切片类型中不含长度。
切片指针其实就是切片,详见6.3. 切片(slice)。
支持的操作有:取长度、索引和取切片运算,不支持指针加减整数操作。
运行期切片不支持解引用操作,编译期切片支持解引用操作。
语法 ?*T
是一个可选类型,其载荷是指向类型T的指针。
语法 !*T
是一个错误联合类型,其载荷是一个指向类型T的指针。
语法 !?*T
是一个错误联合类型,其载荷是一个可选类型,该可选类型的载荷是指向类型T的指针。
函数指针类型是在函数定义基础上加 *const前缀。
函数指针可以是运行期可知。
结构内属性是指向子结构的指针可自动解引用,可省略 .*
,属性指向普通类型的指针不能自动解引用。
在祼机操作系统上,地址0可以被寻址,这时须加allowzero属性,否则会崩溃。
如果想表示空指针,请使用可选类型。
如果给定的读取或写入有副作用,比如内存映射的输入/输出(MMIO),从外设接口地址读取或写入时,应使用volatile。在下面的示例中,mmio_ptr的读取和写入被保证全部发生,而且顺序与源代码相同。
易变性与并发和原子操作(atomic)无关,如果在MMIO外使用 volatile ,通常是个错误。
设a是变量,则指针解读和取值方法为:
语法 |
取a值方式 |
解读 |
*a |
a.* |
指针,指向a |
?*a |
a.*.? |
可选类型,载荷是指向a的指针 |
*?a |
a.?.* |
指针,指向载荷为a的可选类型 |
!*a |
try a.* |
错误联合类型,其载荷是指针,指针指向a |
!?*a |
try a.?.* |
错误联合类型,其载荷是可选类型,可选类型的载荷是指向a的指针 |
!*?a |
try a.*.? |
错误联合类型,其载荷是指针,指向载荷为a的可选类型 |
设T是类型,则数组相关指针为:
- *T 单项指针,不能加减
- [*]T 多项指针,无长度属性
- *[]T 指针,指向切片
- *[N]T 指针,指向数组
- *[:x]T 指针,指向指定值结尾切片
- *[N:x]T 指针,指向指定值结尾数组
只读相关指针解读参见:8.2. const指针(const pointer)
类型转换是指变量在内存单元中值不变,把变量的类型转换成另一种。
类型强转是完全安全和明确的转换,精确转换是不希望发生意外的转换,成对类型解析是用于有多个操作数时,确定结果类型的转换。
当需要的类型和提供的类型不同、完全明确类型如何转换、保证安全不发生未定义行为时,自动进行类型强转。
@as(comptime T:type,expression) T
说明:如果转换是明确和安全的,则允许此强转,这是在类型之间进行转换的首选方法。
运行时相同表现形式的值可以强转到更严格的限定,反之则不行。包括:
- const 可以从非const到const
- volatile 可以从非volatile到volatile
- align 对齐值可以从大到小
- error 错误可以从子集到超集
- 指针可强转为const optional指针
这些强转在运行时是无操作的,因为值表现形式没有改变。
整数可强转到能表示旧类型每个值的整数类型,同样浮点数可强转到能表示旧类型每个值的浮点类型。
小数部分为0的浮点数可强转为整数。
本例中,产生编译错误是对的,因表达式有两种强转结果,编译器无所适从:
54.0 :comptime_int 54/5 ==> 10 :f32
5 :comptime_float 54.0/5.0 ==> 10.8 :f32
可选类型的载荷类型,和 null 一样,可被强转为可选类型,也可被强转为错误联合类型。
错误联合类型的载荷类型,和错误集类型一样,也可被强转为错误联合类型。
如果可以在目标类型中表示编译期可知数值,则该数值可以被强转。
标记联合可以被强转成枚举。
当枚举是编译期可知的标记联合的属性,且该属性只有1个值,可被强转为标记联合。
指针类型转换参见9.2.4. 指针和整数精确转换(pointer and integer explicit cast)
单项指针转换为多项指针,参见8.4.1.1. 单项指针赋值给多项指针(single-item is assigned to many-item pointer)。
多项指针: [*]T
运行期切片指针:[]T
数组指针(也就是编译期切片指针): *[N]T
这三者指针互相转换的方式见本例。
undefined可转换为任意类型。
用内置函数来进行精确转换。
这些内置函数可能是安全的,可能是执行语言级断言,可能是在运行期是无操作的。
- @intCast-整数类型之间转换
- @truncate-整数类型之间转换,截断多余位
- @floatCast-将大浮点数转换为较小浮点数
- @floatToInt-取得浮点数值的整数部分
- @intToFloat-将整数转换为浮点数
@intCast(comptime DestType:type,int:anytype) DestType
说明:将一个整数转换为另一个整数,同时保持相同的数值。试图转换超出目标类型范围的数字是未定义行为。
@truncate(comptime T:type,integer:anytype) T
说明:从整数类型中截断比特位,生成较小或相同大小的整数类型。
这个函数总是截断整数的有效位,而不管目标平台上的字节序是什么。
可对目标类型范围之外的数字调用 @truncate 。
使用@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 |
@floatCast(comptime DestType:type,value:anytype) DestType
说明:把浮点数类型类型转换为 DestType 。这个转换是安全的,但数值可能会失去精度。
本例中,因为1.8e40已超过f32的最大值,所以精确转换为正无穷,没有发生未定义行为。
@floatToInt(comptime DestType:type,float:anytype) DestType
说明:浮点数的整数部分类型转换为 DestType 。
浮点数的整数部分不适合 DestType,是未定义行为。
Test [2/2] test.@floatToInt UB... thread 3060 panic: integer part of floating point value out of bounds
@intToFloat(comptime DestType:type,int:anytype) DestType
说明:将整数转换为最接近的浮点数,这种情况总是安全的。
- @boolToInt-true为1,false为0
- @enumToInt-取得枚举或标记联合的整数标记值
- @intToEnum-根据整数标记值取得枚举值
@boolToInt(value:bool) u1
说明:转换true 为 @au(u1,1) , false 为 @as(u1,0)。
如果value在编译时可知,则返回类型是comptime_int。
@enumToInt(enum_or_tagged_union:anytype) anytype
说明:把enum的值转换为其整数标记值,把标记union的枚举值转换为其整数标记值。
如果只有1个可能的enum值,结果值是编译期可知的,类型为comptime_int。
@intToEnum(comptime DestType:type,integer:anytype) DestType
说明:将整数转换为enum值。
试图转换在所选枚举类型中不表示值的整数,是未定义行为。
error: enum 'test_intToEnum.e' has no tag with value '10'
###注:把const i改为var i,测试可以通过。与预期不符。
- @errSetCast-转换为较小的error set
- @errorToInt-取得error值的整数值
- @intToError-根据整数值取得error值
@errSetCast(comptime T:DestType,value:anytype) DestType
说明:把value类型转换为另一个错误集。
转换不在 DestType中的错误会导致受安全保护的未定义行为。
error: error sets 'error{six}' and 'error{one,two}' have no common errors
@errorToInt(err:anytype) std.meta.Int(.unsigned,@sizeOf(anyerror)*8)
操作数类型:全局错误集,错误集,错误联合类型
说明:将 err 转换为错误的整数表示。
因为随着源代码变动,错误的整数表示是不稳定的,通常情况下不建议使用此函数。
@intToError(value:std.meta.Int(.unsigned,@sizeOf(anyerror)*8)) anyerror
说明:将错误的整数表示转换为全局错误集类型。
因为随着源代码变动,错误的整数表示是不稳定的,通常情况下不建议使用此函数。
尝试转换与任何错误不对应的整数是未定义行为。
- @ptrCast-指针类型之间转换
- @ptrToInt-取得指针的地址值
- @intToPtr-将整数地址值转换为指针
- @alignCast-改变指针对齐值
- @addrSpaceCast-改变指针地址空间
@ptrCast(comptime DestType:type,value:anytype) DestType
将指针转换为另一类型的指针。
允许使用optional指针。
将null的optional指针强转为非optional指针将调用安全检查的未定义行为。
Test [1/1] test.null cast UB... thread 14120 panic: cast causes pointer to be null
@ptrToInt(value:anytype) usize
操作数类型:是指针 *T
或可选类型指针 ?*T
。
说明:把指针的自身值,转换为usize类型的值。
usize f35bfff8d4
语法:@intToPtr(comptime DestType:type,address:usize) DestType
说明:将整数转换为指针。
如果 DestType 是允许地值为0的类型,则整数0可正常转换。
如果 DestType 是可选类型,则整数0可正常转换为null。
如果 DestType 是不允许地值为0的非可选类型,转换整数0将引发未定义行为。
error: pointer type '*i32' does not allow address zero
参见4.4.3.3. @alignCast。
@addrSpaceCast(comptime addrspace:std.builtin.AddressSpace, ptr:anytype) anytype
将指针从一个地址空间转换为另一个地址空间。
根据当前目标和地址空间的不同,此强制转换可能是无操作、复杂操作或非法的。如果强制转换是合法的,则生成的指针指向与指针操作数相同的内存位置。在相同的地址空间之间强制转换指针始终是有效的。
@bitCast(comptime DestType:type,value:anytype) DestType
操作数类型:
说明:将一种类型的值转换为另一种类型。
断言:@sizeOf(@TypeOf(value)) == @sizeOf(DestType)
断言:@typeInfo(DestType) != .Pointer
。因为针对指针,可以用@ptrCast和@intToPtr。
如果value在编译时可知,则在编译时计算。
对未定义布局的值进行位转换@bitcast是一个编译错误。这意味着,除了具有专用的转换内置函数(枚举、指针、错误集)的类型的限制之外,裸结构、错误联合、切片、选项以及任何其他没有明确定义的内存布局的类型也不能用于此操作。
可以用于类似于:
比特值不变,f32转换为u32, i32转换为u32等。
当表达式有多个操作数时,成对类型解析选择多个操作数都可以强转的类型做为结果类型。
if, while, 表达式中,条件表达式的结果值必须是 bool 类型,或最终载荷值是 bool 类型。
switch,for 表达式中,条件表达式的结果值必须是普通类型,不可以是可选类型、错误联合类型、错误联合可选类型等。
10.1 表达式和语句(expression and statement)
在Zig语言中,if, switch, while, for, break, continue等本质上是表达式,表达式的结果值如果是非 void ,必须要赋值给其它变量;表达式结果是 void ,就是语句。
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}
while表达式基本语法:
while(condition) {block}
说明:当条件表达式为真时,运行语句块。
用 break 可中途退出循环。
break 不影响 defer 。
用带标签的 break 可跳出当前块,并可使块表达式有值,参见3.5. 块(block)
用 continue 可跳到循环开始处,继续循环。
step表达式主要用于循环变量递增或递减;循环体内continue语句生效时,也要运行step表达式。
语法:while (cond) : (step) {block};
执行顺序类似于如下伪代码:
LOOP: if(cond==true) {block; step; goto LOOP;};
循环内有continue时的语法:
while (cond) : (step) {block1; if(exp) continue; block2;}
执行顺序类似于如下伪代码:
如果有多个变量需要步进,步进表达式需要用({stepa;stepb;}),因为{}是语句块表达式,而stepa是语句。
while的条件表达式为 false 时,如果有 else 子句,则执行 else 子句后退出。
break 可以带返回值退出当前while,用 break 退出循环时,不执行 else 子句。
for表达式基本语法:
for(container) |v| {block}
for循环是对数组、切片、结构等容器进行遍历。
container必须是容器类型,不可以是可选类型、错误联合类型、错误联合可选类型等。
for循环必须要有捕获。
for表达式与while表达式类似,也可用break和continue语句。
for表达式与while表达式类似,也可用else子句。
当遍历正常结束时运行else子句,break时则不运行。
语法:
说明:
- 条件表达式的值必须是普通类型,不可以是可选类型、错误联合类型、错误联合可选类型等
- 根据条件表达式的值,进入符合条件的分支运行。如果没有符合条件的分支,则进入 else 分支运行
- 分支不能 fallthrough ,即当前分支运行结束后,不会进入下一个分支继续运行,而是退出switch表达式
- 进入分支后可以是任意复杂的语句块表达式,如下例中 101 分支
- switch表达式的结果值是进入分支后,表达式的值
- 多个实例用 ',' 分隔,如下例中 1,2,3 是指a的值可以是 1,2,3中的一个
- 可以用 a...b 表示a<=值<=b,如下例中 5...100 是 5<=a<=100
- 分支选择可以是编译期可知的任意复杂的表达式,如下例中的 zz 和其下一个分支的判断条件
switch 语句的 else 分支通常是必须的,见上一节示例。
如果所有实例都有对应分支,则else分支可以省略。
如果分支不包含所有实例时,没有else分支,则会产生编译错误。
test_not_else.zig:8:4: error: switch must handle all possibilities
switch的判断表达式是枚举或标记联合时,在分支判断处可省略类型。
在同一个分支上,可以包括相同类型的联合属性,如本例中的 .a1 和 .a3。
捕获是指在选择、循环或遍历期间,获得条件表达式(或容器元素)的值。
捕获得到的值的作用域仅用于流程控制语句对应的语句块。
捕获语法为:
-
|v|
捕获值
-
|v,i|
捕获值和索引
-
|*v|
捕获指针,可修改被捕获变量的值
-
|_|
捕获值无用,丢弃捕获值
当条件表达式的值是普通类型时,if, while 表达式不能捕获。
switch表达式捕获是可选的,是在选定分支,进入对应表达式后捕获。
for表达式必须要有捕获,且仅for表达式可捕获索引。
if, while 的条件表达式结果值是可选类型时,else子句不能省略。
如果条件表达式的值为 null ,则进入 else 子句。
if, while 表达式必须要有捕获,捕获得到载荷值。
else 子句不能有捕获。
while 表达式的捕获表达式须放在步进表达式之前。
if, while 的条件表达式结果值是错误联合类型时,else子句不能省略。
如果条件表达式的值为错误值 ,则进入 else 子句。
if, while 表达式必须要有捕获,捕获得到载荷值。
else 子句也必须要有捕获,捕获得到错误值。
while 表达式的捕获表达式须放在步进表达式之前。
if, while 的条件表达式结果值是错误联合可选类型时,else子句不能省略。
如果条件表达式的值为错误值 ,则进入 else 子句。
if, while 表达式必须要有捕获,捕获得到可选类型值。
else 子句也必须要有捕获,捕获得到错误值。
while 表达式的捕获表达式须放在步进表达式之前。
内嵌循环的带标签的break 或 continue语句,可跳转至对应的外层带标签while语句。
与while类似,for也可带标签。
如果if语句的条件表达式的值是编译期可知,那么编译期能确定的不可能进入的分支流程不会生成二进制代码。
switch表达式可以在函数外部用来给变量赋值,如下例中的os_msg赋值语句。
在函数内部,如果判断表达式的值是编译期可知的,switch语句则默认是在编译期计算。
在本例中,builtin.target.os.tag 的值是编译期可知的,所以如果在非fuchsia操作系统上,编译时 .fuchsia 分支块不会被解析,其分支内@compileError语句不会执行;如果在fuchsia操作系统上,则 .fuchsia语句块被解析,@compileError执行。
switch表达式中的分支加 inline 标记,表示编译时展开该分支所有可能的值。
本例中,当isfieldopt函数的参数T的值是 struct1 时,其inline分支,编译时等同于isfiledopt_roll函数的分支0和分支1;
编译后,等同于isfiledopt_struct1函数。
内联 While 循环会展开循环体,主要用于在编译期执行循环,如本例中把类型做为值。
与while类似,for也可inline。
用Debug和ReleaseSafe编译模式,则当执行到unreachable语句时,程序会崩溃,显示 reached unreachable code.
用ReleaseFastReleaseSmall模式,优化器根据unreachable,对假设的不可达代码优化。
std.debug.assert的实现如下:
1/1 test.this will fail... thread 142805 panic: reached unreachable code
unreachable的类型是noreturn,但本例无法编译,因为执行到内有unreachable的语句,是编译错误。
error: unreachable code
Zig语言的重点之一就是表达式在编译期是否可知的concept,这有助于程序更快、更可读和更强大。
编译期参数是编译期鸭子类型,以实现泛型。
在Zig语言中,类型可以分配给变量,做为函数的参数值和返回值。但类型只能在编译期可知的表达式中使用,所以本例中参数 T 必须用comptime修饰。
comptime参数是指:在调用处,必须在编译期可知参数值;在函数定义中,参数值在编译期可知。下例编译出错,因为试图将仅在运行期可知的值cond,传递给编译期参数T。
如果在解析函数参数的类型值时,进行非法操作也会出错。因为bool类型不能比较大小,所以下例编译时出错。
上例的代码改进后,测试通过。
if表达式的条件如果是编译期可知的,则编译时所有处理编译期可知的代码被删除,保证跳过未采用的分支。本例中max函数实际生成为:
fn max(a:bool,b:bool)bool{ return a or b;}
对switch表达式也是如此。
名词解释:
鸭子类型(:duck type) 英文俗语有:当一只鸟走路像鸭子,游水像鸭子,叫声像鸭子,那它就是鸭子。鸭子类型是可以简单理解为用各种语法把类型做为值,编译期动态确定运行逻辑;而不是用类继承体系来实现。
变量标记为comptime,保证编译器在编译期对变量读写。例如可以编译一部分在编译期计算、一部分在运行期计算的函数。
这个例子主要展示编译期变量用法,全部在运行期也能正常运行。这个示例针对不同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/)
用comptime表达式来保证在编译期计算。本例中,编译期调用exit函数(或任何其它外部函数)无意义,所以编译错误。
test_comptime_err.zig:3:19: error: comptime call of extern function
在comptime表达式内:
所有变量是comtime变量;
所有if, while, for, switch表达式均在编译期计算;
所有函数调用是在编译期解析,如果函数试图运行有全局副作用影响的代码,则编译错误。
这样,可以创建同一个函数,即可在编译期被调用,也可在运行期被调用。
本例中省略fib函数中的初始值判断,编译时出错。
test_fib_err.zig:4:17: error: overflow of integer type 'u32' with value '-1'
上例用的是无符号整数,这样试图0-1时,触发未定义行为,如果编译期可知,则是编译错误。
如果是下例中的有符号整数,编译器在编译期计算花费很多时间,则放弃并产生编译错误。
如果想增加编译预期时间,用
@setEvalBranchQuota函数将1000改为其它值。
如果fib函数正确,而expect函数中期望值错误,如下例:
容器级下(任何函数之外),任何表达式都是编译期表达式。这样可以用函数来初始化复杂的静态数据。
编译器会用事先计算好的结果来生成常量,下面是生成的LLVM IR中的代码行:
这里,我们不需要对函数语法做任何特殊设置,就像调用参数是长度和值仅运行期可知的切片的sum函数。
只要源代码不依赖于未定义的内存布局,指针也可以在编译时工作。
只要指针没有被解引用,在编译期代码中也可保存内存地址。
基于编译期计算可实现泛型。为保持语言小巧和一致,所有聚合类型是匿名的。为了错误信息和调试,从创建匿名结构时调用的函数名和参数推断出名称 List(i32)。
把类型赋值给常量,常量名即为类型名,如本例中ListInt。
名词解释:
泛型(:generic) 没有泛型功能的函数,其参数类型一经定义不可变。有泛型的函数,本质上是把参数的类型当成编译期可知的变量。当参数类型在调用函数的源代码中给定后,实例化到给定的类型。每个实例化都生成不同的目标代码。滥用泛型会导致代码膨胀。
@setEvalBranchQuota(comptime new_quota:u32)
说明:在放弃并产生编译错误之前,更改编译期代码执行可以使用的最大回退分支数。
如果 new_quota 小于默认值(1000)或先前明确设置,则忽略它。
示例
用@setEvalBranchQuota后,则测试通过。
@compileError(comptime msg:[]u8)
语义分析时,引发编译错误并显示 msg。
用条件表达式是编译期常量的if或switch语句,以及comptime函数,来避免对代码进行语义分析。
@compileLog(args:...)
显示输出在编译时传递给它的参数
为防止 @compileLog 遗留在代码库中,构建时 @compileLog引发编译错误,这样可以防止生成代码,但不影响解析。
如果删除所有 @compileLog 调用,或者解析没有遇到这些调用,程序就会成功编译,并测试通过。
反射是指在编译期或运行期,获取变量和类型的信息。
可得到函数定义的相关信息。
@src() std.builtin.SourceLocation
返回结构值,表示函数在源代码中的名称和位置。@src必须在函数中调用。
可取得枚举属性个数、名字、标签类型等信息。
用std.meta.Tag函数可得到标记联合的标记类型。
用@tagName函数可得到标记联合的标记名。
可选类型可在编译期反射,得到其正常值类型。
可在编译时得到错误联合类型的类型信息。
获知指针指向类型。
@Type(comptime info:std.builtin.Type) type
说明:是@typeInfo的逆操作,它将类型信息具体化为一个类型。
对函数,BoundFn无效。
示例:
@typeInfo(comptime T:type) std.builtin.Type
说明:提供类型反射。
结构、联合、枚举、错误集的类型信息的属性保证与源文件中的外观顺序相同。
示例:
@TypeOf(...) type
说明:这是一个特殊的内置函数,它接受任意(非零)数量的表达式作为参数,并使用对等类型解析返回结果的类型。
这些表达式是可以计算的,但是它们保证不会产生运行时副作用。
示例:
@typeName(T:type) *const [N:0]u8
说明:以数组的形式返回类型的字符串表示。它等效于类型名称的字符串字面值。返回的类型名称是含有一系列点操作符的类型名。
@errorName(err:anyerror) [:0]const u8
说明:返回错误的字符串表示。
如果整个程序没有调用@errorName,或者所有@errorName调用的 err 参数都是编译期可知的,则不会生成错误名称表。
示例:
@tagName(value:anytype) [:0]const u8
说明:把枚举或联合的值转换为其名字的字符串字面值。
如果枚举是非穷举的,且标记值没有映射名字,则调用安全检查未定义行为。
示例:
@field(lhs:anytype,comptime field_name:[]const u8) (field)
说明:通过编译时的字符串访问内部属性和内部定义变量。
@hasField(comptime Container:type,comptime name:[]const u8) bool
说明:返回结构、枚举、联合内,是否有与 name 匹配的属性。
不检查是否匹配函数、变量或常量。
结果是编译期常数。
@hasDecl(comptime Container:type,comptime name:[]const u8) bool
说明:返回结构、枚举、联合内,是否有与 name 匹配的定义变量。
不检查是否匹配属性。
如果 @hasDecl 调用是在另一个文件,则返回 false。
示例:
@sizeOf(comptime T:type) comptime_int
说明:返回在内存中保存 T 所需的字节数。结果是一个特定目标平台的编译期常数。
结果值可能包含填充字节。
如果内存中有两个连续的 T,结果值是位于索引0的元素和位于索引1的元素之间,以字节为单位的偏移量。
对于整数,分情况使用@sizeOf(T) 或 @typeInfo(T).Int.bits.
此函数在运行时测量字节位长。对于运行时不允许的类型,如 comptime_int 和 type,结果是0。
@alignOf(comptime T:type) comptime_int
说明:返回当前目标平台的C语言ABI下,该类型应对齐的字节数。结果是特定目标平台的编译期常数,保证小于等于 @sizeOf(T)。
当指针指向数据的类型与此字节数相等时,可以在指针定义中省略对齐方式。
示例:
@offsetOf(comptime T:type,comptime field_name:[]const u8) comptime_int
说明:返回结构内属性的字节偏移量。
示例:
@bitOffsetOf(comptime T:type,comptime field_name:[]const u8) comptime_int
说明:返回结构内属性的位偏移量。
对非压缩结构,总可以被8整除;对压缩结构,未按字节对齐的字段共享字节偏移量,但位偏移量不一样。
@bitSizeOf(comptime T:type) comptime_int
对压缩结构或压缩联合的属性,则返回在内存中保存 T 所需的比特位数。结果是一个特定目标平台的编译期常数。
此函数在运行时测量比特位长,对于运行时不允许的类型,如 comptime_int 和 type,结果是0。
@This() type
说明:返回此函数调用所在的最内层结构、枚举和联合。这对于需要引用自身的匿名结构非常有用。
在文件作用域使用@This()时,返回对当前文件的结构的引用。
未定义行为简称为UB,是指源代码符合语法规范,但真实运行逻辑有错,会产生不可预料的行为。
例如下面的代码均符合语法规范,但明显逻辑有错。
在编译期检测到未定义行为,会产生编译错误并退出编译。
当有安全检查时,大多数编译期检测不到的未定义行为,都可以在运行期检测到。
如果安全检查发现未定义行为,通常会崩溃退出,并产生栈追踪。
用@setRuntimeSafety可以在语句块禁用安全检查。
为了优化,ReleaseFast和ReleaseSmall构建模式禁用安全检查(内含@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;
}
}
注意: 将来计划将 @setRuntimeSafety 替换为 @OptimizeFor
@panic(message:[]const u8) noreturn
调用崩溃处理函数。
崩溃处理函数默认调用根代码文件中公开的 panic 函数,如果没有指定,则从 std/builtin.zig 中调用 std.builtin.default_panic 函数。
通常建议使用@import("std").debug.panic。然而,@panic可以用于下列两种情况:
从库代码中,如果程序员的崩溃函数在根代码文件中是公开的,则调用@panic;
混合C语言源代码和Zig语言源代码时,跨多个.o文件的标准panic实现。
编译时:
error: reached unreachable code
运行时:
Test [1/1] test.unreachable UB... thread 2696 panic: reached unreachable code
编译时:
error: index 5 outside array of length 5
运行时:
Test [1/1] test.index out of bounds UB... thread 8924 panic: index out of bounds: index 5, len 5
编译时:
error: type 'u32' cannot represent integer value '-1'
运行时:
Test [1/1] test.cast negative number to unsigned number... thread 6620 panic: attempt to cast negative value to unsigned integer
为了截断多余位,应该用@truncate函数。
编译时:
error: type 'i8' cannot represent integer value '300'
运行时:
Test [1/1] test.cast truncates data... thread 3200 panic: integer cast truncated bits
+ 加, - 减, - 取负值, * 乘,
/ @divTrunc @divFloor @divExact 除
上面的运算符都可以引发整数溢出。
编译时:
error: overflow of integer type 'u8' with value '257'
运行时:
Test [1/1] test.integer overflow... thread 13532 panic: integer overflow
标准库的下列函数可能会返回错误:
@import("std").math.add sub mul divTrunc divFloor divExact shl
本例抓取加法溢出。
下列内置函数返回bool值表示是否有溢出,同时在输入参数以指针方式返回溢出后数值。
@addWithOverflow, @subWithOverflow, @mulWithOverflow, @shlWithOverflow
本例测试通过。
+% 回绕加, -% 回绕减, -% 回绕取负值, *% 回绕乘
上面的运算是回绕运算。
和普通左移溢出的区别是,只要移出的位有bit 1就是溢出。
编译时:
error: operation caused overflow
运行时
Test [1/1] test.exact left shift overflow... thread 14132 panic: left shift overflowed bits
和普通右移溢出的区别是,只要移出的位有bit 1就是溢出。
编译时:
error: exact shift shifted out 1 bits
运行时:
Test [1/1] test.exact right shift overflow... thread 7584 panic: right shift overflowed bits
编译时:
error: division by zero here causes undefined behavior
运行时:
Test [1/1] test.division by zero... thread 10292 panic: division by zero
除法运算有余数则出错。
编译时:
error: exact division produced remainder
运行时:
Test [1/1] test.exact divisioin reaminder... thread 8568 panic: exact division produced remainder
13.3.10 试图解包裹null(attempt to unwrap null)
编译时:
error: unable to unwrap null
运行时:
Test [1/1] test.attempt to unwrap null... thread 18204 panic: attempt to use null value
编译时:
error: caught unexpected error 'notint'
运行时:
Test [1/1] test.attempt to unwrap error... thread 3168 panic: attempt to unwrap error: notint
编译时:
error: integer value '11' represents no error
运行时:
Test [1/1] test.invalid error code... thread 13012 panic: invalid error code
编译时:
error: enum 'test_cub13.foo' has no tag with value '3'
运行时:
Test [1/1] test.invalid enum cast... thread 15908 panic: invalid enum value
编译时:
error: 'error.B' not a member of error set 'error{A,C}'
运行时:
Test [1/1] test.invalid error set cast... thread 7312 panic: invalid error code
编译时:
error: pointer address 0x1 is not aligned to 4 bytes
运行时:
Test [1/1] test.incorrect pointer alignment... thread 15972 panic: incorrect alignment
编译时:
error: access of union field 'u' while field 'i' is active
运行时:
Test [1/1] test.wrong union field access... thread 18668 panic: access of inactive union field
对extern union 和 packed union,这个安全检查无效。
改变union的激活属性可以用指针或undefined,下例可测试通过。
编译时:
error: float value '2567' cannot be stored in integer type 'u8'
运行时:
Test [1/1] test.out of bounds float to integer cast... thread 17024 panic: integer part of floating point value out of bounds
这个未定义行为发生在将地址值为0的指针类型转换为不可能地址值为0的指针时。
允许地址值为0的指针类型有,C指针,optional指针,和allowzero指针。
编译时:
error: null pointer casted to type *i32
运行时:
Test [1/1] test.pointer cast invalid null... thread 1472 panic: cast causes pointer to be null
Zig语言没有运行时虚拟机和垃圾收集,不替程序员进行自动内存管理。这是Zig语言代码可以适应多种平台(例如包括实时软件、操作系统内核、和低延迟服务器等)的原因之一。
在Zig语言中通常不设默认内存分配器,每个需要内存的函数都要有Allocator参数来指定分配器。
通常分配器需要调用init函数来初始化,然后调用allocator函数返回具体的分配器变量,再用分配器变量去申请释放管理内存。
GeneralPurposeAllocator 是有许多内存安全保障措施的分配器。
deinit函数返回为true时,表示有内存泄漏。
FixedBufferAllocator 不涉及任何堆分配,是把内存某片固定区域用做内存分配。
FixedBufferAllocator用完后,不需要deinit函数。
缓冲区根据分配用途,预估对齐值。
ArenaAllocator初始化参数,需要把另一种分配器做为该分配器的基层分配器。
用ArenaAllocator来分配内存,可以多次分配不用每次释放,待调用deinit函数时全部释放。
page_allocator 每次内存分配均要进行系统调用(可能是mmap),实际上是以页面为单位分配。
page_allocator 是最基础的分配器,如不能深刻理解其用途,尽量不要用这个。
page_allocator 不用调用init函数,也不用调用deinit函数,也不用调用allocator函数,可直接使用。
c_allocator 是libc库的分配器,相当于直接调用C语言库中的malloc函数来分配内存。用这个分配器须链接libc库。
c_allocator 不用调用init函数,也不用调用deinit函数,也不用调用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。
分配器的选用取决于许多因素。建议的选择流程如下:
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。
分配器上常用的内存分配函数如下,如果返回的是错误联合类型,调用时记得用 try :
-
alloc(T,n:usize) ![]T
分配n个T类型的内存
-
realloc([]T,new_nusize) ![]T
在原切片的基础上,改变长度为new_n个元素。new_n==0表示释放内存
-
free([]T)
释放内存,每个alloc均需要有对应的free
-
create(T) !*T
分配1个T类型的内存
-
destroy(*T)
释放内存,每个create均需要有对应的destroy
-
dupe(T,m:[]const T) ![]T
申请内存并复制m
-
dupeZ(T,m:[]const T) ![:0]T
申请内存并复制0结尾的m
许多编程语言用无条件崩溃来处理堆分配失败。Zig语言中,当堆分配失败时,Zig库返回error.OutOfMemory。
由于某些操作系统默认启用了内存overcommit,有些人认为处理堆分配失败毫无意义。这种想法存在许多问题:
只有一些操作系统具有overcommit。Linux默认启用且可配置;Windows不会overcommit;嵌入式系统不会overcommit;Hobby操作系统可能有也可能没有。
实时系统没有overcommit,且应用程序的最大内存量是提前确定的。
写库的主要目标之一是代码重用,正确处理分配失败,库就可能多的被重用。
overcommit是许多用户体验极差的原因。当启用overcommit(如Linux)接近内存耗尽时,OOM会不确定的杀掉某些进程,这就经常导致某个重要的进程被终止,并且无法使系统恢复到工作状态。
@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]);
@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);
@prefetch(ptr:anytype,comptime options:std.builtin.PrefetchOptions)
如果目标平台CPU支持,则告知编译器发出预取指令。不支持,则无操作。
此函数对程序的运行逻辑没有影响,只对性能有影响。
ptr 参数可以是任何指针类型,确定了要预取的内存地址。此函数不会对指针解引用,将指向无效内存的指针传给此函数是完全合法的,不会导致任何未定义行为。
options参数是一个struct,Zig语言代码生成的这个struct必须和编译器实现保持同步。
rw属性:明确是为了读还是为了写预取。
locality属性:为0,表示没有临时用处,数据可以在访问缓存后立即从缓存删除;为3,表示有高的时间局部性,数据应一直保存在缓存中,因为可能很快再次被访问。
cache属性:要执行预取缓存
@wasmMemorySize(index:u32) u32
返回由index标识的Wasm内存大小,单位为Wasm页,每个Wasm页的大小为64KB。
这个函数是个低级别的内在函数,没有安全机制,通常对以Wasm为目标的分配器设计者有用。因此,除非是从头开始编写新的分配器,否则用@import("std").heap.WasmPageAllocator之类操作。
@wasmMemoryGrow(index:u32,delta:u32) i32
以无符号的Wasm页面数为单位,按 delta 递增 index 标识的Wasm内存大小,每个Wasm页的大小为64KB。
成功时,返回以前的内存大小;失败时,如果分配失败,则返回-1。
这个函数是个低级别的内在函数,没有安全机制,通常对以Wasm为目标的分配器设计者有用。因此,除非是从头开始编写新的分配器,否则用@import("std").heap.WasmPageAllocator之类操作。
std.fs下的Dir和File模块负责文件读写。
目录的主要操作如下,使用时,注意用 try 或 catch :
-
Dir.openDir(dirname,Opt) !Dir
打开目录,Opt可以是 .{}
-
Dir.makeDir(dirname) !void
建立子目录
-
Dir.deleteTree(dirname) !void
删除子目录,删除时子目录可以非空
-
Dir.close() void
关闭子目录
-
std.fs.cwd() Dir
返回当前目录
-
Dir.openIterableDir(dirname,Opt) !Dir
为了遍历打开目录,Opt可以是 .{}
-
Dir.iterate() Iterator
返回目录遍历器
-
Iterator.next() !?Entry
遍历下一个
遍历捕获的Entry结构为:
const Entry={name:const []u8, kind:File.Kind};
示例
文件的主要操作如下,使用时,注意用 try 或 catch :
-
Dir.createFile(filename,CreateFlags) !File
新建文件 CreateFlags=struct{read:bool=false,truncate:bool=true,...}
-
Dir.openFile(filename,OpenFlags) !File
打开文件 OpenFlags=struct{mode:OpenMode=.read_only,...}; OpenMode=enum{read_only,write_only,read_write}
-
file.close() void
关闭文件
-
Dir.deleteFile(name:[]const u8) !void
删除文件
-
file.seekBy(offset:i64) !void
读写定位从当前移动offset个字节。正数往尾部移动,负数往头部移动
-
file.seekTo(offset:i64) !void
读写定位从头部移动offset个字节。
-
file.seekFromEnd(offset:i64) !void
读写定位从尾部移动offset个字节。
-
file.readToEndAlloc(alloc:Allocator,max_bytes:usize) ![]u8
从当前读写定位读到文件尾,注意调用方释放内存
-
file.readAll(buffer:[]u8) !usize
读取切片大小字节,返回读取个数。
-
file.writeAll(buffer:[]const u8) !void
写入切片大小字节。
@embedFile(comptime path:[]const u8) *const [N:0]u8
返回编译期常量指针,指向以null结尾固定长度的数组,长度等于path对应文件的字节数,数组内容是文件内容。
相当于是包含文件内容的字符串字面值。
path是当前文件的绝对或相对路径,类似于@import。
示例:
在当前目录上,建立文件,文件名:embed.txt 文件内容:hello
hello
这里只介绍改名和取文件信息操作如下,其它操作详看标准库的相关文档。
-
Dir.rename(oldname,newname) !void
改名
-
Dir.statFile(filename) !Stat
给定文件名获取文件信息
-
Dir.stat() !Stat
获取目录信息
-
File.stat() !Stat
获取文件信息
Stat 结构 主要属性有 .size .atime .mtime .ctime
在C语言标准库和unix类操作系统中:
- stdin 标准输入流,通常是指键盘
- stdout 标准输出流,通常是指命令行窗口
- stderr 标准错误流,通常是指命令行窗口
- stdin, stdout 通常可重定向到文件、管道或其它
- stdin, stdout 默认行缓冲,意思是遇到 \n 才结束输入或把输出显示到屏幕
- stderr 无缓冲,直接显示输出
本例在屏幕上键入名字,并输出显示
std库中提供了从字符串中获得整数和浮点数的函数,如下:
-
parseFloat(T:type,comptime T:type,s:[]const u8) !T
根据输入字符串获得浮点数
-
parseInt(comptime T:type,buf:[]const u8,radix:u8) !T
根据输入字符串获得整数
radix的参数含义为:10是分析十进制数字符串,0是分析0b 0o 0x 开头的其它进制字符串。
在格式串中,用{}表示需要格式化输出参数,格式串形式是:
{[参数序号][输出类型]:[填充字符][对齐方式][总宽度].[小数位数]}
其中,
- 参数序号:指描述的是第几个参数,从0开始
- 填充字符:ASCII字符
- 对齐方式:
<
左对齐, >
右对齐, ^
居中对齐
输出类型是:
格式字符 |
输出类型 |
d |
十进制(整数默认值) |
b |
二进制 |
o |
八进制 |
x X |
十六进制,x是输出小写字母,X是输出大小字母 |
e |
科学计数法浮点数 |
c |
把整数做为ASCII字符输出,整数最大8位长 |
u |
把整数做为UTF-8序列输出,整数最大必须有21位 |
s |
字符串,对应参数是0结尾的多项指针或u8类型c指针,或u8类型切片 |
* |
值的地址 |
any |
默认格式输出,参数可以是任何类型 |
? |
解包裹后的值或null,和上列类型符组合 |
! |
解包裹后的值或错误值,和上列类型符组合 |
要输出 {
在格式串中用 {{
,}
用 }}
随着计算机速度越来越快,核心数越来越多,为了最大限度压榨硬件性能,近些年来编程语言在多进程、多线程、协程(可视做用户级多线程)的相关功能方面越来越丰富。
这就需要引入
- 原子操作或锁,来进行多进程、多线程、协程间的数据同步
- 引入异步函数,主要用来多线程或协程的运行流程切换
普通函数是不能在运行中途跳出,运行其它函数中语句后,再回来继续运行的。所以普通函数又称为同步函数。
例如下面的伪代码,在main调用fa时中途返回再中途进入,在绝大多数编程语言(可以说是所有)中是不能正常编译或运行的。
函数运行中间可以挂起,条件满足时可以在挂起处继续恢复运行,这种函数称之为异步函数。
在普通函数调用期间,如果中途退出回来继续运行,则栈帧会乱掉,程序会跑飞。普通函数调用过程基本上流程是:
- 系统在调用函数前的前期工作:在程序主栈上,填充返回地址、参数值等内容,构成该函数的栈帧。
- 函数运行,在栈帧上存放局部变量,如有必要在全局堆上申请内存。
- 系统在函数运行结束的后续工作:在程序主栈上填充返回值,然后把该函数的栈帧弹出,恢复到函数调用处继续运行。
异步函数调用流程基本是:
- 系统在调用异步函数前的前期工作:构成栈帧。
- 异步函数运行。
- 运行期间异步函数要挂起,系统保存现场(也就是当前栈帧),记好恢复点,退出函数继续运行。
- 在其它地方要恢复异步函数运行,则还原现场(就是恢复当前栈帧),运行转到恢复点继续执行异步函数剩余部分,实质上等同于新发起一个函数调用,只不过调用这个函数的数据是异步函数保存下来的现场数据。
- 异步函数正常退出,弹出栈帧,在异步函数恢复处继续运行。
在函数的内部用 suspend 关键字来修饰语句块,表示在此处挂起。
以异步方式调用函数时,须用 async 关键字修饰,返回类型是本函数栈帧,返回值是本函数的挂起栈帧值。
本例中,异步调用foo函数,x加1变为2后,到suspend 挂起点,程序中途退出,异步调用的退出值是fr,其类型是 foo 函数栈帧。
所以此时x==2成立。
注意:foo函数栈帧是类型,因为可能在不同处调用foo函数,则foo函数的栈帧的具体值是不一定相同的。
另异步函数编译需要加 -fstage1
参数
名词解释:
进程调度(:process scheduling) 操作系统中进程通常有新建(:create)、就绪(:ready)、阻塞(:wait)、运行(:run)、终止(:exit)五个状态。从运行到就绪为挂起,从就绪到运行为恢复。协程相关方面概念与此类似。
在异步函数内或调用异步函数的流程上,用 resume 关键字带一个栈帧类型的操作数,表示恢复到此栈帧对应的挂起点处,继续运行。
本例中,异步调用foo函数,r加1,函数挂起,此时r值为1,返回挂起处栈帧值fr。
主流程r加1后,r值为2。
resume 的操作数是fr,表示从foo函数挂起处继续执行后正常返回,此时r值为12。
可以在suspend语句块中赋挂起栈帧指针,用resume恢复。
本例中,从挂起块中用@frame取得挂起处栈帧指针。
通过输出显示内容,可以直观的看到挂起和恢复的运行流程。
注意一点是挂起块不是在@frame函数调用处挂起的,因为语句块也是表达式,所以是在挂起块运行完后(即语句块表达式计算完后)才挂起的,可以从 ffff3 即可得知。
可以在挂起块内用resume恢复挂起。
如果想把内有 suspend 块的异步函数当普通函数使用,则调用时加 nosuspend 关键字。
此外,该异步函数须保证当普通函数使用时,运行流程不会进入挂起点。假如把本例中
const j=nosuspend foo(false);
改为
const j=nosuspend foo(true);
则程序运行崩溃,输出信息为:
thread 17440 panic: async function called in nosuspend scope suspended
通常异步函数由 async 启动,其 async 的返回值做为 await 表达式的操作数。
await 的操作数是 anyframe->T
类型,T是异步函数的返回类型。
await 表达式一直等到对应的 async 函数运行结束,将 async 函数的返回值做为表达式的值。
然后程序继续执行。
请仔细阅读本示例中的输出信息,借以理解 await 的运行流程。
将 run_await.zig中 的 main 函数改为如下,交换一下resume frfoo1 和 frfoo2的顺序,其它行不变。
则程序输出变为:
因为await fr1表达式在前,所以尽管frfoo2先恢复,即先输出 dddd ,也要等到 frfoo1 运行完能够得到返回值后,程序才继续运行。
一般来说,大多数应用程序代码将只使用 async 和 await ,事件循环等偏底层应用将在内部使用 suspend 。
17.6 异步和栈帧相关内置函数(async and frame builtin function)
@frame() *@Frame(func)
返回指向给定函数栈帧的指针。其类型可强转为 anyframe->T 或 anyframe ,T是作用域中函数的返回类型。
这个函数不标记挂起点,但确实会使作用域内的函数变成异步函数。
@Frame(func:anytype) type
返回 func 的栈帧类型,适用于异步函数和没有特定调用约定的函数。
此类型适合用作async的返回类型。例如,它允许堆分配一个async函数帧:
@frameAddress() usize
返回当前栈帧的基指针。
由目标平台确定,并不是所有目标平台都一致,由于优化,栈帧地址在release模式下可能不可用。
此函数仅在函数作用域内有效。
@frameSize(func:anytype) usize
等同于@sizeOf(@Frame(func)),fnuc可能是运行期可知。
此函数通常与@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中复制结果。
@returnAddress() usize
返回当前函数返回时将执行的下一个机器代码指令地址。
这个结果是针对特定目标平台,并不是所有目标平台都一致。
此函数仅在函数范围内有效。如果函数内联到调用函数中,则返回的地址将应用于调用函数。
多进程、多线程、协程间的数据同步,需要共享锁。
传统的共享锁实现复杂,运行代价重,适合于严格数据同步。
现代大部分桌面和服务器CPU,支持原子操作,可以实现轻量级的数据同步,俗称无锁数据结构。
因Zig语言这部分还在继续完善,此节仅罗列内置函数,其余暂时不写了。
@atomicLoad(comptime T:type,ptr:*const T,comptime ordering:builtin.AtomicOrder) T
原子解引用并返回值。
T 必须是指针, bool, 浮点数,整数,enum。
@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小,则保存它。支持整数和浮点数。
@atomicStore(comptime T:type,ptr:*T,value:T,comptime ordering:builtin.AtomicOrder) void
原子保存值。
@cmpxchgStrong(comptime T:type,ptr:*T,expected_value:T,new_value:T,success_order:AtomicOrder,fail_order:AtomicOrder) ?T
运行强原子比较交换操作,运行逻辑相当于下面没有原子操作的代码:
在循环中使用 cmpxchg,那么 @cmpxchgWeak 是更好的选择,因为可以在机器指令中更有效的实现。
T 必须是指针, bool, 浮点数,整数,enum。
@typeInfo(@TypeOf(ptr)).Pointer.alignment must be >= @sizeOf(T).
@cmpxchgWeak(comptime T:type, ptr:*T,expected_value:T,new_value:T,success_order:AtomicOrder,fail_order:AtomicOrder) ?T
运行弱引用原子比较交换操作,运行逻辑相当于下面没有原子操作的代码:
如果在循环中使用 cmpxchg,那么零星的故障将不成问题,而 cmpxchgWeak 是更好的选择,因为可以在机器指令中更有效的实现。
如果需要更强有力的保证,请使用 @cmpxchgStrong
T 必须是指针, bool, 浮点数,整数,enum。
@typeInfo(@TypeOf(ptr)).Pointer.alignment must be >= @sizeOf(T).
@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操作可见。
静态语言的程序构建流程基本是:
- 生成源代码:程序员编写源代码,生成文本格式的源代码文件,通常文件名后缀是 .c .zig .cpp
- 构建配置:配置构建参数,例如输出目录、链接库、优化等级等,构建参数通常保存为文本格式的 make 文件
- 编译:编译又分为:词法分析、语法分析、语义分析、生成中间代码、优化、生成指定平台(通常是当前平台)的二进制目标文件,通常文件名后缀是 .obj (Windows) 或 .o (unix类)。
- 链接:把编译生成的目标文件,链接静态或动态库文件,最终生成内有目标文件和部分静态库内容的二进制可执行程序文件或库文件,通常可执行程序文件名后缀是 .exe (Windows) ,unix类平台无后缀。静态库是二进制文件,通常文件名后缀是 .lib (Windows) 或 .a 。动态库也是二进制文件,通常文件名后缀是 .dll (Windows) 或 .so (unix类) 。
- 运行:运行程序时,操作系统通常生成新进程,把程序的可执行代码加载到进程的内存空间中。运行时根据需要可加载动态库。
对于有宏功能的程序设计语言,需要先在预处理器把宏文本替换,再开始正式编译。比如gcc可以在宏文本替换后生成 .i 文本文件。
编译过程通常以中间代码为界,又可分为前端和后端。前端是指源代码文件经过词法、语法、语义等分析,生成中间代码格式的文本或二进制格式文件,例如在LLVM中文件名后缀是 .ll 。
后端是指中间代码文件经过代码优化和链接优化,最终生成指定平台(通常是当前平台)的可执行程序或库。
共有4种构建模式: Debug (默认值),用于调试; ReleaseFast 目的是运行速度更快;ReleaseSafe 目的是更安全;ReleaseSmall 目的是生成二进制程序更小。
使用时在命令行输入与下面类似的命令,在 -O 选项后面输入构建模式:
$ zig build-exe exam.zig -O ReleaseFast
4种构建模式比较:
编译模式 |
编译速度 |
安全检查 |
生成程序大小 |
生成程序运行速度 |
Debug |
快 |
有 |
大 |
慢 |
ReleaseFast |
慢 |
无 |
大 |
快 |
ReleaseSafe |
慢 |
有 |
大 |
适中 |
ReleaseSmall |
慢 |
无 |
小 |
适中 |
用 zig build --help来查看构建系统命令行选项帮助。
$ zig init-exe
自动生成build.zig文件
文件中standardTargetOptions是构建目标平台选项,默认值是本机;
standardReleaseOptions是构建模式选项。
$ zig init-lib
自动生成build.zig文件
@export(declaration, comptime options: std.builtin.ExportOptions) void
declaration 必须是:函数或变量的标识符 (x) ;函数或变量的属性标识符 (x.y)
从 copmtime 块中调用 @export ,以有条件的导出符号。
当 declaration 是 C语言调用约定的函数,且 options.linkage 是 strong ,相当于在函数前用 export 关键字:
即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.
@extern(T: type, comptime options: std.builtin.ExternOptions) *T
在输出目标文件中创建对外部符号的引用。
@import(comptime path:[]u8) type
查找与path对应的zig源代码文件,如果还未添加则将其添加到构建中。
@import返回与文件对应的结构类型,其名称等于不包含扩展名的文件名称。
带pub关键字的定义可以从不同于其定义的源文件中引用。
path可以是相对路径,也可以是包的名称。如果是相对路径,则它相对于包含@import函数调用的文件。
在build.zig文件中,增加类似于本例的源代码。
编译选项可输入 --single-threaded,单线程构建是指:
所有线程本地变量当做静态变量处理;
异步函数的开销等同于普通函数调用开销;
@import("builtin").single_threaded 为 true,因此读这个变量的各种用户态API更高效。例如std.Mutex变成1个空数据结构,所有相关函数是空操作。
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 接口。
可导入 builtin 包访问编译期变量,其中包含了编译期常量,如当前平台、大小端模式等。
虽然Zig独立于C语言,且与大多数语言不同,不依赖于libc,但Zig语言与现有C代码交互还是很重要的。
2.1.5. C语言ABI兼容类型(c ABI compatible)保证与C语言ABI兼容,可以像其它类型一样在Zig源代码中使用。
与C语言void类型互操作,用 anyopaque 。
@cImport函数可直接从 .h 文件中导入符号。
@cImport函数将编译期求值的表达式作为参数,用于控制预处理器指令或包括多个 .h文件:
$ zig translate-c
把C语言源代码转译为Zig语言源代码。转译后的文件写入stdout。
-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代码的平台三元组信息,如未指定平台,则用当前主机平台信息。
用zig translate-c转译C语言代码时,必须和转译后的Zig代码的 -target 三元组相同。还必须确保使用的 -cflags 与目标平台上代码使用的 cflags相匹配。
用不正确的-target或-cflags可能会转译失败,或者在与C语言代码链接时会有ABI不兼容。
@cImport和zig translate-c使用相同的C语言转译方法,所以在技术层面上它们是等价的。
@cImport可以快速方便地访问数值常量、类型定义,不需要任何多余设定来记录类型,非常有用。
如果需要将cflags传递给clang,或者想编辑转译后的代码,建议使用zig translate-c,并将结果保存到文件中。
编辑转译后代码的常见原因有:将宏函数中的anytype类型参数更改为更具体的类型;将指针类型由 [*c]T 改为 [*]T 或 *T ,以提高类型安全性;在指定函数内用@setRuntimeSafety启用或禁用运行时安全。
C语言转译功能集成了Zig缓存系统,用相同源代码文件、cflags和目标平台的再次转译,将用缓存而不是重新转译。
使用 --verbose cImport 选项,可查看编译 @cImport 用到的缓存文件存储位置。
cimport.h包含要转译的文件(用@cInclude, @cDefine, @cUndef构造), cimport.h.d 是文件依赖项列表,cimport.zig是转译输出的Zig代码。
@cDefine(comptime name:[]u8,value)
仅在 @cImport内使用。
在 @cImport临时缓冲区追加:
#define $name $value
默认是不用value,类似于:
#define _GNU_SOURCE
用void,类似于:
@cDefine("_GNU_SOURCE",{})
@cImport(expression) type
解析C语言代码,并将函数、类型、变量和兼容宏定义,导入到1个新的空struct type中,并返回该type。
expression在编译时计算。内置函数 @cInclude, @cDefine, @cUndef在这个expression内运行,并追加输出到临时缓冲区。
通常在整个应用程序中应该只有一个@cImport,因为可以避免编译器多次调用clang,并防止重复内联函数。
有多个@cImport的理由为:
为了避免符号冲突,例如,如果 foo.h 和 bar.h 都
#define CONNECTION_COUNT
用不同的预处理器定义解析C语言源代码
@cInclude(comptime path: []u8)
仅在 @cImport内使用。
在 c_import 临时缓冲区追加:
#include <$path>\n
@cUndef(comptime name: []u8)
仅在 @cImport内使用。
在 c_import 临时缓冲区追加:
#undef $name
有些C语言构造无法转译为Zig代码,如goto, 位域, 粘贴宏。Zig用降级,来继续翻译。
降级有三种类型:-opaque, extern 和 @compileError。
不能正确转换的C struct 和union 被转译为 opaque{}。
包含不能转译的代码构造和 opaque 类型的函数,被降级为extern 定义。
因此,不可转译类型仍然可以用作指针,只要链接器清楚已编译的函数,就可以调用不可转译函数。
当顶级定义(全局变量,函数原型,宏)无法降级时,使用@compileError。
由于Zig对顶级定义使用延迟解析,所以不可转译的实体不会在代码中导致编译错误,除非实际使用它们。
无法转译的宏被降级为@compileError。使用宏的C语言代码可以正常转译,只有宏本身无法转译为Zig语言。
下例中,尽管无法转译宏,但代码还是被正确转译。因为MAKELOCAL宏不能转译为Zig语言函数,所以被降级为 @compileError。
仅在转译自动生成的代码中使用C指针,除此之外尽量避免使用。
导入C头文件时,C指针往往不能明确转换为单项指针(*T)还是多项指针([*]T)。所以C指针是一种折衷方案,Zig代码可以用转译过的头文件。
[*c]T C指针。
- 支持其他两种指针类型的所有语法。
- 就和 optional 指针一样,强转为其它类型指针。当C指针强转为可选类型指针时,如果地址值为0,会发生安全检查的未定义行为。
- 允许地址值为0。在非裸机目标平台上,解引用地址值为0是安全检查的未定义行为。可选类型C指针引入其它比特位来追踪null,用起来就像 ?usize。注意没有必要创建可选类型 C指针,因为可以使用普通的可选类型指针。
- 支持和整数间的类型强转。
- 支持和整数比较。
- 不支持对齐等Zig语言独有的指针属性。请使用普通指针。
C指针指向单个struct(而不是array)时,解引用来访问结构属性的语法为:
ptr_to_struct.*.struct_member
这与在C语言中 -> 运算符类似。
当C指针指向结构数组时,语法将恢复为:
ptr_to_struct_arry[index].struct_member
Zig语言的一个主要用途就是导出C语言ABI接口的库,供其它编程语言调用。
库API接口由带export关键字前缀的函数、变量和类型组成。
构建静态库命令行:
$ zig build-lib mathtest.zig
构建动态库命令行:
$ zig build-lib mathtest.zig -dynamic
下面用同一个目录下mathtest.zig, test.c, build.zig这3个文件,演示导出C库的方法。
实际不行,fatal error: 'mathtest.h' file not found
必须手工添加:
才可以。
可将Zig目标文件与其它遵循C语言ABI接口的目标文件混合,例如:
###头文件不能自动生成。。
汇编语言与具体的CPU密切相关,除了裸机开发,绝大部分场景极少用到。如有需要,可参看:
Zig语言文档assembly部分
gcc扩展文档assembly部分
1个文件中可以写1个或多个测试定义。
zig test命令构建并运行源代码中的测试定义,测试功能的源代码在lib/test_runner.zig中。
expect函数的参数如果为 false 则返回错误,为 true 则继续执行,测试结果输出到标准输出(stderr)。
输出的 1/1 中的第一个1表示的是第1个测试,第二个1表示总共有1个测试。如果某条测试定义OK,则会立即清除该行显示。最终显示测试通过总数。
TODO 是否加个选项,测试通过显示后该行不清除。
测试定义形式为:test testname {block}
测试定义的返回类型是anyerror!void。
测试定义是文件作用域级变量,所以与顺序无关,可以写在被测代码之前或之后。
如果不用zig test命令构建,则测试定义在构建时被忽略,不包括在构建最终结果中(编译生成的obj文件或可执行文件)。
匿名测试只能用于运行其它测试,且不能被过滤。
zig测试构建时,只有已确定的测试定义被构建,除了文件作用域级的测试定义外,没有被引用的嵌套的测试定义不会被构建。
下例中refAllDecls函数表明执行文件中所有文件作用域级的测试定义,@this内置函数返回文件本身,_=S; 表示计算S的值,但丢弃计算结果。
当测试返回错误时,其错误返回追踪(error return trace)输出到标准错误,并输出失败总个数。
跳过测试的一种方法是用命令行选项过滤,zig test --test-filter testname,使测试只包括 testname。匿名测试定义无法过滤。
另一种方法是测试定义返回error.SkipZigTest,则该测试被跳过,并输出被跳过的总数。
当测试在默认的I/O阻塞模式下运行时,会跳过内有挂起(suspend)点的测试定义。(可使用--test-evented-io命令行参数改为I/O事件模式)
本示例中,如果使用nosuspend关键字,在I/O阻塞模式下,该测试不会被跳过。(参见: ###Async和Await)
如果用std.testing.allocator来分配内存,则测试时会报告发现的内存泄漏。例如本示例中,定义了list数组后,测试结束后没有释放内存(deinit函数),发生内存泄漏,测试时输出此错误。
把defer处的注释符去掉,则无内存泄漏,测试正常。
可用编译期变量@import("builtin").is_test检测是否处于测试构建状态。
除了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名字空间。
@breakpoint()
插入一个特定平台的调试陷阱指令,会引发调试器在那里中断。
此函数仅在函数作用域内有效。
关键字 |
简要说明 |
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 |
类型 |
内置函数由编译器提供,可直接使用,其函数名前缀是 @ 。
内置函数 |
所属章节 |
@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 |
参见Zig语言文档Grammer部分
编译器不强制执行这些编码约定,建议参考。
4个空格缩进;
'{' 在同一行,除非需要换行;
如果列表的内容大于2,将每一项单独一行,并在末尾加 ',' ;
行长小于100。
根据情况分别使用这3种风格:camelCaseFunctionName, TitleCaseTypeName, snake_case_variable_name。
type 用 TitleCase。如果是名字空间,即0个属性的struct且从不实例化,用snake_case;
可被调用且返回type,用TitleCase;
否则,用snake_case。
在书面英语中,大写字母缩写词、专有名词或任何其他有大写规则的单词,也受命名约定约束。
有顶级属性的名字空间文件(内含struct),使用TitleCase;否则用snake_case。目录名用snake_case。
省略任何基于被记录事物名称的冗余信息。
鼓励将信息复制到多个相似的函数上,因为这有助于IDE和其他工具提供更好的帮助文本。
用 assume 来表示引发未定义行为的不变量。
用 assert 表示引发安全检查未定义行为的不变量。
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/,在这两种情况下,空白都可以被正确识别。
这是我在本手册中用到的英文,我基本按个人理解进行翻译,有些可能与主流翻译不一致。
汉语 |
英文 |
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 |
本文档用纯文本按progdoc格式编辑,用progdoc程序生成单一html文件。
progdoc格式解析程序是用zig语言开发的。 :)
本文未经许可严禁转载。
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。