老式面向对象Perl

如果您需要编写无依赖的面向对象Perl代码,则必须使用老式Perl语法。本文描述了老式面向对象Perl的主要功能,包括类声明、构造函数、析构函数、方法、属性、访问器和继承。

如果您想使用现代Perl工具编写面向对象Perl代码,请考虑使用Moose进行功能丰富的实现,包括类型检查、角色和访问器方法。另一个选择是Moo,它提供了一个快速的、简约的实现,没有Moose的糖(或开销)。

Perl类是在带有包声明的Perl模块文件 (*.pm) 中定义的。至少,类必须包含一个构造函数方法,并可选择包含其他类方法和属性。与所有Perl包一样,它必须返回一个真值(通常是1,放在文件末尾)。一个最小示例Perl类如下所示

# This is the package declaration
package Shape;

# This is the constructor method
sub new {
    return bless {}, shift;
}

1;

示例Shape类必须保存为名为‘Shape.pm’的文件。

构造函数方法

构造函数方法是一个Perl子程序,它返回一个类的实例的对象。按照惯例,构造函数方法的名称为‘new’,但它可以是任何有效的子程序名称。构造函数方法通过在哈希引用上使用bless函数和类名(包名)来实现。当调用new时,包名‘Shape’位于默认数组@_中,使用shift将包名从@_中取出并传递给bless函数。让我们修改构造函数方法以更清楚地传达这种行为

package Shape;

sub new {
    my $class = shift;
    my $self = {};
    return bless $self, $class;
}

1;

这种编写构造函数的风格是Perl代码中经常看到的一种常见模式。它不仅更清楚地传达了代码的行为,而且还允许轻松扩展到$self,例如添加默认属性和/或接受初始化参数。

属性

基本上,Perl对象是对哈希引用的bless引用。这个哈希引用存储了对象的属性数据,即键值对。让我们给我们的Shape类添加一些有用的属性

package Shape;

sub new {
    my $class = shift;
    my $self = {
        color  => 'black',
        length => 1,
        width  => 1,
    };
    return bless $self, $class;
}

1;

Shape类已被赋予颜色、长度和宽度属性。我们可以通过使用我们的Shape类在Perl程序中访问这些属性,让我们称这个程序为‘Draw.pl’(它应该保存与Shape.pm相同的目录中)

use strict;
use warnings;
use feature qw/say/;
use Shape;

# create a new Shape object
my $shape = Shape->new;

# print the shape object attributes
say $shape->{color};
say $shape->{length};
say $shape->{width};

让我们回顾一下关键行:‘use feature qw/say/;’启用say函数,它打印并追加换行符。‘use Shape;’导入Shape类,my $shape = Shape->new;创建一个新的Shape对象。使用$shape,我们可以使用花括号括起来的属性名称的解引用箭头->访问属性,并使用say函数打印属性。

使用构造函数参数设置属性值

目前,Shape.pm相当僵化;我们使用的每个Shape对象都将具有相同颜色、长度和宽度的值。我们可以通过接受设置颜色、长度和宽度属性值的参数来增加Shape类的灵活性。让我们相应地修改Shape.pm的构造函数

package Shape;

sub new {
    my ($class, $args) = @_;
    my $self = {
        color  => $args->{color},
        length => $args->{length},
        width  => $args->{width},
    };
    return bless $self, $class;
}

1;

在Perl中调用任何子程序时,参数都包含在默认数组变量@_中。Shape.pm构造函数现在期望一个包含属性值的哈希引用作为其参数,并将其分配给$args。我们可以更新Draw.pl以传递这些参数

use strict;
use warnings;
use feature qw/say/;
use Shape;

# pass color, length and width arguments to the constructor
my $shape = Shape->new({
                color => 'red',
                length=> 2,
                width => 2,
            });

# print the shape object attributes
say $shape->{color};
say $shape->{length};
say $shape->{width};

创建默认属性值

现在我们可以创建不同颜色、长度和宽度的Shape对象。但如果在我们的Perl程序中没有传递所有参数呢?在这种情况下,对象将使用空值初始化这些属性。为了避免这种情况,我们可以在对象构造时设置默认值,如果存在参数则会覆盖这些默认值。我们可以在Shape.pm中使用逻辑或运算符||来达到这个效果。

package Shape;

sub new {
    my ($class, $args) = @_;
    my $self = {
        color  => $args->{color} || 'black',
        length => $args->{length} || 1,
        width  => $args->{width} || 1,
    };
    return bless $self, $class;
}

1;

现在我们拥有了最好的两者:当使用Shape.pm时,您可以选择性地传递属性值,或者Shape将使用默认值构造。我们可以通过更新和运行Draw.pl来证明这一点。

use strict;
use warnings;
use feature qw/say/;
use Shape;

# pass color, length and width arguments to the constructor
my $red_shape = Shape->new({
                    color => 'red',
                });
# use the default attribute values
my $black_shape = Shape->new;

say $red_shape->{color};
say $black_shape->{color};

在Draw.pl中,我们使用一个参数对(color => red)初始化了$red_shape,但对于$black_shape,我们没有提供任何参数,所以颜色属性默认为黑色。

动态添加属性

可能向对象的哈希引用中插入新属性,例如,我们可以使用长度和宽度属性计算形状的面积。

use strict;
use warnings;
use feature qw/say/;
use Shape;

# pass color, length and width arguments to the constructor
my $red_shape = Shape->new({
                    color => 'red',
                    length=> 2,
                    width => 2,
                });
# calculate the area of $red_shape
my $area = $red_shape->{length} * $red_shape->{width};

# insert the area attribute and value into $red_shape
$red_shape->{area} = $area;

say $red_shape->{area};

现在$red_shape具有属性:颜色、长度、宽度和面积。

方法

方法只是属于类的一个Perl子程序。Shape类已经有一个方法,即构造函数new。让我们向Shape.pm添加一个新的方法来计算和返回面积。

package Shape;

sub new {
    my ($class, $args) = @_;
    my $self = {
        color  => $args->{color} || 'black',
        length => $args->{length} || 1,
        width  => $args->{width} || 1,
    };
    return bless $self, $class;
}

sub get_area {
    my $self = shift;
    my $area = $self->{length} * $self->{width};
    return $area;
}

1;

我们添加了get_area方法(在命名方法时使用动词-名词风格是很好的实践)。当一个对象方法被调用时,默认数组@_的第一个元素将包含包名和对象的引用。在get_area方法中,我们将这个参数存储在$self中。然后,我们将长度和宽度属性解引用以计算并返回$area。我们可以更新Draw.pl以使用新的面积方法。

use strict;
use warnings;
use feature qw/say/;
use Shape;

# pass color, length and width arguments to the constructor
my $red_shape = Shape->new({
                color => 'red',
                length=> 2,
                width => 2,
            });
# call the area method and print the value
say $red_shape->get_area;

访问器方法

访问器方法是子程序,用于访问对象属性。与直接解引用对象哈希引用中的属性相比,这可以使代码更易于阅读和维护(尤其是如果您遵循动词-名词风格的方法命名)。我们可以通过更新Shape.pm来为其颜色属性添加get和set方法。

package Shape;

sub new {
    my ($class, $args) = @_;
    my $self = {
        color  => $args->{color} || 'black',
        length => $args->{length} || 1,
        width  => $args->{width} || 1,
    };
    return bless $self, $class;
}

sub get_area {
    my $self = shift;
    return $self->{length} * $self->{width};
}

sub get_color {
    my $self = shift;
    return $self->{color};
}

sub set_color {
    my ($self, $color) = @_;
    $self->{color} = $color;
}

1;

让我们更新Draw.pl以使用新方法。

use strict;
use warnings;
use feature qw/say/;
use Shape;

# pass color argument to the constructor
my $shape = Shape->new({
                color => 'red',
            });

# print the shape color using get_color method
say $shape->get_color;

# set the shape color to blue
$shape->set_color('blue');

# print the shape color using get_color method
say $shape->get_color;

使用示例方法get_color和set_color,应该很明显如何编写额外的get和set方法来处理长度和宽度属性。

构造方法

构造方法是一个内部子程序,用于在构造时设置对象属性的值。Perl程序员使用的约定是使用名称前缀下划线来表示内部方法。让我们向Shape.pm添加一个构造方法,使用Time::Piece核心模块设置创建日期和时间。

package Shape;
use Time::Piece;

sub new {
    my ($class, $args) = @_;
    my $self = {
        color    => $args->{color} || 'black',
        length   => $args->{length} || 1,
        width    => $args->{width} || 1,
    };
    my $object = bless $self, $class;
    $object->_set_datetime;
    return $object;
}

sub get_area {
    my $self = shift;
    return $self->{length} * $self->{width};
}

sub get_color {
    my $self = shift;
    return $self->{color};
}

sub set_color {
    my ($self, $color) = @_;
    $self->{color} = $color;
}

sub _set_datetime {
    my $self = shift;
    my $t = localtime;
    $self->{datetime} = $t->datetime;
}

sub get_datetime {
    my $self = shift;
    return $self->{datetime};
}

1;

_set_datetime构造方法在new方法中被调用。还添加了一个相应的访问器来获取日期和时间。我们可以通过更新Draw.pl来测试它。

use strict;
use warnings;
use feature qw/say/;
use Shape;

my $shape = Shape->new;

# get datetime
say $shape->get_datetime;

析构方法

析构方法在Perl中当对象的引用全部超出作用域时会自动调用 - 它永远不会直接调用。如果类创建需要清理的临时文件或线程,析构方法很有用。它们还可以用于事件日志记录。Perl提供了一个特殊的析构方法名称,'DESTROY',必须在声明析构方法时使用。

package Shape;
use Time::Piece;

sub new {
    my ($class, $args) = @_;
    my $self = {
        color    => $args->{color} || 'black',
        length   => $args->{length} || 1,
        width    => $args->{width} || 1,
    };
    my $object = bless $self, $class;
    $object->_set_datetime;
    return $object;
}

# The new destructor method, called automatically by Perl
sub DESTROY {
    my $self = shift;
    print "I have been garbage-collected!\n";
}

sub get_area {
    my $self = shift;
    return $self->{length} * $self->{width};
}

sub get_color {
    my $self = shift;
    return $self->{color};
}

sub set_color {
    my ($self, $color) = @_;
    $self->{color} = $color;
}

sub _set_datetime {
    my $self = shift;
    my $t = localtime;
    $self->{datetime} = $t->datetime;
}

sub get_datetime {
    my $self = shift;
    return $self->{datetime};
}

1;

继承

面向对象编程的一个关键概念是能够创建现有类的子类。这些子类将继承其父类的方法和属性,这允许程序员通过将可重用的通用代码与专业代码分离来保持DRY

假设我们想实现圆形形状。目前Shape.pm具有长度和宽度属性,这些属性适用于矩形形状,但不适用于圆形,因为圆形需要半径、直径和周长属性。我们可以从Shape.pm派生出一个新的Circle类,该类继承Shape的方法并实现新的特定于圆形的功能。

package Shape::Circle;
use parent Shape;

sub new {
    my ($class, $args) = @_;
    my $self = {
        color    => $args->{color} || 'black',
        diameter => $args->{diameter} || 1,
    };
    my $object = bless $self, $class;
    $object->_set_datetime;
    return $object;
}

sub get_radius {
    my $self = shift;
    return $self->{diameter} * 0.5;
}

sub get_circumference {
    my $self = shift;
    return $self->{diameter} * 3.14;
}

sub get_area {
    my $self = shift;
    my $area = $self->get_radius ** 2 * 3.14;
}
1;

Circle.pm文件应保存在Shape/Circle.pm目录下。'use parent Shape;'这行代码告诉Perl,Circle.pm继承自Shape类。Circle类现在继承了来自Shape的datetime和color方法,同时也提供了新的特定于圆形的方法。请注意,Circle.pm也继承了来自Shape.pm的'new'和'get_area'方法,但它通过在Circle.pm中重新定义它们来覆盖了这些方法。让我们使用Draw.pl测试Circle.pm。

use strict;
use warnings;
use feature qw/say/;
use Shape::Circle;

my $circle = Shape::Circle->new;

# get datetime - inherited method
say $circle->get_datetime;

# try new Circle methods
say $circle->get_radius;
say $circle->get_circumference;
say $circle->get_area;

总结

本文探讨了Perl的核心面向对象功能。有关更多信息,官方Perl文档有教程更详细的参考。关于传统面向对象Perl的权威文本是Damian Conway的面向对象Perl(Manning,1999)。


本文最初发布在PerlTricks.com

标签

David Farrell

David是一位专业程序员,他经常在Twitter博客上关于代码和编程艺术进行推文。

浏览他们的文章

反馈

这篇文章有什么问题吗?请在GitHub上打开一个问题或拉取请求来帮助我们。