Ziglings 笔记 46: 优雅的链表终点

寻找出口

在 Ziglings 的第 46 个练习中,我们再次回到了大象链表的问题。 之前因为没有 Optional 类型,我们被迫创建了一个无限循环的圆圈。现在,有了 ?null,我们可以创建一个有始有终的正常链表了。

挑战:断开圆环

我们需要修改 visitElephants 函数。 在遍历链表时,当我们到达最后一个大象(它的 tailnull)时,程序不应该崩溃,也不应该陷入死循环,而是应该优雅地停止遍历。

解决方案

这是利用 orelse 控制流的实现:

const std = @import("std");

// 1. 结构体定义
const Elephant = struct {
    letter: u8,
    // 使用 ?*Elephant 允许尾巴为空 (null),表示链表的尽头
    tail: ?*Elephant = null, 
    visited: bool = false,
};

pub fn main() void {
    var elephantA = Elephant{ .letter = 'A' };
    var elephantB = Elephant{ .letter = 'B' };
    var elephantC = Elephant{ .letter = 'C' };

    linkElephants(&elephantA, &elephantB);
    linkElephants(&elephantB, &elephantC);
    // C 的尾巴保持为 null,不再指向 A,链表变成了线性 A -> B -> C -> null

    visitElephants(&elephantA);

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

fn linkElephants(e1: ?*Elephant, e2: ?*Elephant) void {
    // 使用 .? 快捷方式:如果 e1 是 null,程序会 panic。
    // 这表达了我们对输入的信心:链接时源头必须存在。
    e1.?.tail = e2.?;
}

fn visitElephants(first_elephant: *Elephant) void {
    var e = first_elephant;

    while (!e.visited) {
        std.debug.print("Elephant {u}. ", .{e.letter});
        e.visited = true;

        // 核心代码:orelse break
        // 尝试获取下一个大象。
        // 如果 tail 是 null,说明到了尽头,执行 break 跳出循环。
        // 如果不是 null,将下一个大象赋值给 e,继续循环。
        e = e.tail orelse break;
    }
}

核心知识点总结

1. orelse 的灵活性

这个练习展示了 orelse 不仅仅能提供默认数据,还能控制程序流程。

  • x orelse 0:提供默认值。
  • x orelse return:如果为空,函数返回。
  • x orelse break:如果为空,跳出循环。
  • x orelse unreachable (即 x.?):如果为空,触发崩溃。

2. 安全的空值处理

在 C/C++ 中,遍历链表通常写成 while (e != NULL) { ... e = e->next; }。如果不小心在 next 为空时解引用,程序就炸了。 Zig 的写法强制我们在解引用前处理空值情况。e.tail 本身是不能直接赋值给 e 的(因为类型不同),必须通过 orelse 剥离 ?,这从编译层面保证了类型安全。

3. .? 缩写

虽然 .? 写起来很爽,但在生产环境中要慎用。它相当于把所有的错误处理责任都抛给了 Panic 机制。只有当你从逻辑上 100% 确定该值不可能为空时(例如在做过 check 之后),才应该使用它。


后续预告:我们已经掌握了结构体存储数据。但是,如果我们想把函数和数据绑定在一起,就像“面向对象”编程中的“方法”那样,Zig 支持吗?下一篇我们将揭晓 Zig 的方法(Methods)。