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
Last modification:December 25, 2023