Ziglings 笔记 29: 失败时的后悔药 (errdefer)
只有出错时才执行
在学习了 defer(总是执行)之后,Ziglings 的第 29 个练习向我们展示了它的条件变体:errdefer。
在系统编程中,我们经常遇到这样的场景:我做了一半的工作(比如打开了一个数据库连接),准备做下一半(查询数据)。如果查询失败了,我需要回滚之前的操作(关闭连接);但如果查询成功了,我要把连接留给调用者使用。
errdefer 就是为此而生的。
挑战:条件性日志
我们需要完善 makeNumber 函数,使得它:
- 总是打印 “Getting number…”。
- 如果成功拿到数字,打印 “got X.”。
- 只有在失败时,打印 “failed!”。
解决方案
使用 errdefer 来挂载错误处理逻辑:
const std = @import("std");
var counter: u32 = 0;
const MyErr = error{ GetFail, IncFail };
pub fn main() void {
// 第一次尝试:成功
const a: u32 = makeNumber() catch return;
// 第二次尝试:失败(main 函数会在这里通过 catch return 退出)
const b: u32 = makeNumber() catch return;
std.debug.print("Numbers: {}, {}\n", .{ a, b });
}
fn makeNumber() MyErr!u32 {
std.debug.print("Getting number...", .{});
// 核心代码:errdefer
// 这行代码只有在当前函数返回错误时才会执行
errdefer std.debug.print("failed!\n", .{});
var num = try getNumber();
// 这里可能会出错!如果出错,上面的 errdefer 就会被触发
num = try increaseNumber(num);
// 如果运行到这里,说明没有错误,errdefer 将会被忽略
std.debug.print("got {}. ", .{num});
return num;
}
// ... 辅助函数省略 ...
核心知识点总结
1. errdefer vs defer
defer: 这里的代码是清理工,不管房间有人没人,每天结束都得打扫。errdefer: 这里的代码是保险员,只有出事故了才出现。
2. 完美的“事务”处理
errdefer 使得编写事务性代码变得非常整洁。
const object = createObject();
errdefer destroyObject(object); // 如果后续步骤失败,销毁它
try doSomethingWith(object);
try doAnotherThing(object);
return object; // 如果一切顺利,对象被返回,不会被销毁
这种模式避免了复杂的 if (error) { cleanup(); return error; } 嵌套。
3. 程序输出分析
运行这个程序,你会看到:
Getting number...got 5. Getting number...failed!
注意最后一行没有打印 Numbers: ...,因为 main 函数在第二次调用失败时直接 return 退出了。这展示了错误是如何沿着调用栈一路传播并触发沿途的 errdefer 的。
后续预告:我们已经掌握了大部分控制流。接下来,我们要面对 Zig 中最强大的结构之一,它能替代 if-else 链,甚至能做更多事情——Switch 语句。