跳转到内容

使用 Moose 编程/语法/has

来自维基教科书,开放世界中的开放书籍

Moose 最重要的关键字可以说是 "has",它本质上是迄今为止最强大的访问器生成器。许多这些功能可以在 MooseClass::MOP::Accessor 的附带文档中找到。

has 的属性

[编辑 | 编辑源代码]

下面你可以找到所有 has 的属性。[1] 星号代表默认值。

[+]has => (
   isa        => $type_constraint,
   is         => 'ro|rw',
   coerce     => 0*|1,
   weak_ref   => 0*|1,
   does       => $role_name,
   required   => 0*|1
   lazy       => 0*|1
   auto_deref => 0*|1
   init_arg   => $str|undef,
   default    => $default_value|sub { $default_value },
   metaclass  => $voodoo,
   trigger    => $sub_ref,
   initializer=> $sub_ref,
   handles    => Array|Hash|Regex|Role|Code,
   lazy_build => 0*|1,
   clearer    => $clearer_name,
   predicate  => $predicate_name,
   builder    => $builder_name,
   reader     => $reader_name,
   writer     => $writer_name,
);


has 的第二个最重要的属性是 isaisa 发音为 "Is a",它实际上只是告诉属性可以被什么设置。

  Any
  Item 
      Bool
      Undef
      Defined
          Value
              Num
                Int
              Str
                ClassName
          Ref
              ScalarRef
              ArrayRef
              HashRef
              CodeRef
              RegexpRef
              GlobRef
                FileHandle
              Object    
                  Role

这些是 isa 的默认选择。除了它们之外,isa 可以设置为任何类名,例如 isa => "URI"isa => "HTML::Treebuilder"

has 'foobar' => (
    isa => 'Str',
    is  => 'ro',
);

has 'foobar' => (
  isa => 'MooseObject',
  is  => 'ro',
);

Moose v0.26 引入了参数化类型[1]——没有人知道它们到底是什么,但它们提供了执行以下操作的能力

has 'foobar' => (
  isa => 'ArrayRef[URI]'
);

现在当您尝试存储到 ->foobar 时,您最好只向 ArrayRef 中添加 URI 元素,否则您将 croak

has 生成属性的核心是 is 属性。如果某物是 is => 'ro',用户将拥有一个指定名称的getter(只读访问器)。相反,如果提供的 value 是 is => 'rw',用户将拥有一个 getter/setter 混合(读写访问器)。

默认值是不生成任何类型的访问器。

hascoerce 属性比 isisa 复杂得多。它告诉 Moose,“嘿,老兄,如果你能把这个值变成我想要的数据类型,而不用一直这么麻烦,那会很酷。”

Moose 通常会听你的。

package MyMoose;
use Moose;
use Moose::Util::TypeConstraints;
use URI;

subtype 'URI'
	=> as 'Object'
	=> where { $_->isa('URI') };

coerce 'URI'
	=> from 'Str'
	=> via { URI->new($_) };

has 'myuri' => ( is => 'rw', isa => 'URI', coerce => 1 );

package main;
my $m = MyMoose->new;
$m->myuri( 'foobar' );

print ref $m->myuri;  ## print URI::_generic

要使用 coerce,您必须提供一个可以强制转换的子类型。

如果您设置了 weak_ref => 1,则不能使用 coerce => 1

Weak_ref 阻止属性中持有的引用计数被对象递增。这意味着,如果您有一个副本,并且一百个对象使用它并且 weak_ref => 1,您可以通过 undef'ing 您的一个副本成功地销毁它们。当 Perl 的垃圾收集器的引用计数达到零时,它将销毁该项目。使用 weak_ref,您的对象永远不会增加引用计数。这阻止了循环递归,但使您容易受到“从脚下撤走地毯”的风险,可以这么说。

package MyMoose;
use Moose;

has 'foo' => (
	isa      => 'ArrayRef',
	is       => 'ro',
	weak_ref => '1',
);

package main;

my $foo = [qw/ foo bar baz/];
my $m = MyMoose->new({ foo => $foo });

print @{ $m->foo };  ## prints foobarbaz

undef $foo;

## Can't use an undefined value as an ARRAY reference at test.pl line 21.
print @{ $m->foo };

这将接受存储在这个属性中的值预期已使用过的角色的名称。

required 的内容是 required => 1。所有必需的内容都必须在发送到 ->new 的哈希的引用中指定一个值。

package MyMoose;
use Moose;

has 'foo' => ( required => 1 );

package main;
my $m1 = MyMoose->new();             ## dies with error pinpointing cause
my $m2 = MyMoose->new({ foo => 4 }); ## Blisses in the fact the user is not a git
另请参见:default|#default|default

不想在编译时发生的事情... 延迟它!Lazy 需要一个默认值:您必须提供一个。

这个属性引入了一种全新的建模问题的方式。它带来了一种新的思维方式,不要做工作,直到需要完成。计算机程序终于有了拖延的能力,而不会让我感到压力。我讨厌将这种奢侈的权利限制在自己身上。

将 lazy/default 组合视为一个子程序,该子程序存储其返回值,因此它不必在后续调用中重新计算它。

在这个示例中,foobar 直到您请求它才会被设置;并且,在这个示例中,它会拉取一个网页,因此我们不想在知道我们需要结果之前执行这个可能很耗时的操作。

sub build_foobar { return get( 'http://www.example.com/somedatafeed' ); }

has 'foobar' => (
  isa     => Int,
  is      => 'rw',
  lazy    => 1,
  default => \&build_foobar,
);

auto_deref

[编辑 | 编辑源代码]

Auto_deref 是一种狡猾且具有欺骗性的东西。它不能修复 Perl,它只是让事情变得稍微简单一些。这个属性只是为 ArrayRef 返回一个扩展的 Array,或为 HashRef 返回一个 Hash。要使用auto_deref您必须有 isa=>ArrayRefisa=>HashRef 或两者之一的参数化类型:isa=>ArrayRef[Int]

has 'foo' => ( isa => 'HashRef', is => 'rw', auto_deref => 1 );


我的意思是欺骗性是什么意思呢auto_deref仍然只是简单地按值传递。

package Class;
use Moose;

has 'foo' => ( isa => 'ArrayRef', is => 'rw', auto_deref => 1 );

my $c = Class->new({ foo => [qw/foo bar baz/] });

## Uses auto_deref
s/.*// for $c->foo;
print $_ for $c->foo; # foo bar baz

## Uses manual deref
s/.*// for @{$c->foo};
print $_ for $c->foo; # Nada.
另请参见:lazy|#lazy|lazy

Default 具有两种截然不同的功能

  1. 设置一个默认的静态值。
  2. 在第一次调用访问器时,设置一个动态 lazy(lazy=>1)默认值

Default 的第一个也是最简单的应用,只是设置一个回退静态值

package Person;
use Moose;

has 'name' => (
 isa     => 'Str',
 is      => 'rw',
 default => "I'm too stupid to fill out a form",
);

package main;
my $p =  Person->new({ name => $_ });
print 'Greetings' . $p->name;

Default 的第二个实现要复杂得多。使用这种方法,您将属性声明为 lazy => 1(在编译时不执行任何操作),然后将 default 指向一个函数,例如 sub { DBI->connect }。当该函数被调用时,它会看到该插槽未设置(通过 predicate 测试失败),然后调用 default 指向的 sub,用返回值设置该插槽。该值被缓存起来,除非该插槽再次变为未设置状态——就像调用 clearer 的情况一样,否则不会重新计算。

metaclass

[编辑 | 编辑源代码]
另见:谓词|#predicate|谓词懒加载|#lazy|懒加载默认值|#default|默认值

如果您需要撤消对值所做的所有更改,clearer是一个很好的方法。假设您已经对以下内容进行了第一次调用:lazy => 1属性,它将初始值设置为相应的default. 假设您不喜欢此值,并且希望将其重置为从未调用过一样。在这种情况下,调用clearer指定的 方法将实现此目的。在内部,clearer会删除该槽位。

如果您在cleared 值上调用访问器,它将返回undef,除非存在lazy/default组合指定。在这种情况下,它将重新初始化,就像第一次调用一样。如果您调用clearer在一个普通的default上,可能看起来设置为undef. 事实上,该槽位不存在。要测试这一点,请参见 谓词

package MyMoose;
use Moose;

has 'foo' => (
	isa     => 'Int',
	is      => 'rw',
	clearer => 'clear_foo',
);

package main;
my $m = MyMoose->new;
$m->foo(5);
$m->clear_foo;

print "defined" if defined $m->foo;
print "exists" if exists $m->{foo};
print $m->{foo};
Use of uninitialized value in print at test.pl line 22.

切勿访问 Moose 对象的底层哈希,除非您正在为书籍编写丑陋的示例。Moose 应该抽象出它所祝福的内容。不要让人想起哈希的存在。

谓词所做的只是创建一个子例程,如果属性的相应槽位存在,则返回真 (1),如果不存在,则返回假 (0)。这是一个简单的便利属性。如果没有predicate => 'name',您将无法轻松区分该槽位是否存在,而不会违反面向对象的黑盒原则。谓词使此步骤变得容易,并允许您继续而无需查看 Moose 内部。谓词仅对以下情况有用:

  • 与它的家族函数clearer->(它删除槽位)进行交互。在该上下文中,可以将谓词视为clearer_test
  • 测试是否向构造函数提供了非必需属性。[2]
  • 测试具有defaultlazy => 1 是否已触发

谓词接受一个字符串,它用来生成谓词子例程的名称

predicate => 'my_predicate_sub'

在这里我们将看到它的实际应用

package MyMoose;
use Moose;

# To accept undef also as a valid data.
has 'ulcer' => (
  isa       => 'Str|Undef',
  is        => 'rw',
  predicate => 'is_sick',
);

package main;

my $m = MyMoose->new({ ulcer => 'painful' });
print $m->is_sick # true

## User doesn't know affliction
## Here Ulcer description isn't defined but we still have an ulcer
my $m = MyMoose->new({ ulcer => undef });
print $m->is_sick # true

触发器

[编辑 | 编辑源代码]

触发器是一个函数,在您写入访问器或使用构造函数设置值后调用它。但是,在撰写本文时,它们不会在使用以下内容设置的属性上触发:lazy_build, defaultbuilder. 使用以下内容设置触发器:CodeRef,它接收以下值:$self、$value 和 $oldValue。

use Moose;
sub shout { print 'Moosegasm' }
has 'foo' => ( is => 'rw', trigger => \&shout );

如果您想要一个触发器仅在您运行设置器时执行,请尝试更接近以下内容:[3]

after 'foo' => sub {};
after $self->meta->get_attribute('foo')->get_write_method => sub {}

如果您希望触发器在从构建器/默认值/lazy_build 构建的内容上触发,您要么必须在构建器/默认值/lazy_build 代码中显式展开触发器,要么在构建器周围使用一个包装器。这是因为触发器可以假定属性已设置并从中读取。但是,Moose 仅在构建器返回后设置属性,因此您不能简单地从构建器内部调用触发器。

around '_build_foo' => sub {
	my ( $sub, $self, @args ) = @_;
	my $foo = $self->$sub;
	$self->foo( $foo );
	$self->_init_foo;
	$foo;
};


另见:初始化器|#initializer|初始化器与初始化器的比较|#trigger_vs_initializer|与初始化器的比较

此属性有两个不同的用途

  • 它可以改变 Moose 用于设置属性的构造函数哈希中的键。
  • 您可以通过将其设置为 undef 来忽略构造函数中发送的任何值。

您可以通过显式设置来告诉 Moose 在构造函数的哈希 (->new( $constructorsHash )) 中使用不同的键init_arg为属性名称以外的任何内容。例如

package Class;
use Moose;

has 'foo' => ( isa => 'Str', is => 'rw', init_arg => 'bar' );

package main;
say Class->new({ bar => "BarValue" })->foo ## outputs "BarValue"

或者,您可以让 Moose 忽略构造函数的哈希。即使使用默认值,这也将起作用。

package Class;
use Moose;
has 'foo' => ( isa => 'Bool', is => 'rw', default => 1, init_arg => undef );

package main;
say Class->new({ foo => 0 })->foo; ## returns true

此属性不能被继承,也不能使用 +attr 修改

package Class;
use Moose::Role;
has 'foo' => ( isa => "Int", is => "ro" );
has "+foo" => (init_arg => "FoO");
Class->new({FoO=>1})

初始化器

[编辑 | 编辑源代码]

此功能有助于初始化(设置槽位)。当满足以下条件之一时,它会触发:

  • 存在默认值
  • 构造函数中发送了一个值
  • 从 lazy_build 计算出一个值
  • 从构建器计算出一个值
  • 或者,通过元数据触发,例如Class::MOP::Attributeset_initial_value

如果您没有在初始化器中设置槽位,该槽位将有效地设置为undef,假设类型系统允许它。

初始化器子例程接收四个值:$self、$value、$writerSubRef 和 $attributeMeta。其中 $attributeMeta 可能是 Class::MOP::Attribute 的实例。$writerSub 不是一个方法。

关于初始化器的一个非常重要的点是,它在类型系统之后触发。该值必须通过类型强制或成为有效类型才能在初始化器触发之前。

package MooseClass;
use Moose;

has "foo" => (
  isa => 'Value',
  is  => 'rw',

  initializer => sub {
    my ( $self, $value, $writer_sub_ref, $attribute_meta ) = @_;

    $writer_sub_ref->($value);
  }
);

my $c = MooseClass->new( { foo => 5 } );
say $c->foo;
另见:触发器|#trigger|触发器与初始化器的比较|#trigger_vs_initializer|与初始化器的比较

lazy_build

[编辑 | 编辑源代码]

Lazy_build 是一种属性,旨在简化类的快速构造。以下是它的组成部分:[4]

   #If your attribute name starts with an underscore:
   has '_foo' => (lazy_build => 1);
   #is the same as
   has '_foo' => (lazy => 1, required => 1, predicate => '_has_foo', clearer => '_clear_foo', builder => '_build__foo');
   # or
   has '_foo' => (lazy => 1, required => 1, predicate => '_has_foo', clearer => '_clear_foo', default => sub{shift->_build__foo});

   #If your attribute name does not start with an underscore:
   has 'foo' => (lazy_build => 1);
   #is the same as
   has 'foo' => (lazy => 1, required => 1, predicate => 'has_foo', clearer => 'clear_foo', builder => '_build_foo');
   # or
   has 'foo' => (lazy => 1, required => 1, predicate => 'has_foo', clearer => 'clear_foo', default => sub{shift->_build_foo});

此功能提供另一种将方法委托给其他模块的方法。

handles属性可以用不同的方式配置,具有不同的语法糖度

  • 数组
  • 哈希
  • 正则表达式

以下来自 HTML::TreeBuilderX::ASP_NET 的示例使用正则表达式配置

has 'hrf' => (
  isa          => 'HTTP::Request::Form',
  is         => 'ro',
  handles    => qr/.*/,
  lazy_build => 1,
);

一个好的通用用例是handles => qr/.*/,它告诉 Moose 将所有在您的包中声明但未重写的函数委托给在isa中指定的模块。这通常可以避免子类化和修改 ->new。

Traits,更准确地说是属性 Traits,是一种扩展属性功能的机制。

一个例子是 Moose 通过以下内容提供的功能:Nativetraits。这些将为所有容器类型提供帮助程序方法哈希, 数组;以及强类型Number, String, Bool, CouterCode. Native traits 具有可以通过额外的handles属性按需打开的功能。

has 'attributes' => (
    isa     => 'HashRef',
    traits  => ['Hash'],
    is      => 'ro',
    handles => { get_attr => 'get' },
);

消除混淆

[编辑 | 编辑源代码]

初始化器与默认值

[编辑 | 编辑源代码]

初始化器是默认过程的一部分。设置default的层级与触发初始化器的层级相同。默认值会将槽位初始化为设定的值。触发顺序defined初始化器对于属性是已定义的;但是,对于所有模块属性来说是完全未定义的。

对于属性已定义

has 'foo' => (
	isa => 'Str'
	, is => 'rw'
	, default => 'foobarbaz'
	, initializer => sub {
		my ( $self, $arg, $writerSub, $attributeMeta ) = @_;
		print $arg; # prints 'foobarbaz'
	}
);

对于属性集未定义:一个属性 (foo) 可能会触发它的初始化器,写入另一个属性 (bar),只是让 (bar) 被它自己的默认值覆盖。

has 'bar' => ( isa => 'Str', is => 'rw', default => 'bar' )
has 'foo' => (
	isa => 'Str'
	, is => 'rw'
	, default => 'foobarbaz'
	, initializer => sub {
		my ( $self, $arg, $writerSub, $attributeMeta ) = @_;
		$self->bar('foo');
	}
);
...
print MooseObject->new->bar ## prints bar
另见:默认值|#default|默认值初始化器|#initializer|初始化器

初始化器与触发器

[编辑 | 编辑源代码]

参数

两者触发器初始化器都使用 $self、$value 和 $attributeMeta。

触发器
$self、$value、$attributeMeta。
初始化器
$self、$value、$writerSub、$attributeMeta。


触发顺序

触发器在任何写入槽位的事件中都不触发可能被认为是一个错误。

Moose 0.58 中触发器和初始化器的比较
条件 示例 触发器 初始化器
构造函数 Class->new({foo=>1}) 触发 触发
lazy_build Class->new->foo 触发
builder Class->new 触发
default Class->new 触发
运行时显式 Class->new->foo(5) 触发

共存

如果一个触发器可用,初始化器将始终在触发器之前触发。但是,它仍然不会在运行时显式将值设置为插槽时触发。

您可以使用触发器在运行时显式提供值时使子触发。这种方法的缺点是在构造函数提供值时会触发两次:一次来自触发器,一次来自初始化器.

has 'foo' => ( isa => 'Value', is => 'rw', initializer => \&bar, trigger => \&bar );
sub bar { stuff }
另请参见:trigger|#trigger|trigger 和 initializer|#initializer|initializer
  1. ^ 由于 Moose 的继承以及缺乏外部文档,几乎不可能确定我们是否列出了所有内容。例如,在官方 Moose 文档中,没有提到 ->clear->predicate;但是,它们在 Class::MOP::Attribute 中有记录。

参考资料

[编辑 | 编辑源代码]
  1. Little, Stevan (2007-09-27). "Changes/Revision History". Retrieved 2007-10-20.
  2. 以及必要的那些,但如果没有提供,则会 die()
  3. 感谢 irc.perl.org 的 Sartak 提供的信息
  4. Little, Stevan. "Moose::Meta::Attribute". Moose 0.32. Infinity Interactive. {{cite web}}: Unknown parameter |access_date= ignored (help); Unknown parameter |coauthors= ignored (|author= suggested) (help)
华夏公益教科书