骏马金龙 (新博客:www.junmajinlong.com)

网名骏马金龙,钟情于IT世界里的各种原理和实现机制,强迫症重症患者。爱研究、爱翻译、爱分享。特借此一亩三分田记录自己成长点滴!!!

Perl面向对象(2):对象

本系列:

第3篇依赖于第2篇,第2篇依赖于1篇。


已有的代码结构

现在有父类Animal,子类Horse,它们的代码分别如下:

lib/Animal.pm中:

#!/usr/bin/env perl

use strict;
use warnings;

package Animal;

sub speak {
    my $class = shift;
    print "a $class goes ",$class->sound(),"!\n";
}

sub sound { die 'You have to define sound() in a subclass'; }

1;

lib/Horse.pm中:

#!/usr/bin/env perl

use strict;
use warnings;

package Horse;
use parent qw(Animal);

sub sound { "neigh" }

1;

一个perl程序speak.pl文件:

#!/usr/bin/env perl

use strict;
use warnings;
use lib "lib";
use Horse;

Horse->speak();

执行上面的speak.pl,将输出:

a Horse goes neigh

上面使用Horse->speak()的方式调用speak()方法,它首先调用到父类Animal中的speak(因为Horse类中没有重写该方法),然后Animal中的speak又重新回调Horse类中的sound()。

这个speak是所有Horse都共享的,如果想要定义每个Horse对象都私有的数据呢?比如为每个Horse对象命名。这里Horse的名字就是Horse类的实例数据(在其它编程语言中常称之为成员变量),是每个对象独有的。

bless创建实例数据:对象

在Perl的面向对象编程中,一个对象表示的就是一个对内置类型的引用,比如标量引用、数组引用、hash引用。也就是说,所谓对象就是一个指向内置数据结构的引用,这个数据结构可以认为是每个对象私有的成员变量。

强烈建议使用hash引用的方式,不过此处先以标量引用的方式开始本文的介绍。

修改speak.pl文件:

#!/usr/bin/env perl

use strict;
use warnings;
use lib "lib";
use Horse;

my $name = "baima";
my $bm_horse = \$name;
bless $bm_horse,'Horse';

bless的语法为:

bless REFERENCE,CLASS;

它表示为CLASS类设置一个唯一标识符,并返回这个唯一标识符,这个唯一标识符是一个数据结构的引用,这个唯一标识符也被称为对象。也就是说,对象就是一个引用,所以我们常常会使用my $obj = bless REF,CLASS;来返回一个对象。另一方面,bless表示将一个数据结构和类进行关联,表示这个数据结构(也许是空的,也许是经过一定初始化的)已经附加在类上,当创建这个类的实例(对象)时,它将返回一个引用,对这个数据结构的引用,换句话说,这个对象就已经拥有了这个数据结构。

如下图所示:

上面的Class是类,引用变量$obj_ref是这个类的唯一标识符,它指向一个数据结构,这个数据结构是这个类的属性。使用Horse进行具体化,Horse是类,$bm_horse是这个类的唯一标识符,这个引用指向值为"baima"的标量数据结构,所以$bm_horse是一个对象,"baima"就是它独有的属性。

也就是说,bless $bm_horse,'Horse';已经创建了一个名为$bm_horse的对象,而"baimai"这个属性是只属于这一个对象的数据。

调用实例方法

bless生成一个引用后,这个引用是类的实例,可以通过这个引用变量去调用类的方法。

#!/usr/bin/env perl

use strict;
use warnings;
use lib "lib";
use Horse;

my $name = "baima";
my $bm_horse = \$name;
bless $bm_horse,'Horse';

print $bm_horse->sound(),"\n";

上面通过对象去调用类方法,它首先搜索出sound()在何处(即类中还是父类中),然后将参数传递给sound()。传递的参数列表中,第一个参数是实例的名称,也就是$bm_horse,就像通过类名去调用类方法时,传递的第一个参数是类名一样。所以,下面两个是等价的:

$bm_horse->sound();
Horse::sound($bm_horse);

实际上,bless最初的目的就是通过一个引用来关联正确的类,以便perl能正确地找到所调用的方法,免去通过硬编码类名的麻烦。

再调用speak()试试:

$bm_horse->speak();

它将输出:

a Horse=SCALAR(0xc78610) goes neigh!

这是因为$bm_horse是一个指向标量数据结构的引用,speak()方法中将其赋值给$class$class也仍然是引用,而且speak()中并没有去解除这个引用,所以如此输出。至于解决方法,留待后文。

访问实例数据

因为实例的名称是每个对象的唯一标识符,而现在可以通过传递给方法的第一个参数获取实例的名称,借此名称,可以进一步地获取到该实例的其它数据。

现在,在lib/Horse.pm文件中添加一个name方法:

sub name {
    my $self = shift;
    $$self;
}

注意上面$$self,因为$self是对象名,而对象名总是一个引用变量,因此将其解除引用。

然后在speak.pl中调用这个方法:

#!/usr/bin/env perl

use strict;
use warnings;
use lib "lib";
use Horse;

my $name = "baima";
my $bm_horse = \$name;
bless $bm_horse,'Horse';

print $bm_horse->name()," says ",$bm_horse->sound(),"\n";

该print将输出:

baima says neigh

perl中几乎都使用$self作为类名或对象名的代名词,就像java中的"this"一样。实际上,你可以使用任何变量名称,但约定俗成地,大家都喜欢用self。

通过构造器构造对象

前面构造Horse对象是在独立的speak.pl文件中实现的,这样生成Horse对象的方式是手动的,是完全私有的,构造对象时的实例数据(即name属性)也是完全暴露的。当在多个文件中都这样构造Horse对象,迟早会出错。

于是,在类文件lib/Horse.pm中定义一个构造方法new(),每次要构造Horse对象的时候只需调用这个方法即可。

#!/usr/bin/env perl

use strict;
use warnings;

package Horse;
use parent qw(Animal);

sub new {
    my $class = shift;
    my $name = shift;
    belss \$name,$class;
}

sub name {
    my $self = shift;
    $$self;
}

sub sound { "neigh" }

1;

上面在Horse.pm中定义了一个new()方法,该方法里面包含了bless语句,且作为new()方法的最后一个语句,表示构造一个对象并返回这个对象的唯一标识符:引用变量表示的对象名。因此,这个new()方法被称之为构造方法:用于构造该类的实例。

方法名new()可以随意,例如hire(),named()等都可以,但面向对象编程语言中,基本上都使用new这个词语来表示创建新对象,所以,也建议采用约定俗成的new(),如果使用其它方法名作为构造方法,请做好注释。

现在,只要调用Horse中的这个new()方法,就表示在当前包中构建一个Horse的实例(bless的返回值):

my $bm_horse = Horse->new("baima");

注意,bless返回的是对象引用,所以赋值给变量$bm_horse,这时$bm_horse将代表这个对象,是这个对象的唯一标识符。

上面调用new()的过程中,首先找到类方法new(),然后传递参数列表('Horse',"baima"),new()方法中,bless将baima这个数据结构附加到Horse类中,并返回指向该数据结构的引用。以后,通过$bm_horse就能找到这个数据结构,因为这个数据结构是对象$bm_horse的实例数据。

继承构造方法

在上面lib/Horse.pm中的构造方法new()中是否有Horse所特有的个性内容?完全没有。无论是Horse、Cow还是Sheep的构造方法都是通用的,所以将共性的代码抽取到父类Animal中。

lib/Animal.pm中:

#!/usr/bin/env perl

use strict;
use warnings;

package Animal;

sub new {
    my $class = shift;
    my $name = shift;
    bless \$name,$class;
}

sub name {
    my $self = shift;
    $$self;
}

sub speak {
    my $class = shift;
    print "a $class goes ",$class->sound(),"!\n";
}

sub sound { die 'You have to define sound() in a subclass'; }

1;

lib/Horse.pm中:

#!/usr/bin/env perl

use strict;
use warnings;

package Horse;
use parent qw(Animal);

sub sound { "neigh" }

1;

如此一来,无论是Horse、Cow还是Sheep都继承父类Animal中的构造方法new()以及name()。注意,上面name()方法也抽取到了Animal类中,因为它也是共性的,不过本小节暂时用不到该方法,下一小节会修改该方法。

现在,在speak.pl文件中构建一个Horse对象:

my $bm_horse = Horse->new("baima");

然后通过这个对象调用speak方法:

$bm_horse->speak();

它将输出:

a Horse=SCALAR(0xc78610) goes neigh!

这个实验前文已经验证过了。之所以会如此,是因为传递给speak()的第一个参数是对象的引用变量,而speak()中并没有去解除这个引用。再次看看speak()的代码:

sub speak {
    my $class = shift;
    print "a $class goes ",$class->sound(),"!\n";
}

speak()中的$class期待的其实是一个类名,而不是对象名,因为类名是具体的字符串,而非引用变量。例如,使用Horse->speak()就不会出现上面的问题。

如何解决父类中的这种问题,使其能同时处理类名和对象名?

让方法能同时处理类和对象

为了让父类中的方法能同时处理类名和对象名,可以加入一个额外的方法对类名和对象名进行判断。如何判断是类名还是对象名?只需使用ref即可,如果ref能返回一个值,表示这是一个引用,说明这是对象,ref返回false,则说明这不是引用,也就是类名。

之前因为name()方法因为共性的原因被抽取到Animal.pm后并没有使用过,这里派上用场了。

lib/Animal.pm中:

#!/usr/bin/env perl

use strict;
use warnings;

package Animal;

sub new {
    my $class = shift;
    my $name = shift;
    bless \$name,$class;
}

sub name {
    my $self = shift;
    ref $self ? $$self : "an unamed Class $self";  # 修改此行
}

sub speak {
    my $class = shift;
    print $class->name()," goes ",$class->sound(),"!\n";  # 调用name()方法
}

sub sound { die 'You have to define sound() in a subclass'; }

1;

这样speak()就变得共性化,既能处理类名,也能处理对象名。

my $bm_horse = Horse->new("baima");
$bm_horse->speak();    # 传递对象名
Horse->speak();        # 传递类名

将输出如下结果:

baima goes neigh!
an unamed Class Horse goes neigh!

之所以加入新的方法,是因为在speak()中类名和对象名是相互独立的,也就是无法共性的,要么是类名,要么是对象名。为了让一段代码共性化,解决方法就是添加额外的代码将非共性内容化解掉,这些额外的代码可以直接加在speak()内部,也可以放进一个新定义的方法中,然后在speak()中调用这个方法。这是一种编程思想。

使用hash数据结构:添加额外的成员变量

经常地,perl使用hash作为对象的数据结果,这个数据结构中可以存储不同的数据、引用,甚至是对象,其中hash的key常作为实例数据(成员变量)。

再次说明,perl面向对象时最常用的对象数据结构是hash,但标量、数组也一样可以,至少很少用。

想要使用hash数据结构,只需将一个hash结构bless到类上即可。它表示这个hash数据结构附加在类上,bless返回一个引用,这个引用就是对象,所以这个对象指向这个数据结构,从而对象拥有这个数据结构。

例如,绑定一个空的hash结构:

bless {},$class;

上面的bless将一个匿名hash附加到类中。

对于父类Animal来说,由于已经有了name的属性,现在如果想要加上一个color属性,就可以将这两个成员属性放进一个hash结构中:

lib/Animal.pm中:

#!/usr/bin/env perl

use strict;
use warnings;

package Animal;

sub new {
    my $class = shift;
    my $name = shift;
    my $self = { 
        Name  => $name,
        Color => $class->default_color(),
    };
    bless $self,$class;
}

sub name {
    my $self = shift;
    ref $self 
        ? $self->{Name}      # 此处需要修改,因为$self不再是标量引用变量,而是hash引用变量
        : "an unamed Class $self";
}

sub default_color {
    die "You have to override default_color method in subclasses";
}

sub speak {
    my $class = shift;
    print $class->name()," goes ",$class->sound(),"!\n";
}

sub sound { die 'You have to define sound() in a subclass'; }

1;

上面将一个包含key:Name和Color的hash数据结构bless到类上,其中Name成员变量通过构造对象时传递参数赋值,Color则调用各类自己的默认颜色方法default_color(),各个子类必须重写该方法。这是显然的,我们可以为某一子类动物设置默认毛色,但不能为所有动物设置同一种默认毛色。

然后修改lib/Horse.pm和lib/Sheep.pm,重写default_color():

lib/Horse.pm中:

#!/usr/bin/env perl

use strict;
use warnings;

package Horse;
use parent qw(Animal);

sub sound { 'neigh' }

sub default_color {
    'black'
}

1;

lib/Sheep.pm中:

#!/usr/bin/env perl

use strict;
use warnings;
package Sheep;
use parent qw(Animal);

sub sound { 'baaaah' }

sub default_color {
    'white'
}

1;

然后,speak.pl中构造Horse对象和Sheep对象,并访问自己的成员属性:

my $bm_horse = Horse->new("baima");
my $by_sheep = Sheep->new("xiaoyang");

print $bm_horse->{Name},"\n";
print $bm_horse->{Color},"\n";
print $by_sheep->{Name},"\n";
print $by_sheep->{Color},"\n";

结果:

baima
black
xiaoyang
white

子类重写构造方法

从父类中继承构造方法时,创建的对象的数据结构是完全一致的。如果某个子类想要多添加一些固定的数据元素,可以让子类重写父类的构造方法。

但需要注意的是,重写方法时,一般都强烈建议只对父类方法进行扩展,而不应该否定父类方法,完全修改父类方法(抽象方法除外)。

例如,现在父类Animal中的构造方法如下:

sub new {
    my $class = shift;
    my $name = shift;
    my $self = { 
        Name  => $name,
        Color => $class->default_color(),
    };
    bless $self,$class;
}

想要为子类Horse添加一种固定的属性,马的类型是战马、比赛用的马还是普通的马。于是,在Horse类中:

package Horse;
use parent qw(Animal);

sub new {
    my $self = shift->SUPER::new(@_);
    $self->{Type} = "Racehorse";
    $self;
}

注意,上面Horse中的构造方法new()中并没有给bless语句。当调用Horse->new()构建对象的时候,首先调用父类的new(),父类的new会关联一个hash结构并返回这个hash结构,这个hash结构又赋值给$self,为此hash结构添加一种元素后,子类的new()返回$self,使得这个hash结构成为子类对象的数据结构。

为了后面的实验,本节所修改的Horse内容请删除。

设置和获取实例数据:setter & getter

上面设置Color的时候只能通过方法default_color()设置默认的毛色,但马有黑马、棕色马、条纹马等等,所以需要能手动设置各种颜色。此外,还要更及时获取到当前最新的成员变量值,比如获取某Horse对象的名称和颜色。这就是俗称的setter和getter方法的作用。

在此示例中,Name属性是直接通过构造方法传值设置的,在逻辑上它唯一标识这个对象(对我们而言,对perl而言是通过对象引用来唯一识别的),所以Name属性不应该允许重新设置。再者,因为设置和获取各对象的属性的代码是共性的,所以直接将这两类方法写到父类Animal中。

lib/Animal.pm中新加的代码片段:

sub set_color {
    my $self = shift;
    $self->{Color} = shift;
}

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

sub get_name {
    shift->{Name};
}

现在可以为每个Horse或Sheep对象都设置对象自己的颜色,并且能获取颜色和名称:

my $bm_horse = Horse->new("baima");
my $by_sheep = Sheep->new("xiaoyang");

$bm_horse->set_color("white-and-black");
print $bm_horse->get_color(),"\n";
print $by_sheep->get_name(),"\n";

结果如下:

white-and-black
xiaoyang

注意上面get_name()中的一种简写方式:shift->{NAME},shift没有给参数,所以它的操作对象是@_,它等价于(shift @_)->{Name},也等价于:

my $self = shift;
$self->{Name};

关于setter返回值的问题

在为setter方法进行编码的时候,需要考虑它的返回值,一般来说有以下4种返回值类型:

  • (1).set成功后的值
  • (2).set之前的值
  • (3).返回对象自身
  • (4).返回成功/失败布尔值

这4种返回值各有优缺点,但无论如何都请注释好返回值的类型,并且设计好之后就别再修改

第(1)种是最通用、最常见也最简单的行为,传递什么参数给setter,就返回什么参数值,正如set_color()一样:

sub set_color {
    my $self = shift;
    $self->{Color} = shift;
}

一般来说,这种setter方法是放在空上下文(void context)中执行的,但在perl中也可以直接输出它:print set_color("COLOR")

第(2)种要返回设置之前的值,也很简单,只需使用一个临时变量存储一下原始值并返回该变量即可:

sub set_color {
    my $self = shift;
    my $temp = $self->{Color};
    $self->{Color} = shift;
    $temp;
}

这里有一点点小优化。因为是set,所以它可能是在空上下文中执行的,也就是说这时返回之前的值是多余的。可以通过wantarray来判断一下,wantarray函数用于检查执行上下文,如果在列表上下文中则返回true,标量上下文中则返回false,空上下文中则返回undef。

sub set_color {
    my $self = shift;
    if(defined wantarray){
        # 非空上下文,返回值有用
        my $temp = $self->{Color};
        $self->{Color} = shift;
        $temp;
    } else {
        # 空上下文,无需返回值
        $self->{Color} = shift;
    }
}

第(3)种返回对象自身:

sub set_color {
    my $self = shift;
    $self->{Color} = shift;
    $self;
}

一般来说不会用到这种情况。但有时候有奇效,例如可以形成对象链。例如,Person有4个成员变量:Name,Age,Height,Weight,它们的setter方法都返回对象自身,那么可以:

my $people = Person->set_name("abc")->set_age(23)->set_height(168)->set_weigth(60);

# 格式化一下:
my $people = 
    Person->set_name("abc")
          ->set_age(23)
          ->set_height(168)
          ->set_weigth(60);

第(4)种返回布尔值有时候非常有效,特别是对于经常更新出错的情况。如果是前3种返回值方式,会抛出异常,需要判断并使用die进行终止。

别暴露实例数据

在面向对象编程中,常使用一个术语don't look inside box来表示不要暴露对象的成员数据。

通过$obj_ref->{KEY}的方式可以在类的外部访问或设置的数据结构(成员变量),这是违反对象封装原则的,它将每个对象的内部属性都暴露出来了。对象就像是一个黑盒子,$obj_ref->{KEY}就像是将锁链撬开一样。

面向对象的目的之一是让Animal或Horse的维护者可以对它们的方法能独立地做出合理的修改,并且修改后那些已经导出的接口仍然能够正常工作。为什么直接访问hash结构违反了这个原则?当Animal的Color属性不再使用颜色的名称作为它的值时,而是使用RGB三原色的方式来存储颜色呢?

在此示例中,以一个虚构的模块Color::Conversions来修改颜色数据的格式,该模块有两个函数rgb_to_name()和name_to_rgb(),用于转换RGB和颜色的字符串名称,其中name_to_rgb()返回的是一个包含RGB三原色的数组引用。

可以修改set_color()和get_color()方法:

use Color::Conversions qw(rgb_to_name name_to_rgb);

sub set_color {
    my $self = shift;
    my $color_name = shift;
    $self->{Color} = name_to_rgb($color_name);
}

sub get_color {
    my $self = shift;
    rgb_to_name($self->{Color});
}

现在我们可以照旧使用setter和getter,但内部其实已经改变了,这些改变对使用者来说是透明的。此外,我们还可以添加额外的接口,使得我们可以直接设置RGB格式的颜色:

sub set_color_rgb {
    my $self = shift;
    $self->{Color} = [@_];
}

sub get_color_rgb {
    my $self = shift;
    @{ $self->{Color} };
}

如果我们在类的外面直接使用$bm_horse->{Color},将无法直接查看,因为它是一个RGB三原色元素列表的引用,而非直接显示出来的RGB元素值或颜色名称。

这正是面向对象编程所鼓励的行为,对于perl而言,只需将成员变量对应的值设置为一个引用即可。以关联hash数据结构的Animal类为例:

     Animal
       |--------------------------|
       |->  KEY1 => $ref_value1   |
       |->  KEY2 => $ref_value2   |
       |->  KEY2 => $ref_value2   |
       |--------------------------|

为了让数据通过引用的方式隐藏起来,且能通过getter方法查找出来,需要合理设计setter和getter方法。例如,让setter以普通的字符串为参数,但却将其存储到一个引用中,让getter以引用为参数,但却返回人眼可识别的内容。

简化setter和getter的书写

对于面向对象来说,这两个方法写的实在太频繁了。perl一切从简的原则,自然也要将其简化书写:

sub get_color { $_[0]->{Color} }
sub set_color { $_[0]->{Color} = $_[1] }

或者:

sub get_color { shift->{Color} }
sub set_color { pop->{Color} = pop }

合并getter和setter

如果不考虑默认传递的类名或对象名参数,getter方法通常是不含参数的,setter方法通常是包含参数的。通过这个特性,可以将getter和setter合并起来:

sub get_set_color {
    my $self = shift;
    if(@_) {
        # 有参数,说明是setter
        $self->{Color} = shift;
    } else {
        # 没有参数,说明是getter
        $self->{Color};
    }
}

或者简写的:

sub get_set_color { @_ > 1 ? pop->{Color} = pop : shift->{Color} }

使用时:

# 设置颜色
$bm_horse->get_set_color("blue");

# 获取颜色
print $bm_hore->get_set_color(),"\n";

限制方法类别:类方法和实例方法

在perl中所有的方法都是子程序,没有额外的功能来区分一个方法是类方法还是实例方法。对我们来说,如果传递的第一个参数为类名的是类方法,如果传递的第一个参数为对象引用名的方法是实例方法。

好在perl提供了ref函数,可以通过检查引用的方式来检查这是类方法还是实例方法。

use Carp qw(croak);

sub instance_method {
    ref(my $self = shift) or croak "this is a instance method";
    ...CODE...
}

sub class_method {
    ref(my $class = shift) and croak "this is a Class method";
    ...CODE...
}

这里使用croak替换了die,这样报错的时候可以直接告诉错误所在的行数。

私有方法(private method)

私有方法是指类中不应该被外界访问的方法,它可以在类自身其它地方调用,但不应该被对象或其它外界访问以免破坏数据。

例如,类Class1中的方法get_total()内部调用一个私有方法_get_nums(),通过该私有方法返回的数组来获取一个包含数值的数组。如果有一个子类Class2继承了Class1,且重写了_get_nums()使其返回一个数组引用而非数组,这时对象要调用的get_total()整段代码就废了。

package Class1;

sub new {
    my ($class,$args) = @_;
    return bless $args,$class;
}

sub get_total {
    my $self =  shift;
    my @nums = $self->_pri_sub;   # 期待该私有方法返回一个列表
    my $total = 0;
    foreach (@nums) {
        $total += $_;
    }
    return $total;
}

sub _pri_sub {
    my $self = shift;
    ...some codes...
    return @nums;      # 返回一个数组
}

1;

一般来说,这样的问题并不常发生,因为程序毕竟是程序员写的,遵循规范的情况下,大家都知道这是什么意思。但如果程序比较庞大,也许无意中就重写了一个私有方法。面向对象,一个最基本的规则就是保护数据不被泄漏、不被破坏。

但perl中所有的方法都是public(公共的),谁都能访问,并没有提供让方法私有化的功能。只是以一种呼吁式的规范,让大家约定俗成地使用下划线"_"作为方法名的前缀来表示这是一个私有方法(例如sub _name {})。但这只是一种无声的声明"这是私有方法,外界请别访问",perl并不限制我们从外界去访问下划线开头的方法。

要实现方法的真正私有化,可以将匿名子程序赋值给一个变量来实现,或者通过闭包的方式实现

sub get_total {
    my $self =  shift;
    my @nums = $self->$_pri_sub(ARGS);
    my $total = 0;
    foreach (@nums) {
        $total += $_;
    }
    return $total;
}

my $_pri_sub = sub {
    my $self = shift;
    ...some codes...
    return @nums;
}

需要注意的是,上面$self->$_pri_sub()中箭头的右边是一个变量,在其它面向对象语言中是无法将变量作为方法名的,但perl支持。

因为$_pri_sub是词法变量,构造的对象无法取得这样的数据。但通过一些高级技术,对象还是能够取得这个方法,要想完全私有化,通过闭包实现:

sub get_total {
    my $self =  shift;
    my $_pri_sub = sub {
        my $self = shift;
        ...some codes...
        return @nums;
    }
    my @nums = $self->$_pri_sub(ARGS);
    my $total = 0;
    foreach (@nums) {
        $total += $_;
    }
    return $total;
}

祖先类:UNIVERSAL

UNIVERSAL是一切类的祖先,所有的类都继承于它。它提供了3个方法:isa()、cat()和VERSION(),在v5.10.1和之后,还提供了另一个方法DOES()。

1.isa()用于判断某个给定的对象(或类)与某个类是否满足is a的关系,也就是"对象是否是某个类的实例,类1是否是类2的子类"

$object_or_class->isa(CLASS);

2.can()用于判断某个给定的对象(或类)是否能够使用某方法

$object_or_class->can($method_name);

需注意,can()方法检测结果为真的时候,其返回值是该方法的引用。也就是说,可以直接将can()赋值给一个方法的引用变量,避免多次书写。以下是等价的写法:

if(my $method = $obj->can($method_name)){
    $obj->$method;
}

if($obj->can($method_name)){
    $obj->$methdo_name;
}

3.VERSION()用于返回对象(或类)的版本号

设置了our $VERSION=...;之后,既可以通过继承自UNIVERSAL的VERSION()方法获取版本号,也可以通过对象获取VERSION变量值:

$obj->VERSION();
$obj->VERSION;

多重继承

Perl支持多重继承。假设Class3多重继承Class1和Class2:

package Class3;

# 1.
use base qw(Class1 Class2);
# 2.
use parent qw(Class1 Class2);
# 3.
use Class1;
use Class2;
our @ISA = qw(Class1 Class2);

但无论是哪种语言,都强烈建议不要使用多重继承。

假设Class1和Class2都直接继承自UNIVERSAL类,现在Class3多重继承Class1和Class2。

       UNIVERSAL
        |    |
    Class1  Class2
        |    |
        Class3

假设Class2有方法eat(),Class1没有,且Class3没有写eat(),那么Class3的实例调用eat()方法的时候,会调用到Class2的eat()吗?

默认情况下,Perl搜索方法的规则是从左搜索,深度优先。意味着use base qw(Class1 Class2)时,当Class3自身找不到eat()时,将先搜索左边的Class1,搜索完没发现eat(),将搜索Class1的父类UNIVERSAL。也就是说永远也不会去搜索Class2。

实际上,搜索父类时是搜索@ISA中的元素,所以是从左开始搜索。

但是可以使用CPAN上的C3或者mro模块,它们实现从左搜索,广度优先的搜索规则。也就是说,对于use base qw(Class1 Class2),当Class3自身找不到eat()时,将先找左边的Class1的eat(),找不到再找右边的Class2的eat(),还找不到的话最后找父类的eat()。

posted @ 2018-10-18 16:04  骏马金龙  阅读(1643)  评论(0编辑  收藏  举报