zoukankan      html  css  js  c++  java
  • 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(),"!
    ";
    }
    
    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(),"
    ";
    

    上面通过对象去调用类方法,它首先搜索出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(),"
    ";
    

    该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(),"!
    ";
    }
    
    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(),"!
    ";
    }
    

    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(),"!
    ";  # 调用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(),"!
    ";
    }
    
    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},"
    ";
    print $bm_horse->{Color},"
    ";
    print $by_sheep->{Name},"
    ";
    print $by_sheep->{Color},"
    ";
    

    结果:

    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(),"
    ";
    print $by_sheep->get_name(),"
    ";
    

    结果如下:

    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(),"
    ";
    

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

    在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()。

  • 相关阅读:
    go ERROR invalid character '<' looking for beginning of value
    C#实现将网址生成二维码图片
    二、WPF入门教程——Bingding学习
    一、WPF入门教程——创建WPF项目
    C#实现DataTable行列转置
    VBS整蛊代码
    Task.WhenAll和Task.WhenAny
    Task.WaitAll和Task.WaitAny
    CancellationTokenSource
    组合ContinueWith
  • 原文地址:https://www.cnblogs.com/f-ck-need-u/p/9811119.html
Copyright © 2011-2022 走看看