这是一系列关于将代码从 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 中开始一个新项目之前,我建议阅读现代 Perl; 除此之外,它还描述了如何使用 Moose 创建类/对象。
为了简单起见,本文将描述基本 Perl 5 和基本 Perl 6 对象创建之间的一般差异。
如何创建一个“Point”
百闻不如一见。因此,让我们从定义一个 Point 类开始,它具有两个不可变的属性 x 和 y,以及一个接受命名参数的构造函数。以下是它在 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 的语法糖(Pair,其中键与值的变量名称相同)。在 Perl 6 中,这可以表示为 :$x。这将使上面的 new 方法看起来像这样
$ Perl 6
...
method new( Int $x = 0, Int $y = 0 ) { self.bless( :$x, :$y ) }
在此之后,在 Perl 5 和 Perl 6 之间创建 Point 对象非常相似
# 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 条评论