能否通过访问器对Moose属性中的哈希引用执行local本地化?
现有一个Moose类,其中bar属性为只读哈希引用类型,定义如下:
package Foo; use Moose; has bar => ( is => 'ro', isa => 'HashRef', default => sub { {} }, );
开发中需要对$foo->bar对应的哈希引用做local范围临时替换,目前已知的实现方式是直接访问对象的内部哈希存储:
my $foo = Foo->new; # ... my %local_bar = ( asdf => 123 ); local $foo->{bar} = \%local_bar; # 直接访问内部存储的写法 call_to_something_that_needs_bar($foo); for (keys %local_bar) { ... }
但这种直接访问对象内部结构的写法存在兼容问题1,需要找到不触碰对象内部存储结构即可完成属性临时本地化的实现方式。
1 兼容问题说明:生产环境代码运行时
$foo是普通Foo实例,但测试场景下$foo会被Object::Destroyer实例包裹。此时$foo->bar调用仍能正确代理到内层Foo对象的属性,但$foo->{bar}会直接访问外层销毁器对象自身的属性,和实际存储bar值的位置$foo->{object}->{bar}不一致,会导致local操作完全失效。
local的核心作用是对指定标量存储位置绑定临时值,作用域退出时(无论正常返回还是抛出异常)自动恢复原值。由于Moose默认的只读访问器返回的是属性值的副本而非存储位置的别名,不能直接写local $foo->bar = xxx实现效果,可通过以下两种合规方案实现同等效果,全程不需要直接访问对象bless后的哈希内部键:
方案1:通过Moose元对象接口拿到属性存储引用,原生支持local
不需要修改原有类的属性定义,直接通过Moose内置的元对象协议(MOP)公开接口获取属性存储位置的标量引用,直接对该引用做local操作即可,效果和直接访问内部哈希完全一致,且对所有基于Moose实现的对象(无论底层存储结构是hash还是array)都生效:
my %local_bar = ( asdf => 123 ); # 获取属性存储位置的标量引用,不触碰对象内部键 my $bar_slot_ref = $foo->meta->get_attribute('bar')->get_value_ref($foo); # 直接对存储位置做local临时替换 local $$bar_slot_ref = \%local_bar; call_to_something_that_needs_bar($foo); for (keys %local_bar) { ... } # 作用域退出时自动恢复原值,无需额外手动操作
方案2:用作用域守卫模拟local行为,兼容性最强
如果遇到代理类(比如Object::Destroyer)没有完全代理Moose元对象方法的场景,可以用作用域守卫绑定恢复逻辑,完全模拟local的行为,全程只通过公开的访问器、属性写入接口操作,适配所有对象包装场景:
use Scope::Guard; my %local_bar = ( asdf => 123 ); my $bar_attr = $foo->meta->get_attribute('bar'); my $orig_bar = $bar_attr->get_value($foo); # 注册作用域退出钩子,无论正常返回还是抛异常都会自动执行恢复 my $guard = Scope::Guard->new(sub { $bar_attr->set_value($foo, $orig_bar); }); # 写入临时值 $bar_attr->set_value($foo, \%local_bar); call_to_something_that_needs_bar($foo); for (keys %local_bar) { ... } # 离开当前作用域时守卫对象自动销毁,触发原值恢复
如果项目不允许引入额外CPAN模块,也可以手动通过
eval块结合$@异常捕获实现同等的退出时恢复逻辑,仅代码量稍多。
内容的提问来源于stack exchange,提问作者simbabque

