如何使 Perl 更优雅

在本系列比较 Perl 5 和 Perl 6 的第七篇文章中,了解它们如何处理创建类或对象。
155 位读者喜欢这个。
Getting started with Perlbrew

freephotocc via Pixabay CC0

这是一系列文章中的第七篇,关于将代码从 Perl 5 迁移到 Perl 6。本文着眼于如何在 Perl 6 中创建类(对象),以及它与 Perl 5 的不同之处。

Perl 5 有一种非常基础的面向对象形式,你可以认为它是在事后才附加的。人们已经进行了几次尝试来改善这种情况,最值得注意的是 Moose,它“主要基于 Perl 6 对象系统,并借鉴了 CLOS、Smalltalk 和许多其他语言的最佳想法。” 反过来,Perl 6 对象创建逻辑也从 Moose 中吸取了一些经验教训。

Moose 启发了 Perl 5 中许多其他的现代对象系统,最值得注意的是 MooMouse。在你开始在 Perl 5 中启动一个新项目之前,我建议阅读Modern Perl; 除此之外,它还描述了如何使用 Moose 创建类/对象。

为了简单起见,本文将描述基本 Perl 5 和基本 Perl 6 对象创建之间的一般差异。

如何创建一个“Point”

一图胜千言。因此,让我们从定义一个带有两个不可变属性 xyPoint 类和一个采用命名参数的构造函数开始。这是它在 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 方法,也无需代码来创建 xy 的访问器。还要注意,在 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 使用 . (句点)来调用方法,而不是 -> (连字符 + 大于号)。

错误检查

在理想的世界中,方法的所有参数都将被正确指定。不幸的是,我们生活在一个不理想的世界中,因此最好在对象创建中添加错误检查。假设你想确保同时指定了 xy 并且它们是整数值。在 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 rwis required 等特征)。在该方法内部,你可以对对象中的属性执行任何操作。

请注意,TWEAK 方法最好实现为所谓的 submethodsubmethod 是一种特殊类型的方法,只能在类本身上执行,不能在任何子类上执行。换句话说,此方法具有 子例程 的可见性。

位置参数

最后,有时对象的接口非常清晰,以至于你根本不需要命名参数。相反,你想使用位置参数。在 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 更快。

标签
Elizabeth Mattijsen
Elizabeth Mattijsen 自 1978 年以来一直以编程为生,使用过各种(大部分现在已经不再使用)编程语言,之后她开始使用 Perl 4 编程。1994 年,她创立了荷兰第一家商业网站开发公司,使用 Perl 5 作为主要编程语言。从 2003 年开始,她参与了一项在线酒店预订服务的快速增长。

2 条评论

关于提供默认值,我肯定会将该代码简化为«$args{x} //= 0;»

Creative Commons License本作品采用 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.