Perl 41:引用生命周期与析构边界
原定第 31 章要求把引用计数、循环引用、析构和关键写入的显式 close 分开。Perl 传统对象只是被 bless 过的引用;对象何时销毁,取决于引用何时归零。experiments/course-lifecycle.pl 定义 CourseLifecycle::Node,在 DESTROY 中记录节点名。单个节点离开作用域后,计数从 0 变成 1,说明普通引用归零会触发析构。
第二个场景创建 cycle-left 和 cycle-right,两者互相保存强引用。离开词法作用域后,外部变量没了,但两个对象仍互相引用,DESTROY 计数不增加。这个反例不是内存 profiler,也不证明泄漏规模;它只说明“离开作用域”与“引用计数归零”不是同一件事。若对象持有文件、锁或临时路径,把释放责任只放在 DESTROY 里,就可能被引用环拖住。
第三个场景把其中一条边改成 Scalar::Util::weaken。弱引用不增加引用计数;作用域结束后两个节点都能销毁,计数增加到 3。这个实验给出的是本机 Perl 5.34/5.38 兼容的核心模块行为,不扩展到所有引用图,也不把弱引用写成业务所有权模型的默认答案。弱引用适合打断反向指针、缓存或观察者关系;真正拥有对象生命周期的一方仍要明确。
写文件的结论更严格。save_report 不能依赖对象析构替代 close($output),因为关闭时才可能暴露缓冲写失败、文件大小限制或管道错误。补课后的测试通过 CORE::GLOBAL 在独立子进程中覆盖 close 与 unlink,让主 close 失败、临时文件 cleanup 失败、锁 close 失败同时出现;断言错误文本同时保留 cannot close report、cannot remove temporary report 和 cannot close report lock。这补的是“主错误不被清理错误覆盖”,不是公开测试 hook。
同一边界也适用于 CLI 的 STDOUT。旧入口只检查 print STDOUT 时,最终 flush 仍可能在解释器退出时才暴露失败;补课后的 bin/logtool 在主路径末尾显式 close STDOUT,并把关闭管道读端或文件大小限制触发的 stdout 失败映射为退出码 2。课程正文不能把 print 成功写成“所有字节已经到达下游”;需要强保证时,应显式 close 目标句柄并把失败纳入 CLI 退出码契约。
练习:给 Report 对象写一个 DESTROY,在里面 rename 临时文件,是否能简化失败处理?不能。析构触发时间受引用生命周期影响,析构中的异常也不适合承载主业务错误;关键写入应在明确的保存函数里完成,并把 close、rename、cleanup 的结果逐项记录。官方来源为 Perl 5.38.2 pod/perlobj.pod 的 DESTROY、pod/perlref.pod 的引用行为、Scalar::Util 的 weaken,以及 pod/perlfunc.pod 的 close 和 rename。









