上一章的构造器验证等级,若调用者随后把原散列的 INFO 改成 ERROR,报表所见的对象是否跟着变化?new 创建了新的最外层散列,因此不会。测试先构造 INFO,再修改 $fields->{level},$record->level 仍是 INFO;as_hash 又返回 { %$self },修改返回的散列最外层字段同样不改对象。这是两处明确的浅复制,不是 Perl 自动的对象快照。

图:构造复制与对象数据所有权

1
2
3
调用者 hash ----(逐字段复制)----> bless 后的 hash
| |
+-- 修改 level 不传过去 +-- as_hash 另复制一层

反例更关键:$record->{level} = 'WARN' 可以直接改对象内部,之后 level 返回 WARN。bless 没有隐藏散列槽位,也没实施只读约束。本例四个值都是字符串,因此上述浅复制足够描述当前结构;将来若 message 改为嵌套引用,内层仍会共享,需重新制定所有权规则。正例和这个“能够绕过”的反例分别在 t/26-31-objects-and-process.t 的 copy protects object、bless does not prohibit direct hash mutation 断言中复核,prove -v ... 退出 0、STDERR 空;反例是暴露接口边界,不是一次预期抛错。

练习:能否靠重命名 as_hash 防止调用者 $record->{level}?不能;若要强制不变量,必须改变表示或明确限制可接触对象的代码,而不是仅修改方法名。参考 Perl 5.38.2 pod/perlref.pod 与 pod/perlobj.pod,执行与金标在 examples/perl-language/evidence/26-31.md。

组合关系还会影响对象生命周期。experiments/course-lifecycle.pl 先构造一个普通对象,离开作用域后 DESTROY 计数增加;再构造两个互相强引用的对象,离开作用域后计数不增加,因为引用环仍在;最后把其中一条边改成 Scalar::Util::weaken 的弱引用,离开作用域后两个对象都能销毁。这个实验只说明本例的 Perl 引用计数与弱引用边界,不把 DESTROY 写成外部资源提交或事务回滚保证。