这是一系列文章中的第七篇,关于将代码从 Perl 5 迁移到 Perl 6。本文着眼于如何在 Perl 6 中创建类(对象),以及它与 Perl 5 的不同之处。
Perl 5 有一种非常基础的面向对象形式,你可以认为它是在事后才附加的。人们已经进行了几次尝试来改善这种情况,最值得注意的是 Moose,它“主要基于 Perl 6 对象系统,并借鉴了 CLOS、Smalltalk 和许多其他语言的最佳想法。” 反过来,Perl 6 对象创建逻辑也从 Moose 中吸取了一些经验教训。
Moose 启发了 Perl 5 中许多其他的现代对象系统,最值得注意的是 Moo 和 Mouse。在你开始在 Perl 5 中启动一个新项目之前,我建议阅读Modern Perl; 除此之外,它还描述了如何使用 Moose 创建类/对象。
为了简单起见,本文将描述基本 Perl 5 和基本 Perl 6 对象创建之间的一般差异。
如何创建一个“Point”
一图胜千言。因此,让我们从定义一个带有两个不可变属性 x 和 y 的 Point 类和一个采用命名参数的构造函数开始。这是它在 Perl 5 中的样子
# Perl 5
package Point {
sub new {
my $class = shift;
my %args = @_; # maps remaining args as key / value into hash
bless \%args, $class
}
sub x { shift->{x} }
sub y { shift->{y} }
}
以及在 Perl 6 中的样子
# Perl 6
class Point {
has $.x;
has $.y;
}
正如你所看到的,Perl 6 语法更具声明性;无需编写代码来拥有 new 方法,也无需代码来创建 x 和 y 的访问器。还要注意,在 Perl 6 中你需要指定 class 而不是 package。
之后,在 Perl 5 和 Perl 6 中创建 Point 对象非常相似
# Perl 5
my $point = Point->new( x => 42, y = 666 );
# Perl 6
my $point = Point.new( x => 42, y => 666 );
唯一的区别是 Perl 6 使用 . (句点)来调用方法,而不是 -> (连字符 + 大于号)。
错误检查
在理想的世界中,方法的所有参数都将被正确指定。不幸的是,我们生活在一个不理想的世界中,因此最好在对象创建中添加错误检查。假设你想确保同时指定了 x 和 y 并且它们是整数值。在 Perl 5 中,你可以这样做
# Perl 5
package Point {
sub new {
my ( $class, %args ) = @_;
die "The attribute 'x' is required" unless exists $args{x};
die "The attribute 'y' is required" unless exists $args{y};
die "Type check failed on 'x'" unless $args{x} =~ /^-?\d+\z/;
die "Type check failed on 'y'" unless $args{y} =~ /^-?\d+\z/;
bless \%args, $class
}
sub x { shift->{x} }
sub y { shift->{y} }
}
请原谅 /^-?\d+\z/ 行噪声。这是一个正则表达式,用于检查字符串开头的可选 (?) 连字符 (-),该字符串由一个或多个十进制数字 (\d+) 组成,直到字符串结尾 (\z)。
这需要相当多的额外样板代码。当然,你可以将其抽象成一个 is_valid 子例程,像这样
# Perl 5
sub is_valid {
my $args = shift;
for (@_) { # loop over all keys specified
die "The attribute '$_' is required" unless exists $args->{$_};
die "Type check failed on '$_'" unless $args->{$_} =~ /^-?\d+\z/;
}
1
}
或者你可以使用 CPAN 上的许多参数验证模块之一,例如 Params::Validate。无论如何,你的代码看起来像这样
# Perl 5
package Point {
sub new {
my ( $class, %args ) = @_;
bless \%args, $class if is_valid(\%args,'x','y');
}
sub x { shift->{x} }
sub y { shift->{y} }
}
Point->new( x => 42, y => 666 ); # ok
Point->new( x => 42 ); # 'y' missing
Point->new( x => "foo", y => 666 ); # 'x' is not an integer
如果你使用 Moose,你的代码看起来像这样
# Perl 5
package Point;
use Moose;
has 'x' => ( is => 'ro', isa => 'Int', required => 1);
has 'y' => ( is => 'ro', isa => 'Int', required => 1);
no Moose;
__PACKAGE__->meta->make_immutable;
Point->new( x => 42, y => 666 ); # ok
Point->new( x => 42 ); # 'y' missing
Point->new( x => "foo", y => 666 ); # 'x' is not an integer
请注意,使用像 Moose 这样的对象系统,你不需要像在 Perl 6 中那样创建 new 子例程。
然而,在 Perl 6 中,这一切都是内置的。 is required 属性特征表明 必须 指定属性。如果提供的值不是可接受的类型,那么指定类型(例如,Int)会自动抛出一个类型检查异常
# Perl 6
class Point {
has Int $.x is required;
has Int $.y is required;
}
Point.new( x => 42, y => 666 ); # ok
Point.new( x => 42 ); # 'y' missing
Point.new( x => "foo", y => 666 ); # 'x' is not an integer
提供默认值
或者,你可能希望使属性成为可选的,并在未指定时将其初始化为 0。在 Perl 5 中,看起来可能是这样
# Perl 5
package Point {
sub new {
my ( $class, %args ) = @_;
$args{x} = 0 unless exists $args{x}; # initialize to 0 is not given
$args{y} = 0 unless exists $args{y};
bless \%args, $class if is_valid( \%args, 'x', 'y' );
}
sub x { shift->{x} }
sub y { shift->{y} }
}
在 Perl 6 中,你将在每个属性声明中添加一个带有默认值的赋值
# Perl 6
class Point {
has Int $.x = 0; # initialize to 0 if not given
has Int $.y = 0;
}
提供 mutator
到目前为止,在类/对象示例中,对象的属性是不可变的。在创建对象后,不能通过通常的方式更改它们。
在 Perl 5 中,有多种方法可以创建一个 mutator(对象上的一种方法,用于更改属性的值)。最简单的方法是创建一个单独的子例程,它将在对象中设置该值
# Perl 5
...
sub set_x {
my $object = shift;
$object->{x} = shift;
}
可以缩短为
# Perl 5
...
sub set_x { $_[0]->{x} = $_[1] } # access elements in @_ directly
因此你可以像这样使用它
# Perl 5
my $point = Point->new( x => 42, y => 666 );
$point->set_x(314);
有些人更喜欢对访问和修改属性使用相同的子例程名称。指定一个参数则意味着该子例程应该用作 mutator
# Perl 5
...
sub x {
my $object = shift;
@_ ? $object->{x} = shift : $object->{x}
}
可以缩短为
# Perl 5
...
sub x { @_ > 1 ? $_[0]->{x} = $_[1] : $_[0]->{x} }
因此你可以像这样使用它
# Perl 5
my $point = Point->new( x => 42, y => 666 );
$point->x(314);
这是一种经常使用的方式,但这取决于 Perl 5 中对象是如何实现的。由于 Perl 5 中的对象通常只是一个带有额外好处的哈希引用,所以你 可以 将该对象用作哈希引用,并直接访问底层哈希中的键。但这会破坏对象的封装,并绕过 mutator 可能进行的任何额外检查
# Perl 5
my $point = Point->new( x => 42, y => 666 );
$point->{x} = 314; # change x to 314 unconditionally: dirty but fast
创建也可以用作 mutator 的访问器的“官方”方法是使用 lvalue 子例程,但这在 Perl 5 中由于各种原因并不常用。 然而,它非常接近 mutator 在 Perl 6 中的工作方式
# Perl 5
...
sub x : lvalue { shift->{x} } # make "x" an lvalue sub
因此你可以像这样使用它
# Perl 5
my $point = Point->new( x => 42, y => 666 );
$point->x = 314; # just as if $point->x is a variable
在 Perl 6 中,允许将访问器用作 mutator 也是以声明方式完成的,方法是在属性声明中使用 is rw 特征,就像 is required 特征一样
# Perl 6
class Point {
has Int $.x is rw = 0; # allowed to change, default is 0
has Int $.y is rw = 0;
}
这允许你在 Perl 6 中像这样使用它
# Perl 6
my $point = Point.new( x => 42, y => 666 );
$point.x = 314; # just as if $point.x is a variable
如果你不喜欢 Perl 6 中 mutator 的工作方式,你可以通过为它们添加一个方法来创建你自己的 mutator。例如,Perl 5 中的 set_x 用例在 Perl 6 中可以像这样
# Perl 6
class Point {
has $.x;
has $.y;
method set_x($new) { $!x = $new }
method set_y($new) { $!y = $new }
}
但是等等:$!x 中的感叹号是干什么用的???
! 表示类中属性的真实名称;它可以直接访问对象中的属性。让我们退一步,看看属性的所谓 twigil (即,辅助 sigil)意味着什么。
'!' twigil
像 $!x 这样的属性声明中的 ! 表示该属性是私有的。这意味着你无法从外部访问该属性,除非类的开发者提供了这样做的手段。这也意味着它不能通过调用 .new 进行初始化。
用于访问私有属性值的方法可以非常简单
# Perl 6
class Point {
has $!x; # ! indicates a private attribute
has $!y;
method x() { $!x } # return private attribute value
method y() { $!y }
}
事实上,如果你使用 . twigil 声明属性,那么这几乎就是自动发生的事情
'.' twigil
像 $.x 这样的属性声明中的 . 表示该属性是公共的。这意味着将为其创建一个访问器方法(很像上面具有私有属性方法的示例)。这也意味着可以通过调用 .new 初始化该属性。
如果你以其他方式使用属性形式 $.x,则你不是指该属性,而是指其访问器。它是 self.x 的语法糖。但是 $.x 形式的优势在于你可以轻松地在字符串中进行插值。此外,访问器可以被子类覆盖
# Perl 6
class Answer {
has $.x = 42;
method message() { "The answer is $.x" } # use accessor in message
}
class Fake is Answer { # subclassing is done with "is" trait
method x() { 666 } # override the accessor in Answer
}
say Answer.new.message; # The answer is 42
say Fake.new.message; # The answer is 666 (even though $!x is 42)
调整对象创建
有时你需要对对象执行额外的检查或调整,然后才能使用它。无需深入研究 在 Perl 6 中创建对象的细节,你通常可以通过提供 TWEAK 方法来完成你需要的所有调整。假设你也想允许将值 314 视为 666 的替代方法
# Perl 6
class Answer {
has Int $.x = 42;
submethod TWEAK() {
$!x = 666 if $!x == 314; # 100 x pi is also bad
}
}
如果一个类有一个 TWEAK 方法,它将在所有参数都被处理并适当地分配给属性之后被调用(包括分配任何默认值和处理诸如 is rw 和 is required 等特征)。在该方法内部,你可以对对象中的属性执行任何操作。
请注意,TWEAK 方法最好实现为所谓的 submethod。 submethod 是一种特殊类型的方法,只能在类本身上执行,不能在任何子类上执行。换句话说,此方法具有 子例程 的可见性。
位置参数
最后,有时对象的接口非常清晰,以至于你根本不需要命名参数。相反,你想使用位置参数。在 Perl 5 中,看起来可能是这样
# Perl 5
package Point {
sub new {
my ( $class, $x, $y ) = @_;
bless { x => $x, y => $y }, $class
}
sub x { shift->{x} }
sub y { shift->{y} }
}
即使 Perl 6 中的对象创建针对使用命名参数进行了优化,如果你愿意,你可以使用位置参数。在这种情况下,你必须创建你自己的“new”方法。顺便说一句,Perl 6 中的 new 方法没有任何特别之处。你可以创建你自己的,或者你可以创建一个具有另一个名称的方法来充当对象构造函数
# Perl 6
class Point {
has $.x;
has $.y;
method new( $x, $y ) {
self.bless( x => $x, y => $y )
}
}
这看起来与 Perl 5 非常相似,但存在细微的差异。在 Perl 6 中,位置参数是强制性的(除非声明为可选)。 使用默认值使其成为可选的方式与属性声明的方式几乎相同,指示类型的方式也几乎相同:你在 new 方法的签名中指定它们
# Perl 6
...
method new( Int $x = 0, Int $y = 0 ) {
self.bless( x => $x, y => $y )
}
bless 方法提供了 Perl 6 中使用给定命名参数创建对象的逻辑:它的接口与 new 方法的默认实现相同。 你可以随时调用它来创建一个类的实例化对象。
不要重复自己 (DRY) 是你应该始终使用的原则。 在 Perl 6 中使其更容易 DRY 的一个示例是 x => $x 的语法糖(其中 key 具有与 value 的变量相同的名称)。 在 Perl 6 中,这可以表示为 :$x。 这将使上面的 new 方法看起来像下面这样
$ Perl 6
...
method new( Int $x = 0, Int $y = 0 ) { self.bless( :$x, :$y ) }
之后,创建 Point 对象在 Perl 5 和 Perl 6 中非常相似
# Perl 5
my $point = Point->new( 42, 666 );
# Perl 6
my $point = Point.new( 42, 666 );
总结
在 Perl 6 中创建类主要采用声明式,而在标准 Perl 5 中创建对象主要采用过程式。Perl 6 中类的定义方式在语义上与 Moose 非常相似。这是因为 Moose 的灵感来自 Perl 6 对象创建模型的设计,反之亦然。
对对象创建的性能问题的关注一直是 Perl 5 和 Perl 6 的重点。尽管 Perl 6 在对象创建方面提供了比 Perl 5 更多的功能,但基准测试表明,Perl 6 最近在创建和访问对象方面变得比 Perl 5 更快。
2 条评论