zig 入门 - 方方面面
本来是想写一篇 rust 入门的,但是在写 rust 的过程中实在是讨厌关键字满天飞的感觉,因此突然想到 zig 这个语言,就尝试了一下。于是就有了这篇文章,记录一下 zig 的学习过程和一些注意事项。
由于 zig 目前处于 0.11.0 版本,因此或多或少会有些不稳定的情况,不过似乎 zig 版本进入 0.10.0 后就比较稳定了,基本上没有太大的变动。
命名规范
首先必须要了解的就是命名规范,因为目前 zig 的 lsp 还不成熟,标准库中的文档也不是很完善,很多时候都不知道自己到底引入了什么,完全都是根据名字来推断,因此命名规范就显得尤为重要。
- 如果
x
是一个type
,那么就应该使用大驼峰命名,除非该type
是一个不应被实例化的struct
且没有任何结构体变量,在此时,x
被视为一个 namespace,此时使用蛇形命名。 - 如果
x
是 callable 的,并且其返回类型是一个type
,那么就应该使用大驼峰命名。(这个规则通常是针对泛型结构体的,以便与结构体的命名相符合,具体例子见下文) - 如果
x
是 callable 的,并且其返回类型不是一个type
,那么就应该使用小驼峰命名。 - 如果上述条件都不符合,那么就应该使用蛇形命名。
错误处理
zig 的错误处理与 rust 类似,!
可以看作 rust 中的 Result<T, E>
,?
可以看作 rust 中的 Option<T>
,且 zig 也以 sum type 实现其错误处理机制。不过需要注意的是,zig 的 error 只是一个类型,它不具备除了类型名字的任何其他结构(如其他语言最常见的错误描述),这在目前来看似乎是 zig 的一个缺陷(同时也没有语法糖来手动处理上述情况,即错误描述,通常我们打印它)。具体的错误处理机制如下:
错误处理中的糖
通常在错误处理中,我们会使用两种语法糖,一种是 try
,一种是 error.XX
,具体的表达如下:
const foo = try bar();
// aka
const foo = bar() catch |err| return err;
const err = error.FileNotFound;
// aka
const err = (error {FileNotFound}).FileNotFound;
// 在这里 error {..} 创建了一个错误集,而 (error {..}).FileNotFound 则是从类型集中取出了一个类型
错误集与错误类型
一个错误集是一个 sum type,它的每一个成员都是一个错误类型,需要注意的是,函数的返回值应该是一个错误集,而不是错误类型,这要与上文的 error.XX
语法糖区分开来。
zig 中错误集被视为集合,因此,有关错误集的比较均是集合的比较,错误类型之间的转换与错误集的包含关系有关,anyerror
是所有错误集的超集。此外,错误类型是全局唯一的,在实现上,zig 给每个错误类型都分配了一个全局的 id,错误类型的 key 是他的名字,这意味着有关错误类型的比较在 zig 中都极为高效。
const FileErr = error{ NotFound };
const PathErr = error{ NotFound };
const DirErr = error{ NotFound, NotDir };
std.debug.print("{}\n", .{FileErr == PathErr}); // true
std.debug.print("{}\n", .{FileErr == DirErr}); // false
std.debug.print("{}\n", .{FileErr.NotFound == PathErr.NotFound}); // true
错误类型推断
在 zig 中,我们不必为函数的可能返回的错误集显式地声明,zig 会在编译阶段自动推断出函数可能返回的错误集,因此,我们常常会使用fn foo() !void {}
的语法来自动对所有可能返回的错误集取并集。
具体处理
这里仅介绍我在使用 zig 时常用的错误处理方式,具体的错误处理方式可以参考 zig 的官方文档。
// bar is a ?type
const foo = bar orelse return error.Err; // convert null to error seems good
// bar is a !type
const foo = bar() catch |err| {
errdefer .. // errdefer in catch block can reconcile with the specific error type and make error handing more explicit
switch (err) {
err.A => { .. },
err.B => { .. },
else |other_err| => return other_err,
}
}
匿名结构体不是元组
在 zig 里,变量没有名字的匿名结构体被称为元组,但是令人不解的是,元组虽然定义上是结构体,但是他不能被当作结构体来使用。事实上,元组和结构体在实际编程中就可以看作是两个完全不同的东西,zig 中概念的元组不过是用 struct 包了一层正常编程语言常见的 tuple,这并不难,但是如果第一次遇到就有很强的迷惑性。
const foo = .{ 1, 2, 3 };
const bar = .{ .x = 1, .y = 2, .z = 3 };
std.debug.print("{}\n{}\n", .{ @TypeOf(foo), @TypeOf(bar) });
// struct{comptime comptime_int = 1, comptime comptime_int = 2, comptime comptime_int = 3}
// struct{comptime x: comptime_int = 1, comptime y: comptime_int = 2, comptime z: comptime_int = 3}
std.debug.print("{}\n{}\n", .{ foo[1], bar[1] }); // compile error, cannot index into bar(not tuple)
时间处理
应该不久之后,一个新引入 DateTime 的 proposal 就会被 merge 进入 zig 的标准库,大大简化了时间处理的过程,而且 zig 本身可以调用 C 库,直接使用time.h
也是一个不错的选择。如果你坚持要使用 zig 标准库,那么就看看这丑陋的代码吧:
const std = @import("std");
pub const Date = struct {
year: u16,
month: u4,
day: u5,
hour: u5,
minute: u6,
second: u6,
pub fn new(secs: i64) Date {
const epoch = std.time.epoch.EpochSeconds{ .secs = @intCast(secs) };
const epoch_day = epoch.getEpochDay();
const year_day = epoch_day.calculateYearDay();
const month_day = year_day.calculateMonthDay();
const day_seconds = epoch.getDaySeconds();
const year = year_day.year;
const month = month_day.month.numeric();
const day = month_day.day_index + 1;
const hour = day_seconds.getHoursIntoDay();
const minute = day_seconds.getMinutesIntoHour();
const second = day_seconds.getSecondsIntoMinute();
return Date{ .year = year, .month = month, .day = day, .hour = hour, .minute = minute, .second = second };
}
};
没有函数字面量
不用找了,短时间内 zig 不支持函数字面量,具体 proposal 已被否决(RFC: Make function definitions expressions · Issue #1717 · ziglang/zig)。
comptime
在 zig 中,comptime 是一种很强大的特性,我们可以借此实现泛型,我们可以强制性的要求类型必须编译期可知,这样我们就可以在编译期处理类型,进行泛型的处理。按照官方的文档,可以如下定义:
fn List(comptime T: type) type {
return struct {
items: []T,
len: usize,
};
}
// The generic List data structure can be instantiated by passing in a type:
var buffer: [10]i32 = undefined;
var list = List(i32){
.items = &buffer,
.len = 0,
};
因此,本质上,zig 的泛型结构体实现就是返回一个结构体类型,随后利用字面量创建结构体实例。
不过,我们可以看出,zig 的泛型支持是在过于简陋以至于太过强大,通常我们不得不继续使用 comptime 对类型做更更多的限制(使用 metaprogramming),让我们能够在编译阶段就对类型检查等进行控制。一个例子如下:
fn List(comptime T: type) type {
comptime {
if (!std.meta.trait.hasFn("isValid", T)) {
@compileError("Type T must implement a method named 'isValid'");
}
if (@sizeOf(T) > 64) {
@compileError("Type T must be smaller than 64 bytes");
}
if (!std.meta.hasField(T, "value")) {
@compileError("Type T must have a field named 'value'");
}
}
return struct {
items: []T,
len: usize,
pub fn checkValidity(self: *const Self) bool {
for (self.items) |item| {
if (!item.isValid()) {
return false;
}
}
return true;
}
};
}
然而一个很自然的问题就出现了,如果类型编译期不可知怎么办?这时候我们可以使用 anytype
来代替 type
(这又是 zig 中命名的一个坑,anytype
重点不是在 type
上,而是在 any
上,简单来说,anytype
就是 Go 中的 interface{}
),这样我们就可以在运行时对类型进行检查,进而给函数定义赋给不同的函数实现,但是这样做的话,我们就不能在编译期中对类型进行检查了,可以说这是 zig 目前的一个缺陷。
同时我们可以看出,我们还可以借以 comptime 来实现其他语言中的继承或者多态,但是问题和上文一样,如果类型编译期不可知,那么我们就不能在编译期中对类型进行检查了,我们不得不将在运行时检查我们的代码,而这是不被接受的,关于更多本人对于 comptime 的看法,可以查看下一篇文章。
Documentation - The Zig Programming Language
Allow returning a value with an error · Issue #2647 · ziglang/zig
What is Zig's Comptime? | Loris Cro's Blog
std - Zig
Zigで日付を取得する方法
std.time: add Date by Vexu · Pull Request #18272 · ziglang/zig