本系列: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好呢?
对于对象之间的引用环路来讲,转换父类比转换子类好,由于父类只要不须要了就能够直接销毁,此时子类也会随之销毁。而转换子类时,子类在不须要的时候被销毁,但父类可能还在引用别的,也就是说父类不必定会被销毁。
另外,在使用弱引用的时候要很是当心,能不用的时候尽可能别用,不然一出问题,很是难调试排查。