Ziglings 笔记 46: 优雅的链表终点
寻找出口
在 Ziglings 的第 46 个练习中,我们再次回到了大象链表的问题。
之前因为没有 Optional 类型,我们被迫创建了一个无限循环的圆圈。现在,有了 ? 和 null,我们可以创建一个有始有终的正常链表了。
挑战:断开圆环
我们需要修改 visitElephants 函数。
在遍历链表时,当我们到达最后一个大象(它的 tail 是 null)时,程序不应该崩溃,也不应该陷入死循环,而是应该优雅地停止遍历。
解决方案
这是利用 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)。