1.1 语法概念
讲真,学习一门编程语言到底要花多少精力,我不知道。但是语法概念绝对是新手最需要了解掌握的,虽然内容不多,但是知识点全部都是新知识,需要各位读者朋友耐心一起看。
从之前的篇章我们知道 Zig 是更好的 C,内置 C 编译器,所以和 C 一样,语句都是用分号;结尾,代码块都是以花括号{ } 分隔。
接下来我们要看一个完整的小程序代码来讲解主要的语法
const std = @import("std");
// 如果 `main` 不是 `pub` (public),此代码将无法编译
pub fn main() void {
const user = User{
.power = 9001,
.name = "Goku",
};
std.debug.print("{s}'s power is {d}\n", .{user.name, user.power});
}
pub const User = struct {
power: u64,
name: []const u8,
};
你可以把这个代码块保存为 learning.zig 文件,直接跑 zig run learning.zig 就可以得到结果:
Goku's power is 9001
1.2 语法 1:模块引用
啥叫模块,就是编写好的函数代码库,方便你直接在程序代码中直接引用。从上面的范例代码中,我们可以看到:
// 以 @ 开头的函数是内置函数。它们是由编译器提供的,而不是标准库提供的。
const std = @import("std");
上面这行的意思就是Zig 的标准库以 std
作为模块名。简单明了。
当然既然能引用内置的标准库文件,外部的文件是否可以当成模块引用呢。这个肯定是可以的,触类旁通。例如,将 User
结构移动到它自己的文件中,比如 models/user.zig
:
// models/user.zig
pub const User = struct {
power: u64,
name: []const u8,
};
在主程序代码中可以这样引用:
// main.zig
const User = @import("models/user.zig").User;
models/user.zig
可以导出不止一项内容。例如,再导出一个常量:
// models/user.zig
pub const MAX_POWER = 100_000;
pub const User = struct {
power: u64,
name: []const u8,
};
这时,可以这样导入两个变量:
const user = @import("models/user.zig");
const User = user.User;
const MAX_POWER = user.MAX_POWER
1.3 语法 2:代码注释
Zig 的代码注释就一种,
// 如果 `main` 不是 `pub` (public),此代码将无法编译
没有 C 语言的代码注释 如/* ... */
的多行注释语法。
如果你看过 Zig 的标准库文档,你就会看到它的实际应用。//!
被称为顶级文档注释,可以放在文件的顶部。三斜线注释 (///
) 被称为文档注释,可以放在特定位置,如声明之前。如果在错误的地方使用这两种文档注释,编译器都会出错。注意,其实不管怎么变化,注释代码的语法就是双斜杠。
1.4 语法 3:函数
说到函数,我们最需要知道的就是入口函数 main:
pub fn main() void
忽略 main
作为程序入口的特殊作用,它只是一个非常基本的函数:不带参数,不返回任何东西(void)。
接下来我们看一个函数例子:
const std = @import("std");
pub fn main() void {
const sum = add(8999, 2);
std.debug.print("8999 + 2 = {d}\n", .{sum});
}
fn add(a: i64, b: i64) i64 {
return a + b;
}
主函数里面运行的 add 函数可以在不申明的情况下直接运行,这个是有点颠覆 C/C++ 程序员的习惯的,需要注意。
为了提高可读性,Zig 中不支持函数重载(用不同的参数类型或参数个数定义的同名函数)。这也是 Zig 降低程序员心智的设计,让我们可以专注业务编程。
1.5 语法 4:结构体
结构体就是面向对象编程中的对象:
pub const User = struct {
power: u64,
name: []const u8,
};
结构字段以逗号终止,并且可以指定默认值:
pub const User = struct {
power: u64 = 0,
name: []const u8,
};
当我们创建一个结构体时,必须对每个字段赋值。比如如下赋值
const user = User{.name = "Goku"};
因为User 结构体字段 power 配置了默认值,所以上面的代码可以正常编译。
结构体可以有方法,也可以包含声明(包括其他结构),甚至可能包含零个字段,此时的作用更像是命名空间。
pub const User = struct {
power: u64 = 0,
name: []const u8,
pub const SUPER_POWER = 9000;
fn diagnose(user: User) void {
if (user.power >= SUPER_POWER) {
std.debug.print("it's over {d}!!!", .{SUPER_POWER});
}
}
};
上面 diagnose
在定义 User
类型中,接受 User
作为其第一个参数。因此,我们可以使用struct.method()
的语法来调用它。但结构内的函数不必遵循这种模式。一个常见的例子是用于结构体初始化的 init
函数:
pub const User = struct {
power: u64 = 0,
name: []const u8,
pub fn init(name: []const u8, power: u64) User {
return User{
.name = name,
.power = power,
};
}
}
init
的命名方式仅仅是一种约定,在某些情况下,open
或其他名称可能更有意义。如果你和我一样,不是 C++ 程序员,可能对 .$field = $value,
这种初始化字段的语法感到奇怪,但你很快就会习惯它。
当我们创建 "Goku"
时,我们将 user
变量声明为 const
:
const user = User{
.power = 9001,
.name = "Goku",
};
// 第二种写法
const user: User = User{
.power = 9001,
.name = "Goku",
};
// 第三种写法
const user: User = .{
.power = 9001,
.name = "Goku",
};
// 第四种写法
pub fn init(name: []const u8, power: u64) User {
// instead of return User{...}
return .{
.name = name,
.power = power,
};
}
1.6 语法5:数组和切片
数组的大小是固定的,其长度在编译时已知。长度是类型的一部分,因此 4 个有符号整数的数组 [4]i32
与 5 个有符号整数的数组 [5]i32
是不同的类型。
数组长度可以从初始化中推断出来。在以下代码中,所有三个变量的类型均为 [5]i32
:
const a = [5]i32{1, 2, 3, 4, 5};
// 我们已经在结构体中使用过 .{...} 语法,
// 它也适用于数组
const b: [5]i32 = .{1, 2, 3, 4, 5};
// 使用 _ 让编译器推导长度
const c = [_]i32{1, 2, 3, 4, 5};
切片是指向数组的指针,外加一个在运行时确定的长度。你可以将切片视为数组的视图。切片例子如下:
const a = [_]i32{1, 2, 3, 4, 5};
const b = a[1..4];
在上述代码中, b
是一个长度为 3 的切片,并且是一个指向 a
的指针。
在实际代码中,切片的使用可能会多于数组。无论好坏,程序的运行时信息往往多于编译时信息。不过,在下面这个例子中,我们必须欺骗 Zig 编译器才能得到我们想要的示例:
const a = [_]i32{1, 2, 3, 4, 5};
var end: usize = 4;
const b = a[1..end];
b
现在是一个切片了。具体来说,它的类型是 []const i32
。
在创建切片时,我们可以省略上界,创建一个到要切分的对象(数组或切片)末尾的切片,例如 const c = b[2..]
。
当我们想通过切片修改数组的值的时候,一定需要使用 var 赋值。
const std = @import("std");
pub fn main() void {
const a = [_]i32{1, 2, 3, 4, 5};
var end: usize = 4;
const b = a[1..end];
std.debug.print("{any}", .{@TypeOf(b)});
}
因为 b
类型是 const
,执行操作 b[2] = 5
,我们会收到编译时错误:cannot assign to constant.。
如果我们想写入 b
,就需要将 a
从 const
变为 var
。
const std = @import("std");
pub fn main() void {
var a = [_]i32{1, 2, 3, 4, 5};
var end: usize = 4;
const b = a[1..end];
b[2] = 99;
}
1.7 语法 6:字符串
Zig 里字符串是字节(u8)的序列(即数组或切片)。我们可以从 name
字段的定义中看到这一点:name: []const u8。
按照惯例,这类字符串大多数都是用 UTF-8 编码,因为 Zig 源代码本身就是 UTF-8 编码的。但这并不是强制的,而且代表 ASCII 或 UTF-8 字符串的 []const u8
与代表任意二进制数据的 []const u8
实际上并没有什么区别。
"Goku"
的类型更接近于 *const [4]u8
,是一个指向 4 常量字节数组的指针。字符串字面量以空值结束。也就是说,它们的末尾总是有一个 \0
。在内存中,"Goku"
实际上是这样的:{'G', 'o', 'k', 'u', 0}
,Zig 有一种独特的语法来表示以空结尾的数组。"Goku"
的类型是 *const[4:0]u8
,即 4 字节以空结尾的数组指针。
const std = @import("std");
pub fn main() void {
// 建立 3 个布尔值的数值,并且以 false 值结尾
const a = [3:false]bool{false, true, false};
// 这句使用了 mem 的高级函数,先不用试图去理解
std.debug.print("{any}\n", .{std.mem.asBytes(&a).*});
}
上面代码会输出:{ 0, 1, 0, 0}
。
1.8 语法 7:comptime 和 anytype
先看代码
std.debug.print("{s}'s power is {d}\n", .{user.name, user.power});
print
函数的定义要求我们的第一个参数(字符串格式)是编译时已知的:
// 注意变量"fmt"前的"comptime"
pub fn print(comptime fmt: []const u8, args: anytype) void {
因为 print
会进行额外的编译时检查,什么样的检查呢?假设你把格式改为 it's over {d}/n
,但保留了两个参数。你会得到一个编译时错误:unused argument in 'it's over {d}'
。它还会进行类型检查:将格式字符串改为{s}'s power is {s}\n
,你会这个错误invalid format string 's' for type 'u64'
。如果在编译时不知道字符串的格式,就不可能在编译时进行这些检查。
comptime
会对编码产生直接影响的地方是整数和浮点字面的默认类型,即特殊的 comptime_int
和 comptime_float
。这行代码是无效的:var i = 0
。comptime
代码只能使用编译时已知的数据,对于整数和浮点数,这类数据由特殊的 comptime_int
和 comptime_float
类型标识。这种类型的值可以在编译时执行。你需要做的是给变量一个显式类型:
var i: usize = 0;
var j: f64 = 0;
开头print 函数中特殊代码片段 .{user.name, user.power} 是什么呢?根据上述 print
的定义,我们知道它映射到 anytype
类型的变量。我们打印看一下:
pub fn main() void {
std.debug.print("{any}\n", .{@TypeOf(.{.year = 2023, .month = 8})});
}
会输出:
struct{comptime year: comptime_int = 2023, comptime month: comptime_int = 8}
在这里,我们给匿名结构的字段取名为 year
和 month
。在原始代码中,我们没有这样做。在这种情况下,字段名会自动生成 0、1、2 等。print
函数希望结构中包含此类字段,并使用字符串格式中的序号位置来获取适当的参数。
Zig 没有函数重载,也没有可变函数(vardiadic,具有任意数量参数的函数)。但它的编译器能根据传入的类型创建专门的函数,包括编译器自己推导和创建的类型。
1.9 总结
Zig 的基本语法虽然已经精简了,但是还是有一些知识量的,所以本节我们就到这里,我把官方的内容做了一次梳理重组,让知识点能简明扼要,保持 Zig 语法的简洁风格。方便大家学习。请稍安勿躁,下一节继续把基本语法讲完。