初探 Zig 编程:新手入门二

1.1 语法8: 控制流

与大多数语言一样, 在 Zig 中,控制流是通过关键字完成的。ifelse if 和 else 都有支持:

// std.mem.eql 将逐字节进行比较,对于字符串来说它是大小写敏感的。
if (std.mem.eql(u8, method, "GET") or std.mem.eql(u8, method, "HEAD")) {
    // 处理 GET 请求
} else if (std.mem.eql(u8, method, "POST")) {
    // 处理 POST 请求
} else {
    // ...
}

虽然没有三元运算符,但可以使用 if/else 来代替:

const super = if (power > 9000) true else false;

switch 语句类似于if/else if/else,但具有穷举的优点。也就是说,如果没有涵盖所有情况,编译时就会出错。下面这段代码将无法编译:

fn anniversaryName(years_married: u16) []const u8 {
    switch (years_married) {
        1 => return "paper",
        2 => return "cotton",
        3 => return "leather",
        4 => return "flower",
        5 => return "wood",
        6 => return "sugar",
    }
}

如果修正这个错误呢?我们可以使用 else 修正:

// ...
6 => return "sugar",
else => return "no more gifts for you",

在进行匹配时,我们可以合并多个 case 或使用范围;在进行处理时,可以使用代码块来处理复杂的情况:

fn arrivalTimeDesc(minutes: u16, is_late: bool) []const u8 {
    switch (minutes) {
        0 => return "arrived",
        1, 2 => return "soon",
        3...5 => return "no more than 5 minutes",
        else => {
            if (!is_late) {
                return "sorry, it'll be a while";
            }
            // todo, something is very wrong
            return "never";
        },
    }
}

Zig 的 for 循环用于遍历数组、切片和范围。例如,我们可以这样写:

fn contains(haystack: []const u32, needle: u32) bool {
    for (haystack) |value| {
        if (needle == value) {
            return true;
        }
    }
    return false;
}

for 循环也可以同时处理多个序列,只要这些序列的长度相同。下面是其大致实现:

pub fn eql(comptime T: type, a: []const T, b: []const T) bool {
    // if they aren't the same length, they can't be equal
    if (a.len != b.len) return false;

    for (a, b) |a_elem, b_elem| {
        if (a_elem != b_elem) return false;
    }

    return true;
}

上面这个代码段中,注意 if 检查的设计,这是一个必要的防护措施。如果我们去掉它,并传递不同长度的参数,就会出现运行时 panic。注意一定要记住:for 在作用于多个序列上时,要求其长度相等。

for 循环也可以遍历范围,例如:

for (0..10) |i| {
    std.debug.print("{d}\n", .{i});
}

与一个(或多个)序列组合使用时,它的作用就真正体现出来了:

fn indexOf(haystack: []const u32, needle: u32) ?usize {
    for (haystack, 0..) |value, i| {
        if (needle == value) {
            return i;
        }
    }
    return null;
}

因为 while 比较简单,形式如下:while (condition) { },这有利于更好地控制迭代。例如,在计算字符串中转义序列的数量时,我们需要将迭代器递增 2 以避免重复计算 \\

var i: usize = 0;
var escape_count: usize = 0;
while (i < src.len) {
    if (src[i] == '\\') {
        i += 2;
        escape_count += 1;
    } else {
        i += 1;
    }
}

while 可以包含 else 子句,当条件为假时执行 else 子句。它还可以接受在每次迭代后要执行的语句。在 for 支持遍历多个序列之前,这一功能很常用。上述语句可写成

var i: usize = 0;
var escape_count: usize = 0;

// 改写后的
while (i < src.len) : (i += 1) {
    if (src[i] == '\\') {
        // +1 here, and +1 above == +2
        // 这里 +1,上面也 +1,相当于 +2
        i += 1;
        escape_count += 1;
    }
}

Zig 也支持 break 和 continue 关键字,用于跳出最内层循环或跳转到下一次迭代。

代码块可以附带标签(label),break 和 continue 可以作用在特定标签上。举例说明:

outer: for (1..10) |i| {
    for (i..10) |j| {
        if (i * j > (i+i + j+j)) continue :outer;
        std.debug.print("{d} + {d} >= {d} * {d}\n", .{i+i, j+j, i, j});
    }
}

break 还有另一个有趣的行为,即从代码块中返回值:

const personality_analysis = blk: {
    if (tea_vote > coffee_vote) break :blk "sane";
    if (tea_vote == coffee_vote) break :blk "whatever";
    if (tea_vote < coffee_vote) break :blk "dangerous";
};

像这样有返回值的的块,必须以分号结束。

1.2 语法9:枚举

枚举是带有标签的整数常量。它们的定义很像结构体:

// 可以是 "pub" 的
const Status = enum {
    ok,
    bad,
    unknown,
};

与结构体一样,枚举可以包含其他定义,包括函数,这些函数可以选择性地将枚举作为第一个参数:

const Stage = enum {
    validate,
    awaiting_confirmation,
    confirmed,
    completed,
    err,

    fn isComplete(self: Stage) bool {
        return self == .confirmed or self == .err;
    }
};

回想一下,结构类型可以使用 .{...} 符号根据其赋值或返回类型来推断。在上面,我们看到枚举类型是根据与 self 的比较推导出来的,而 self 的类型是 Stage。我们本可以明确地写成:return self == Stage.confirmed 或 self == Stage.err。但是,在处理枚举时,你经常会看到通过 .$value 这种省略具体类型的情况。

switch 的穷举性质使它能与枚举很好地搭配,因为它能确保你处理了所有可能的情况。不过在使用 switch 的 else 子句时要小心,因为它会匹配任何新添加的枚举值,而这可能不是我们想要的行为。

1.3 语法10:带标签的联合 Tagged Union

联合定义了一个值可以具有的一系列类型。例如,这个 Number 可以是整数、浮点数或 nan(非数字):

const std = @import("std");

pub fn main() void {
    const n = Number{.int = 32};
    std.debug.print("{d}\n", .{n.int});
}

const Number = union {
    int: i64,
    float: f64,
    nan: void,
};

一个联合一次只能设置一个字段;试图访问一个未设置的字段是错误的。既然我们已经设置了 int 字段,如果我们试图访问 n.float,就会出错。组合的一个字段 nan 是 void 类型。我们该如何设置它的值呢?使用 {}

const n = Number{.nan = {}};

使用联合的一个难题是要知道设置的是哪个字段。这就是带标签的联合发挥作用的地方。带标签的联合将枚举与联合定义在一起,可用于 switch 语句中。请看下面这个例子:

pub fn main() void {
    const ts = Timestamp{.unix = 1693278411};
    std.debug.print("{d}\n", .{ts.seconds()});
}

const TimestampType = enum {
    unix,
    datetime,
};

const Timestamp = union(TimestampType) {
    unix: i32,
    datetime: DateTime,

    const DateTime = struct {
        year: u16,
        month: u8,
        day: u8,
        hour: u8,
        minute: u8,
        second: u8,
    };

    fn seconds(self: Timestamp) u16 {
        switch (self) {
            .datetime => |dt| return dt.second,
            .unix => |ts| {
                const seconds_since_midnight: i32 = @rem(ts, 86400);
                return @intCast(@rem(seconds_since_midnight, 60));
            },
        }
    }
};

请注意, switch 中的每个分支捕获了字段的类型值。也就是说,dt 是 Timestamp.DateTime 类型,而 ts 是 i32 类型。这也是我们第一次看到嵌套在其他类型中的结构。DateTime 本可以在联合之外定义。我们还看到了两个新的内置函数:@rem 用于获取余数,@intCast 用于将结果转换为 u16@intCast 从返回值类型中推断出我们需要 u16)。

从上面的示例中我们可以看出,带标签的联合的使用有点像接口,只要我们提前知道所有可能的实现,我们就能够将其转化带标签的联合这种形式。

最后,带标签的联合中的枚举类型可以自动推导出来。我们可以直接这样做:

const Timestamp = union(enum) {
    unix: i32,
    datetime: DateTime,

    ...

这里 Zig 会根据带标签的联合,自动创建一个隐式枚举。

1.4 语法11:可选类型 Optional

在类型前加上问号 ?,任何值都可以声明为可选类型。可选类型既可以是 null,也可以是已定义类型的值:

var home: ?[]const u8 = null;
var name: ?[]const u8 = "Leto";

.?用于访问可选类型后面的值:

std.debug.print("{s}\n", .{name.?});

但如果在 null 上使用 .?,运行时就会 panicif 语句可以安全地取出可选类型背后的值:

if (home) |h| {
    // h is a []const u8
    // we have a home value
} else {
    // we don't have a home value
}

orelse 可用于提取可选类型的值或执行代码。这通常用于指定默认值或从函数中返回:

const h = home orelse "unknown"

// 或直接返回函数
const h = home orelse return;

可选类型还可以与 while 整合,经常用于创建迭代器。我们这里忽略迭代器的细节,但希望这段伪代码能说明问题:

while (rows.next()) |row| {
    // do something with our row
}

1.5 语法12:未定义的值 Undefined

通常这样做的一个地方是创建数组,其值将由某个函数来填充:

var pseudo_uuid: [16]u8 = undefined;
std.crypto.random.bytes(&pseudo_uuid);

上述代码仍然创建了一个 16 字节的数组,但它的每个元素都没有被赋值。

1.6 语法13:错误 Errors

Zig 中错误处理功能十分简单、实用。这一切都从错误集(error sets)开始,错误集的使用方式类似于枚举:

// 与第 1 部分中的结构一样,OpenError 也可以标记为 "pub"。
// 使其可以在其定义的文件之外访问
const OpenError = error {
    AccessDenied,
    NotFound,
};

任意函数(包括 main)都可以返回这个错误:

pub fn main() void {
    return OpenError.AccessDenied;
}

const OpenError = error {
    AccessDenied,
    NotFound,
};

如果你尝试运行这个程序,你会得到一个错误:expected type 'void', found 'error{AccessDenied,NotFound}'。因为我们定义了返回类型为 void 的 main 函数,但我们却返回了另一种东西(很明显,它是一个错误,而不是 void)。要解决这个问题,我们需要更改函数的返回类型。

pub fn main() OpenError!void {
    return OpenError.AccessDenied;
}

这就是所谓的错误联合类型,它表示我们的函数既可以返回 OpenError 错误,也可以返回 void(也就是什么都没有)。

Zig 能够为我们隐式创建错误集。我们可以这样做,而不需要提前声明:

pub fn main() !void {
    return error.AccessDenied;
}

错误联合类型的真正价值在于 Zig 语言提供了 catch 和 try 来处理它们。返回错误联合类型的函数调用时,可以包含一个 catch 子句。例如,一个 http 服务器库的代码可能如下所示:

action(req, res) catch |err| {
    if (err == error.BrokenPipe or err == error.ConnectionResetByPeer) {
        return;
    } else if (err == error.BodyTooBig) {
        res.status = 431;
        res.body = "Request body is too big";
    } else {
        res.status = 500;
        res.body = "Internal Server Error";
        // todo: log err
    }
};

switch 的版本更符合惯用法:

action(req, res) catch |err| switch (err) {
    error.BrokenPipe, error.ConnectionResetByPeer) => return,
    error.BodyTooBig => {
        res.status = 431;
        res.body = "Request body is too big";
    },
    else => {
        res.status = 500;
        res.body = "Internal Server Error";
    }
};

老实说,你在 catch 中最有可能做的事情就是把错误信息给调用者:

action(req, res) catch |err| return err;

Zig 提供了 try 关键字用于处理这种情况。上述代码的另一种写法如下:

try action(req, res);

鉴于必须处理错误,这一点尤其有用。多数情况下的做法就是使用 try 或 catch

大多数情况下,你都会使用 try 和 catch,但 if 和 while 也支持错误联合类型,这与可选类型很相似。在 while 的情况下,如果条件返回错误,则执行 else 子句。

有一种特殊的 anyerror 类型可以容纳任何错误。anyerror 主要用在可以是任意错误类型的函数参数或结构体字段中(想象一下日志库)。

使用此类函数有多种方法,但最简洁的方法是使用 try 来解除错误,然后使用 orelse 来解除可选类型。下面是一个大致的模式:

const std = @import("std");

pub fn main() void {
    // This is the line you want to focus on
    const save = (try Save.loadLast()) orelse Save.blank();
    std.debug.print("{any}\n", .{save});
}

pub const Save = struct {
    lives: u8,
    level: u16,

    pub fn loadLast() !?Save {
        //todo
        return null;
    }

    pub fn blank() Save {
        return .{
            .lives = 3,
            .level = 1,
        };
    }
};

1.7 总结

虽然我们还未涉及 Zig 语言中更高级的功能,但我们通过两篇文章也覆盖了常见的语法场景。它们将作为一个基础,让我们能够探索更复杂的话题,而不用被语法所困扰。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xiaodeshi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值