zoukankan      html  css  js  c++  java
  • Perl面向对象(3):解构——对象销毁

    本系列:

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


    perl中使用引用计数的方式管理内存,对象也是引用,所以对象的销毁也是由引用计数的管理方式进行管理的。也就是说,当一个对象(也就是一个数据结构)引用数为0时,这个对象就会被Perl回收。

    对象回收的俗称是"对象销毁"(destroy),术语是解构(destruction),在Perl中回收对象是通过一个名为DESTROY的特殊方法进行回收的,和构造器创建对象相反,这个方法解除构造,所以称之为解构器(destructor)。

    关于DESTROY

    当Perl中对象的最后一个引用要消失时,Perl将自动调用DESTROY方法。Perl处理DESTROY的方式和普通方法一样:

    • 先从本类中搜索,搜索不到再搜索父类
    • 传递的第一个参数为类名或对象名

    和普通方法不同的是,DESTROY是在对象被销毁时自动调用的。

    需要搞清楚的是,DESTROY这个特殊方法是当对象的引用数将要为0之前调用的,该方法执行完成后,对象相关的数据结构才被完全释放,引用数才真正变成0。所以,在DESTROY方法中可以定义很多善后工作(比如清理临时数据)或用来调试,善后完成后才完全释放对象。

    DESTROY示例

    例如,在lib/Animal.pm中定义父类Animal:

    #!/usr/bin/env perl
    
    use strict;
    use warnings;
    
    package Animal;
    
    sub new {
        my $class = shift;
        my $name = shift;
        bless $name,$class;
    }
    
    sub DESTROY {      # 添加此方法
        my $class = shift;
        print "OBJECT-> ",$class->name()," <-died
    "
    }
    
    sub name {
        my $self = shift;
        ref $self ? $$self : "an unamed Class $self";
    }
    
    sub speak {
        my $class = shift;
        print $class->name()," goes ",$class->sound(),"!
    ";
    }
    
    sub sound { die 'You have to define sound() in a subclass'; }
    
    1;
    

    Animal的子类Horse,文件lib/Horse.pm中:

    #!/usr/bin/env perl
    
    use strict;
    use warnings;
    
    package Horse;
    use parent qw(Animal);
    
    sub sound { "neigh" }
    
    1;
    

    然后在speak.pl程序文件中创建对象:

    #!/usr/bin/env perl
    use strict;
    use warnings;
    
    use lib "lib";
    
    use Horse;
    my $bm_horse = Horse->new("baima");   # 创建引用
    $bm_horse->speak();
              # 此处程序结束,引用将全部消失,将自动调用DESTROY方法销毁对象
    

    输出结果:

    baima goes neigh!
    OBJECT-> baima <-died
    

    为了更进一步测试DESTROY,将上面的对象创建放进代码块中:

    use lib "lib";
    use Horse;
    
    {
        my $bm_horse = Horse->new("baima");  # 创建引用
        $bm_horse->speak();
    }   # 引用到此消失,自动调用DESTROY方法销毁对象
    print "program end
    ";
    

    所以输出结果为:

    baima goes neigh!
    OBJECT-> baima <-died
    program end
    

    程序结束时会自动销毁所有对象,这时DESTROY()是在END语句块之后才调用的。

    嵌套对象的销毁

    perl的对象就是一个数据结构,如果这个对象的数据结构是数组、hash,那么可以进行对象的嵌套。

    对象嵌套的场景很多,最简单的解释:创建了Animal类后,再创建一个农场类,农场类的数据结构使用数组、hash结构,这个农场类里会创建一个一个的Animal对象放进农村类的数据结构中。通过农场对象,可以获取这个对象中有哪些以及有多少Animal对象。

    在销毁嵌套对象的时候,先调用外层的DESTROY方法,然后在DESTROY结束的时候销毁外层对象,最后销毁内层对象。也就是说,先让Animal对象们无家可归。注意,销毁外层对象只是会减少一次内层对象的引用,如果一个对象同时添加到了两个或多个嵌套结构中,销毁一个嵌套结构,并不会销毁完全销毁这个对象。就像是一个文件两个硬链接,它们处于两个目录下,删除一个目录只是删除这个硬链接。

    当然,这都是通过代码进行控制的。下面将会演示这两种不同的嵌套对象销毁方式。

    先销毁外层,再销毁内层

    例如,在lib/Farm.pm文件中创建一个农场,使用数组结构作为对象结构,为了方便看结果,将Farm放进代码块:

    #!/usr/bin/env perl
    
    use strict;
    use warnings;
    
    {
        package Farm;
        sub new { bless [],shift }
        sub add { push @{shift()},shift }  # 注意,解除引用时shift()必须不能省略括号,否则会产生歧义
        sub contents { @{shift()} }
        
        sub DESTROY {
            my $self = shift;
            print "$self is being destroyed...
    ";
            for($self->contents()){
                print " ",$_->name, " goes homeless
    ";
            }
            print "$self destroyed...
    ";
        }      # Farm的对象将在此被销毁
               # Farm中嵌套的所有对象将在此被一次性销毁(减少引用数)
    }
    1;
    

    上面的代码中,当准备要销毁Farm的对象时,将触发DESTROY方法,然后把农场对象的引用赋值给$self(因为调用DESTROY的那一刻还能获取到农场对象的引用,所以调用DESTROY的时候还没有销毁农场对象),然后for迭代所有的嵌套对象,直到DESTROY结束,Farm对象被真正销毁,Farm被销毁后,其内嵌套对象因为没有额外的引用数而随之被销毁。

    然后创建一个程序文件small_farm.pl,在其中创建Farm对象,并加入两个Horse对象:

    #!/usr/bin/env perl
    
    use strict;
    use warnings;
    
    use lib "lib";
    use Horse;
    use Farm;
    
    my $farm1 = Farm->new();
    $farm1->add(Horse->new("baima"));
    $farm1->add(Horse->new("heima"));
    
    print "burning the farm1...
    ";
    $farm1 = undef;       # 销毁$farm1对象
    print "End of program
    ";
    

    输出结果:

    burning the farm1...
    Farm=ARRAY(0x14dcf30) is being destroyed...
     baima goes homeless
     heima goes homeless    # DESTROY方法的代码块到此结束,下面将销毁Farm和嵌套的对象
    Farm=ARRAY(0x14dcf30) destroyed...
    OBJECT-> heima <-died
    OBJECT-> baima <-died
    End of program
    

    当销毁farm1时,嵌套在其内部的horse也将被销毁。

    如果,将$farm1拷贝一份:

    my $farm2 = $farm1;
    print "burning the farm1...
    ";
    $farm1 = undef;       # 销毁$farm1对象
    print "End of program
    ";
    

    再执行:

    burning the farm1...
    End of program
    Farm=ARRAY(0x1357f30) is being destroyed...
     baima goes homeless
     heima goes homeless
    Farm=ARRAY(0x1357f30) destroyed...
    OBJECT-> heima <-died
    OBJECT-> baima <-died
    

    可见,销毁farm1时并没有销毁整个对象,直到程序结束时才进行销毁。

    再者,将创建Horse对象的行为放在farm对象的外部:

    my @horses = (Horse->new("baima"),Horse->new("heima"));
    my $farm1 = Farm->new();
    $farm1->add($horses[0]);
    $farm1->add($horses[1]);
    
    print "burning the farm1...
    ";
    $farm1 = undef;       # 销毁$farm1对象,但保留@horses
    print "farm1 gone...
    ";
    @horses = ();         # 清空最后的引用@horses
    print "End of program
    ";
    

    上面每个horse对象都有两个引用,一个在农场farm1中,一个在数组@horses中。

    输出结果:

    burning the farm1...
    Farm=ARRAY(0x1835128) is being destroyed...
     baima goes homeless
     heima goes homeless
    Farm=ARRAY(0x1835128) destroyed...
    farm1 gone...
    OBJECT-> heima <-died
    OBJECT-> baima <-died
    End of program
    

    显然,烧掉了farm1之后,减少了一次引用,直到@horses也被清空后才调用Animal中的DESTROY方法。

    先销毁内层,再销毁外层

    在前面的几次实验中,农场中嵌套的所有对象总时会随着Farm销毁而同时一次性被销毁,但是有时候我们可能会希望一个一个地销毁。换句话说,我们想要先销毁嵌套在Farm中的对象,最后再销毁Farm自身。也就是这两种循环的不同方式:

    sub DESTROY {
        for($self->contents()){
            print " ",$_->name, " goes homeless
    ";
        }
    }  # 从此处开始,Farm和嵌套对象被一次性销毁
    
    sub DESTROY {
        while(@$self) {
            my $who_homeless = shift @$self;
            print " ",$who_homeless->name," goes homeless
    ";
        }
    }
    

    上面的第二种方式之所以能够在DESTROY内部就销毁嵌套对象,是因为shift @$self的时候将嵌套的对象引用计数减少一,但却同时新建了一个$who_homeless词法变量引用这个对象,所以引用数仍然为1,但这个词法变量在一次循环之后就会被覆盖掉(最后一轮循环则是出了循环作用域被销毁),从而使得嵌套的对象在每次进入下一轮循环的时候被销毁。

    修改lib/Farm.pm:

    #!/usr/bin/env perl
    
    use strict;
    use warnings;
    
    {
        package Farm;
        sub new { bless [],shift }
        sub add { push @{shift()},shift }
        sub contents { @{shift()} }
        
        sub DESTROY {
            my $self = shift;
            print "$self is being destroyed...
    ";
    
            while(@$self) {
                my $who_homeless = shift @$self;
                print " ",$who_homeless->name," goes homeless
    ";
            }
        }
    }
    1;
    

    修改small_farm.pl程序文件:

    #!/usr/bin/env perl
    use strict;
    use warnings;
    
    use lib "lib";
    use Horse;
    use Farm;
    
    my $farm1 = Farm->new();
    $farm1->add(Horse->new("baima"));
    $farm1->add(Horse->new("heima"));
    
    print "burning the farm1...
    ";
    $farm1 = undef;       # 销毁$farm1对象,但保留@horses
    print "End of program
    ";
    

    执行结果:

    burning the farm1...
    Farm=ARRAY(0x1a72f30) is being destroyed...
     baima goes homeless
    OBJECT-> baima <-died
     heima goes homeless
    OBJECT-> heima <-died
    Farm=ARRAY(0x1a72f30) destroyed...
    End of program
    

    销毁对象善后示例

    如果Farm、Animal创建对象时会打开一些文件句柄、生成一些临时文件,那么对象销毁可能需要手动去关闭文件句柄(不过perl一般会自动关闭)、清理对象的临时文件。

    以模块File::Temp的tempfile()函数生成临时文件为例,它会返回一个文件句柄和一个临时文件的名称。现在修改Animal类,使其构造对象时打开文件句柄并生成临时文件。

    lib/Animal.pm文件中:

    #!/usr/bin/env perl
    
    use strict;
    use warnings;
    use File::Temp qw(tempfile);
    package Animal;
    
    sub new {
        my $class = shift;
        my $name = shift;
            my $self = { Name => $name, Color => $class->default_color() };
            my ($fh,$filename) = File::Temp::tempfile();
            $self->{temp_fh} = $fh;
            $self->{temp_filename} = $filename;
        bless $self,$class;
    }
    
    sub DESTROY {    # 善后
            my $self = shift;
            my $fh = $self->{temp_fh};
            close $fh;
            unlink $self->{temp_filename};
            print "OBJECT-> ",$self->name()," <-died
    "
    }
    
    sub name {
        my $self = shift;
        ref $self ? $self->{Name} : "an unamed Class $self";
    }
    
    1;
    

    扩展继承的DESTROY

    DESTROY和普通方法并没有什么区别,它可以被继承,也可以被重写。继承而来的DESTROY自然是共性的,如果子类需要额外的善后工作,就需要对父类的DESTROY进行扩展。

    但重写DESTROY方法时,必须注意是扩展父类方法,而不是否定父类DESTROY的行为而完全重造一个新的DESTROY,因为子类并不知道父类的DESTROY有哪些善后操作。换句话说,重写DESTROY时,必须要调用父类的DESTROY,然后进行额外的扩展,否则本该父类善后的操作会被遗漏。

    例如,为子类Horse添加一个DESTROY方法:

    sub DESTROY {
        my $self = shift;
        $self->SUPER::DESTROY if $self->can( "SUPER::DESTROY" );
        print $self->name()," from subclass Horse gone
    ";
    }
    

    在上面的代码中,还对SUPER::DESTROY进行了检测,因为子类不知道父类是否定义了DESTROY方法,但如果父类定义了,就应该去调用它。

    再次声明,在子类重写DESTROY的时候,为了善后一切正常,必须在子类重写的DESTROY代码中包含$self->SUPER::DESTROY

    子类中额外的实例变量

    要在子类中维护额外的实例变量,只需重写父类的构造方法即可。

    例如Horse类下的RaceHorse子类,为其添加关于赛马战绩相关的4种额外实例数据:win、places、shows、losses。

    package RaceHorse;
    use parent qw(Horse);
    
    sub new {
        my $self = shift->SUPER::new(@_);
        $self->{$_} for qw(wins places shows losses);
        $self;
    }
    

    关于重写父类构造方法,在前一篇文章中已经解释过。

    只是这里需要注意的是,通过$self->{$_}的方式添加属性,其实已经"opened the box",破坏了面向对象的封装原则。但对于父类来说,如果能确保父类永远不会访问或涉及到这4种属性,那么是无关紧要的,这种情况对于Java来说,RaceHorse是父类Horse的友好成员,或者称之为"友好类"(friend class)。如果父类中的属性可能会命名为这4种之一,那么名称冲突,这是不应该出现的,甚至父类的返回类型修改后不是hash而是数组,那就更严重了。

    为了解耦这种依赖性问题,在创建子类的时候应当使用组合的方式而不是继承的方式。在此示例中,在创建RaceHorse类的时候,需要将Horse对象作为RaceHorse的一个实例数据,然后将剩余的数据放进独立的实例数据中,这样RaceHorse也将获得Horse对象的所有数据,还添加了属于自己的新数据,但因为不是继承关系,所以RaceHorse得把Horse类中的所有方法都重新写一遍,这可以通过"委托"的方式实现。虽然Perl支持委托,但委托的实现方式一般速度比较慢,也比较笨重。

    不过对于本文来说,无所谓了,让它们以"友好类"的方式存在即可。

    添加几个访问这些属性的方法:

    sub won { shift->{wins}++; }
    sub placed { shift->{places}++; }
    sub showed { shift->{shows}++; }
    sub lost { shift->{losses}++; }
    sub standings {
        my $self = shift;
        join ', ', map "$self->{$_} $_", qw(wins places shows losses);
    }
    

    每调用一次won()表示赢一次,standings()表示输出战绩。

    类变量(管理注册信息)

    可以使用类变量跟踪所有已创建的对象。比如使用一个hash结构的变量,将各个对象的引用保存到hash的值。那么什么作为hash的key?可以将对象的hash结构字符串化后(stringfy)的字符串作为key。

    hash结构字符串化是什么意思?看下面:

    my %myhash = (
        name => "longshuai",
        age  => 23,
    );
    print %myhash,"
    ";
    

    print输出的"namelongshuaiage23"就是hash结构字符串化的结果。字符串化的结果是将所有key和value都连在一起形成一个字符串。注意,hash结构的字符串化不能插入到双引号中,所以print "%myhash"是不会字符串化的,而是直接输出%myhash

    所以,如果一个hash变量%HASH1,其中一个value为%myhash结构,那么这个%HASH1的结构大致如下:

    %HASH1 {
        ...
        namelongshuaiage23 => { name => "longshuai",age  => 23 },
        ...
    }
    

    所以,将对象的hash数据结构作为value,对象字符串化的字符串作为key,可以保证所有的对象都是唯一的,除非创建的对象是完全一致的。这个key其实没有用处,只是用来充当占位符,使得对象的数据结构能嵌套保存到hash结构中。当然,采取什么作为key并没有要求,只要能保证对象的唯一性就可以。

    现在可以扩展一下Animal的构造方法:

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

    此处以一个词法的hash变量记录注册对象创建信息,每调用new创建一次对象,就将对象引用(hash结构)保存到hash结构%REGISTER中,由于最后一句是赋值语句,所以返回值也是$self,也就是说返回的是这个新创建的对象。

    创建的对象注册到%REGISTRY中后,还需要方法去取得这个对象,例如:

    sub registered {
        return map { "a ".ref($_)." named ".$_->name } values %REGISTRY;
    }
    

    虽然类变量跟踪了已经创建的变量,但正因为%REGISTRY中多了一份对象的引用,使得对象的销毁时间点将出乎预料。例如,下面的代码:

    {
        my $horse1 = Horse->new("baima");
        $horse1->speak();
    }
    

    正常情况下,horse1对象将从代码块结束的那个位置开始销毁,但此时Animal的类变量中还记录了该对象的引用,引用数没有减为0,所以$horse1不会被销毁。

    如果想要避免这种情况,可以创建一个不会被跟踪的对象,然后通过它的DESTROY方法去delete保存在Animal类变量%REGISTRY中的元素。显然,这是很不合理行为。另一种方式是使用弱引用,见下文。

    弱引用

    弱引用(weaken reference)是从perl v5.8版本之后引入的功能,它位于Scalar::Util模块中。一个引用转换为弱引用后,它不会被引用计数,当普通的引用计数减为0后,该数据结构将被销毁,然后这个弱引用将被设置为undef。

    下面一个示例即可解释清楚。修改下Animal的构造方法new():

    use Scalar::Util qw(weaken);
    sub new {
        ref(my $class = shift) and croak 'class only';
        my $name = shift;
        my $self = { Name => $name, Color => $class->default_color };
        bless $self, $class;
        $REGISTRY{$self} = $self;
        weaken($REGISTRY{$self});
        $self;
    }
    

    上面$REGISTRY{$self} = $self;会增加一次引用计数,但随后的weaken($REGISTRY{$self});会将此引用转换为弱引用,使得hash的key部分不再强引用这个对象,所以会减少一次引用计数,使得最终new()退出时将只剩下一次引用计数。

    弱引用还能解决内存泄漏问题,这是采用引用计数管理内存的通病,因为它们无法解决引用环路。例如$a引用$b$b又引用$a,a想要释放就得释放b,b想要释放就得释放a,导致它两的引用计数始终无法减为0,占用的内存永远不会释放。通过弱引用的方式,随便将a或是b转换为弱引用都能解决引用环路问题,问题是转换a好还是转换b好呢?

    对于对象之间的引用环路来说,转换父类比转换子类好,因为父类只要不需要了就可以直接销毁,此时子类也会随之销毁。而转换子类时,子类在不需要的时候被销毁,但父类可能还在引用别的,也就是说父类不一定会被销毁。

    另外,在使用弱引用的时候要非常小心,能不用的时候尽量别用,否则一出问题,非常难调试排查。

  • 相关阅读:
    如何在CentOS 7中安装最新Git(源码安装)
    centos7安装Lua
    syslog-ng 学习
    syslog-ng内容讲解
    java框架篇---spring IOC 实现原理
    java 过滤器filter使用案例
    jsp-TagLib标签库
    阿里服务器+Centos7.4+Tomcat+JDK部署
    IntelliJ IDEA上创建maven Spring MVC项目
    ServiceStack.Redis之IRedisClient
  • 原文地址:https://www.cnblogs.com/f-ck-need-u/p/9818016.html
Copyright © 2011-2022 走看看