Perl面向對象(3):解構——對象銷燬

本系列:html

第3篇依賴於第2篇,第2篇依賴於1篇。數組


perl中使用引用計數的方式管理內存,對象也是引用,因此對象的銷燬也是由引用計數的管理方式進行管理的。也就是說,當一個對象(也就是一個數據結構)引用數爲0時,這個對象就會被Perl回收。數據結構

對象回收的俗稱是"對象銷燬"(destroy),術語是解構(destruction),在Perl中回收對象是經過一個名爲DESTROY的特殊方法進行回收的,和構造器建立對象相反,這個方法解除構造,因此稱之爲解構器(destructor)。less

關於DESTROY

當Perl中對象的最後一個引用要消失時,Perl將自動調用DESTROY方法。Perl處理DESTROY的方式和普通方法同樣:函數

  • 先從本類中搜索,搜索不到再搜索父類
  • 傳遞的第一個參數爲類名或對象名

和普通方法不一樣的是,DESTROY是在對象被銷燬時自動調用的。測試

須要搞清楚的是,DESTROY這個特殊方法是當對象的引用數將要爲0以前調用的,該方法執行完成後,對象相關的數據結構才被徹底釋放,引用數才真正變成0。因此,在DESTROY方法中能夠定義不少善後工做(好比清理臨時數據)或用來調試,善後完成後才徹底釋放對象。調試

DESTROY示例

例如,在lib/Animal.pm中定義父類Animal:code

#!/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\n"
}

sub name {
    my $self = shift;
    ref $self ? $$self : "an unamed Class $self";
}

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

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

1;

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

#!/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\n";

因此輸出結果爲:

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...\n";
        for($self->contents()){
            print " ",$_->name, " goes homeless\n";
        }
        print "$self destroyed...\n";
    }      # 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...\n";
$farm1 = undef;       # 銷燬$farm1對象
print "End of program\n";

輸出結果:

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...\n";
$farm1 = undef;       # 銷燬$farm1對象
print "End of program\n";

再執行:

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...\n";
$farm1 = undef;       # 銷燬$farm1對象,但保留@horses
print "farm1 gone...\n";
@horses = ();         # 清空最後的引用@horses
print "End of program\n";

上面每一個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\n";
    }
}  # 今後處開始,Farm和嵌套對象被一次性銷燬

sub DESTROY {
    while(@$self) {
        my $who_homeless = shift @$self;
        print " ",$who_homeless->name," goes homeless\n";
    }
}

上面的第二種方式之因此可以在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...\n";

        while(@$self) {
            my $who_homeless = shift @$self;
            print " ",$who_homeless->name," goes homeless\n";
        }
    }
}
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...\n";
$farm1 = undef;       # 銷燬$farm1對象,但保留@horses
print "End of program\n";

執行結果:

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\n"
}

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\n";
}

在上面的代碼中,還對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,"\n";

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好呢?

對於對象之間的引用環路來講,轉換父類比轉換子類好,由於父類只要不須要了就能夠直接銷燬,此時子類也會隨之銷燬。而轉換子類時,子類在不須要的時候被銷燬,但父類可能還在引用別的,也就是說父類不必定會被銷燬。

另外,在使用弱引用的時候要很是當心,能不用的時候儘可能別用,不然一出問題,很是難調試排查。

相關文章
相關標籤/搜索