取势 明道 优术

作者为 扶 凯 发表

什么是 Perl 的类型

Moose 提供自己的属性的类型系统.你也能使用 MooseX 模块帮助你来验证类中方法上的参数.
Moose 的类型系统是基于一个Perl 5中的自己的隐式类型的组合和一些 Perl6 的概念.您可以创建自定义约束的 subtypes,因此很容易写出表达任何类型来做参数验证.
类型都有一个名字,你也可以重命名使用他们的名字空间,所以很容易地共享类型到所有的大型应用程序.
但,这并不是真实的类型系统, 它只是一个更加高级的参数检查的系统.可以让你的参数通过一个约束的名字来进行检查.
虽然这行说,它仍然是相当的有用的,我们认为这是使用 Moose 既有趣又强大的事情之一.使用这个类型系统的好处是让你可以更早的确保你得到的数据是可用正确的.这也极大地使你的代码的提升更好的可维护性.

类型

基本的 Moose 的类型层次如下:

  Any
  Item
      Bool
      Maybe[`a]
      Undef
      Defined
          Value
              Str
                  Num
                      Int
                  ClassName
                  RoleName
          Ref
              ScalarRef[`a]
              ArrayRef[`a]
              HashRef[`a]
              CodeRef
              RegexpRef
              GlobRef
                  FileHandle
              Object

实际上, Any 和 Item 只是概念上不同.Item 是作为一些参数类型层次结构中的顶级类型.注 HashRef[Int] 这种结构中,Int 是指的 Hash 的值必须是 Int 型.
这些 types 的其余部分对应于现有的 Perl 的概念.特别是:

     BOOL 类型认为 1 为真和 undef, 0或空字符串作为假.
     [`A] 可以接受 `a 或者 undef.
     Num 接受任何 Perl 认为的数字(详见在 Scalar::Util 模块中的"looks_like_number").
     ClassName 和 RoleName 接受字符串(类名称或角色名称).类/角色是必须已经载入了并经过约束检查后的.
     FileHandle 可以接受一个 IO::Handle 对象或一个内置的 Perl 的文件句柄(详见在 Scalar::Util 模块中的“openhandle).
     Object 接受任何 blessed 过的引用 .

"[`a]" 的类型可以被参数化.因此,不是只是简单的 ArrayRef,比如,我们希望 ArrayRef[Int] 来代替这个.我们甚至可以放入 HashRef[ArrayRef[Str]] .
在这类型当中 “参数类型项[`a]” 特别值得在提一提.单独使用,它并不能表示真实的任何东西(等效于 Item ).在这当它代替着一个参数类型项,这意味着要么是 undef 或 Item 中参数的类型.所以 “参数类型项[`a]”表示一个整数,或者undef.
更加详细的类型结构层次,可以看 Moose::Util::TypeConstraints.

什么是类型

你需要意识到重要的,类型并不是一个类(或包).类型只是对象名字和属性约束(准确的讲是 Moose::Meta::TypeConstraint 对象中).Moose 会维护一个全局的注册过的类型名字.对象可以被转换成相应的名称,如 Num.
然而,类名可以是类型名称.当你使用 Moose 定义一个新的类时,它自动关联的定义了一个相关的类型名称(这时类型名等于类名):

package MyApp::User;

use Moose;

现在你可以使用 'MyApp::User' 的类型名了.因为自动会生成这样一个类型名.

  has creator => (
      is  => 'ro',
      isa => 'MyApp::User',
  );

但是,在非 Moose 的类中是不能这样的,这时您可能需要显式声明类的类型.这会让你有点糊涂,因为 Moose 的类中为属性设了一个 ISA 选项,可以在其中设置任何未知类型的名称.所以可以这样:

  has 'birth_date' => (
      is  => 'ro',
      isa => 'DateTime',
  );

 一般,当 Moose 见到一个未知的名字时,都会假设这个名字是一个类名.

subtype 'ModernDateTime'
      => as 'DateTime'
      => where { $_->year() >= 1980 }
      => message { 'The date you provided is not modern enough' };

  has 'valid_dates' => (
      is  => 'ro',
      isa => 'ArrayRef[DateTime]',
  );

在上面这二个实例中,Moose 都会假设这个 DateTime 是一个类名.

SUBTYPES(子类型)

Moose 使用的子类型( subtypes)是其内置的层次结构中的 .例如,int 是 Num 中的一个子类型.
子类型是定义在父类型和约束下的.首先检查由父(S)中定义的任何约束,其次是子类型定义的约束,一个有效的子类型的值必须通过所有这些检查.
通常情况下,子类型会使用父的约束,使其达到更加具体的约束.
每个子类型也可以定义自己约束失败的信息,比如你见到的出错信息"你提供的值为 20,他不是一个有效的值.这个值必须是 1 – 10",这比默认定义的信息更加友好.默认类型检查只会提示这个值无效.还有一个更加友好的做法,安装 Devel::PartialDump 这个模块.这时 Moose 会使用这个来解析并显示未知的值.
这是一个简单有用的子类型的例子.

  subtype 'PositiveInt',
      as 'Int',
      where { $_ > 0 },
      message { "The number you provided, $_, was not a positive number" };

 注意这个中的语法糖都是由 Moose::Util::TypeConstraints 中导出来的.

类型名

类型名在 Perl 的环境中是全局存在的.在 Moose 内部,会给注册的名字映射到类型对象中.
如果你在同一个进程中有多个 apps 或者函数库使用了 Moose .你可能会发生一些名字冲突的问题.所以推荐你给你的类型名字加个相同的前缀,来防止这种类型的碰撞.
例如,调用“PositiveInt”类型,而不是调用的 "MyApp::Type::PositiveInt" 或 "MyApp::Types::PositiveInt"..我们建议你集中定义在叫 MyApp::Types 一个单一的包名中,这样当前包还是可以使用这个来加载您的应用程序中的其他类.
然而,在您这样做前,建议你应该看看在 MooseX::Types 模块.此模块可以很容易地创建一个“类型库(type library)”模块,它可以导出你的类型,如 Perl 常数.

has 'counter' => (is => 'rw', isa => PositiveInt);

这使得你可以使用一个简短的名称而不需要到处都使用完全限定名( fully qualify the name),它还允许您轻松地创建参数类型:

has 'counts' => (is => 'ro', isa => HashRef[PositiveInt]);

这个模块会在编译的时候就检查你的名字,这个模块它通常在解析较复杂的字符串类型时更加健壮.

 

强制类型转换(COERCION )

强制类型转换可以让 Moose 自动的转换一种类型到另一种类型.

  subtype 'ArrayRefOfInts',
      as 'ArrayRef[Int]';

  coerce 'ArrayRefOfInts',
      from 'Int',
      via { [ $_ ] };

 

你注意到我们创建了一个子类型,而不是直接转换 ArrayRef[Int] .这是一个坏主意,添加强制类型到原始内置类型.
强制类型转换是全局的,就像类型名称.这是上面提到的另外一个使用良好的命名空间的理由.
Moose 绝不会试图强制值的类型转换,除非您明确要求,这需要你通过设置属性选项中的强制为真.

  package Foo;

  has 'sizes' => (
      is     => 'ro',
      isa    => 'ArrayRefOfInts',
      coerce => 1,
  );

  Foo->new( sizes => 42 );

 

此代码示例将是正确的,而新创建的对象将有 [42] 作为其 sizes 属性的值.

递归转换

一个递归类型转换,转换类型参数的类型参数.下面是这样的例子.

  subtype 'HexNum',
      as 'Str',
      where { /[a-f0-9]/i };

  coerce 'Int',
      from 'HexNum',
      via { hex $_ };

  has 'sizes' => (
      is     => 'ro',
      isa    => 'ArrayRef[Int]',
      coerce => 1,
  );

 

如果我们试图在 sizes 属性中放一个十六进制数字的数组引用 ,Moose 不会做任何转换.
但是,您可以定义一个子类型,在两个参数类型之间进行类型转换.

  subtype 'ArrayRefOfHexNums',
      as 'ArrayRef[HexNum]';

  subtype 'ArrayRefOfInts',
      as 'ArrayRef[Int]';

  coerce 'ArrayRefOfInts',
      from 'ArrayRefOfHexNums',
      via { [ map { hex } @{$_} ] };

  Foo->new( sizes => [ 'a1', 'ff', '22' ] );

 

现在 Moose 会转换这个十六进制数字到整数.
Moose 不会尝试级联的强制转换,所以它不会强制转换这个单一的十六进制数.所以要做到这一点,我们需要定义一个单独的强制类型转换:

  coerce 'ArrayRefOfInts',
      from 'HexNum',
      via { [ hex $_ ] };

 

虽然这可能确实变得非常冗长,在这个强制类型转换的魔法中,我们认为这样最好的,因为单独写一个会非常清楚的.

类型联合

Moose 允许你定义属性是两个或多个不同类型.例如,我们可能会允许 Object 或文件句柄:

  has 'output' => (
      is  => 'rw',
      isa => 'Object | FileHandle',
  );

 

Moose 会解析这个字符串,并认出您正在创建类型是什么.output 的属性会接受任何类型的对象或 unblessed 的文件句柄.和平时一样,在正确的时候做正确的事.
每当你使用一个类型联合,你应该考虑使用强制类型转换可能是一个更好的答案.
对于我们上面的例子中,需要强调的是,这个地方使用对象的 print 的方法更加合适.

duck_type 'CanPrint', [qw(print)];

我们可以转换这个 file handles 到一个对象,只需要简单的封装这个类:

  package FHWrapper;

  use Moose;

  has 'handle' => (
      is  => 'rw',
      isa => 'FileHandle',
  );

  sub print {
      my $self = shift;
      my $fh   = $self->handle();

      print {$fh} @_;
  }

 

现在我们能定义一个强制转换,从一个 FileHandle 转到我们包装的类:

  coerce 'CanPrint'
      => from 'FileHandle'
      => via { FHWrapper->new( handle => $_ ) };

  has 'output' => (
      is     => 'rw',
      isa    => 'CanPrint',
      coerce => 1,
  );

 

这种模式使用一个类型转换,而不是一个类型联合,这将有助于使你的类的内部简单些.

类型创建助手

在 Moose::Util::TypeConstraints 的模块中,导出了几个帮助你创建各种类的类型.包含 class_type, role_type,maybe_type, 和 duck_type.相关信息,请查看相关的文档.
值得注意的助手之一是 enum,可以让你创建一个有 Str 的子类型,让你指定他可能出现的值.

enum 'RGB', [qw( red green blue )];

这建了一个类型名叫 RGB ,可以有 red green blue 三个选择.

 

 匿名类型

类型转换的功能会返回一个类型的对象,这个类型的对象可以使用任何你能使用的类型名做为 parent type 用这个来做为属性的 isa 的值

  has 'size' => (
      is  => 'ro',
      isa => subtype( 'Int' => where { $_ > 0 } ),
  );

这对于你只想使用一次的类型,是非常方便的,也不用想着怎么给他一个名字.

 

确认方法的参数

Moose 不提供任何验证方法的参数手段.不过,也有几个 MooseX CPAN 上的扩展,可以让你这样做.
这个最最简单的是使用 MooseX::Params::Validate.这可以让你通过 Moose 的类型.来对你的参数名的值进行检查.

  use Moose;
  use MooseX::Params::Validate;

  sub foo {
      my $self   = shift;
      my %params = validated_hash(
          \@_,
          bar => { isa => 'Str', default => 'Moose' },
      );
      ...
  }

 

MooseX::Params::Validate 同样也支持强制类型转换.
有一些更强大的扩展,支持方法的参数验证,使用 Moose 中的类型,只要加载 MooseX::Method::Signatures ,这个模块会给我们提供全面的关键字.

 method morning ( Str $name ) {
      $self->say("Good morning ${name}!");
  }

 

加载顺序问题
由于在运行的时候定义 Moose 类型,你可能会碰到的加载顺序问题.特别是,您可能要使用一个类的类型约束,在此之前社个类型已定义过了.
为了改善这个问题,我们建议定义所有定制的类型在同一个模块中名中MyApp::Types,然后就可以在你的所有其他模块加载这个模块.

 

 

 

来了就留个评论吧! 没有评论