Skip to content
zig 版本:0.14.0

可选类型

Overview

在 Zig 中,为了在不损害效率的前提下提高代码安全性,可选类型是一个重要的解决方案。它的标志是 ??T 表示该类型的值可以是 nullT 类型。

zig
// 一个普通的i32整数
const normal_int: i32 = 1234;

// i32的可选类型,现在它的值可以是 i32 或者 null
const optional_int: ?i32 = 5678;

通常,可选类型在指针上发挥作用,而非整数。

null(空引用)是许多运行时异常的根源,甚至被指责为计算机科学中最严重的错误

我们可以通过可选类型来规避空引用问题。这是一种相对保守的做法,它同时兼顾了代码的可读性和运行效率。目前,Rust 在这方面更为激进,这增加了程序员在编写代码时的心智负担(因为你需要经常与编译期“斗智斗勇”,但好处是大大减少了运行时调试的负担)。相对而言,Zig 采取了一种折中方案:在编译期进行简单的检测,且检测出的错误通常很容易纠正。然而,这种方案的缺点是不能保证运行时绝对安全(可选类型仅能避免空指针问题,但不能避免悬空指针、迷途指针和野指针等问题)。

Zig 对 null 有特殊处理,并保证 null 不会被赋值给一个非可选类型变量。

和 C 对比

看看下面代码中 Zig 和 C 在处理 null 上的区别。(尝试调用 malloc 申请一块内存。)

zig
// extern 用于连接标准 libc 的 malloc 函数,它是 posix 标准之一
extern fn malloc(size: usize) ?*u8;

fn doAThing() ?*Foo {
    // 尝试调用 malloc 申请内存,如果失败则返回null
    const ptr = malloc(1234) orelse return null;
    _ = ptr; // ...
}
c
// 引用的是 malloc 的原型
void *malloc(size_t size);

struct Foo *do_a_thing(void) {
    char *ptr = malloc(1234);
    if (!ptr) return NULL;
    // ...
}

在这里,我们通过使用 orelse 解构了可选类型,确保 ptr 是一个合法可用的指针;如果 malloc 返回 null,则直接返回 null。(这看起来比 C 更加明了且易用。)

再看下例:

zig
fn doSomethingWithFoo(foo: *Foo) void {
    _ = foo;
}

fn doAThing(optional_foo: ?*Foo) void {
    // 干点什么。。。
    if (optional_foo) |foo| {
        doSomethingWithFoo(foo);
    }
    // 干点什么。。。
}
c
void do_a_thing(struct Foo *foo) {
    // 干点什么。。。
    if (foo) {
        do_something_with_foo(foo);
    }
    // 干点什么。。。
}

看起来区别不大,只是在 if 语法上有所不同。if 块中保证 foo 不为 null

当然,在 C 中,你可以使用 __attribute__((nonnull)) 来告诉 C 编译器这里不可能为 null,但其使用成本明显比 Zig 高。

编译期反射访问可选类型

WARNING

该部分内容需要编译期反射的知识,可以选择暂时跳过!

我们也可以通过编译期函数来实现反射,进而访问可选类型:

zig
// 声明一个可选类型,并赋值为 null
var foo: ?i32 = null;

// 重新赋值为子类型的值,这里是 i32
foo = 1234;

// 使用编译期反射来获取 foo 的类型信息
try comptime expect(@typeInfo(@TypeOf(foo)).optional.child == i32);

可选指针

可选指针会保证与普通指针具有相同的大小,null 会被视为地址 0。