Skip to content
zig 版本:0.13.0

可选类型

Overview

在 Zig 中,要在不损害效率的前提下,尽量提高代码安全性,其中一个方案就是可选类型,他的标志是 ??T表示它的值是它的值是 nullT

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

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

当然,它一般在指针上发挥作用,而不是整数。

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

我们可以通过可选类型来规避它。这其实是一种比较保守的做法,它同时兼顾了代码的可读性和运行效率。目前最为激进的应该是 Rust,它真的是非常的激进,这增加了程序员在写代码时的心智负担(因为你经常需要和编译期斗智斗勇,但好处大大是减少了你在运行时 Debug 的负担)。相对地,zig 采取了一种折中方案,编译期进行简单的检测,而且检测出来的错误一般很容易纠正;这样的缺点是并不能保证你的运行时是绝对安全的(可选类型仅仅能避免空指针问题,却不能避免悬空指针、迷途指针和野指针等问题)。

zig 将 null 特殊看待,并且保证 null 不会被赋值给一个非可选类型变量。

和 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 是一个合法可用的指针,否则直接返回 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 考虑!