如何使 Perl 更优雅

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

freephotocc 通过 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 中开始一个新项目之前,我建议阅读现代 Perl; 除此之外,它还描述了如何使用 Moose 创建类/对象。

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

如何创建一个“Point”

百闻不如一见。因此,让我们从定义一个 Point 类开始,它具有两个不可变的属性 xy,以及一个接受命名参数的构造函数。以下是它在 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 的语法糖(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 更快。

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

2 条评论

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

你是对的。我一定是戴着我的 Perl 6 眼镜看的。谢谢你的指出!

回复 作者 Tux (未验证)

© . All rights reserved.