本系列:html
第3篇依賴於第2篇,第2篇依賴於1篇。數組
perl中使用引用計數的方式管理內存,對象也是引用,因此對象的銷燬也是由引用計數的管理方式進行管理的。也就是說,當一個對象(也就是一個數據結構)引用數爲0時,這個對象就會被Perl回收。數據結構
對象回收的俗稱是"對象銷燬"(destroy),術語是解構(destruction),在Perl中回收對象是經過一個名爲DESTROY的特殊方法進行回收的,和構造器建立對象相反,這個方法解除構造,因此稱之爲解構器(destructor)。less
當Perl中對象的最後一個引用要消失時,Perl將自動調用DESTROY方法。Perl處理DESTROY的方式和普通方法同樣:函數
和普通方法不一樣的是,DESTROY是在對象被銷燬時自動調用的。測試
須要搞清楚的是,DESTROY這個特殊方法是當對象的引用數將要爲0以前調用的,該方法執行完成後,對象相關的數據結構才被徹底釋放,引用數才真正變成0。因此,在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,而後進行額外的擴展,不然本該父類善後的操做會被遺漏。
例如,爲子類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好呢?
對於對象之間的引用環路來講,轉換父類比轉換子類好,由於父類只要不須要了就能夠直接銷燬,此時子類也會隨之銷燬。而轉換子類時,子類在不須要的時候被銷燬,但父類可能還在引用別的,也就是說父類不必定會被銷燬。
另外,在使用弱引用的時候要很是當心,能不用的時候儘可能別用,不然一出問題,很是難調試排查。