通过析构优化代码(1)(译)

通过析构优化代码(1)(译)
Lary Wall说过:Perl让简单的事情做起来很简单,而难事也难不倒Perl。Perl即可以用来在紧要的最后一分钟写一个两行长度的脚本拯救整个世界(好吧,至少拯救你和你的项目),也可以用来写健壮的大型项目。然而,优秀的Perl编程技术在小程序和复杂应用中的体现是截然不同的。比如,我们考虑一下Perl的垃圾回收器。在大多数时候,它可以把一个程序员从各种内存管理问题中解放出来...直到该程序员创建了循环引用。

Perl的垃圾回收器对引用进行计数。当计数达到0(这意味着没人再持有对该资源的引用了),Perl就回收该资源。这种方法简单并且有效。然而,循环引用(当对象A持有一个对象B的引用,同时对象B也持有一个对象A的引用)会导致问题。即便程序中再没有其他东东拥有对象A或者对象B的引用,引用计数却永远无法回到0。对象A和对象B永不会被销毁。如果代码中不断地创建它们(也许在一个循环里),就会引发内存漏洞。程序分配的内存不合理地增加并且永远不会减少。这样的情况对于小型的运行一下就退出的程序也许还可以接受,但是对于24小时X365天运行的程序,比如mod_perl/FastCGI环境或者单机服务器,就不可接受了。

循环引用有时太有用,以致无法避免。一个通常的例子是一个树状的数据结构。为了双向导航──根节点到叶节点以及叶节点到根节点──一个父节点持有子节点的列表而子节点持有一个对父节点的引用。这里就有循环引用了。许多的CPAN模块使用这种方式实现它们的数据模型,包括HTML::Tree、XML::DOM和Text::PDF::File等。所有的这些个模块都提供一个方法来释放内存。客户程序必须在不再使用其对象的时候调用这个方法。然而这种需要显式调用方式令人不爽并且很容易导致不安全的代码:

##
## Code with a memory leak
#
use HTML::TreeBuilder;

foreach my $filename (@ARGV) {
my $tree = HTML::TreeBuilder->new;
$tree->parse_file($filename);

next unless $tree->look_down('_tag', 'img');
##
## Do the actual work (say, extract images) here
## ...
## and release the memory
##
$tree->delete;
}

该代码的问题是next语句:不含<img ... 标签的HTML文档不会被释放。其实,任何对next、last、return(子例程里)或者die(eval{}块里)的调用都是不安全的,将导致内存漏洞。当然,可以把释放代码挪到last或next的continue块中,编写代码在每次return或die之前删除树对象。但是这样容易使代码混乱。

这有更好的解决办法──RAII范式,即“资源获取即初始化(,析构即回收资源)。”(原文:"resource acquisition is initialization (and destruction is resource relinquishment).")(令人讽刺的是,名称的后半部分常常被省略,尽管这部分才是最重要的)。这个点子很简单。创建一个特别的guard对象(守卫另一个类)。它的职责是释放资源。当守卫对象被销毁的时候,它的析构器会删除树对象。代码看起来像这样:

##
## A special sentry object is employed
##
use HTML::TreeBuilder;

foreach my $filename (@ARGV) {
my $tree = HTML::TreeBuilder->new;
$tree->parse_file($filename);

my $sentry = Sentry->new($tree);

next unless $tree->look_down('_tag', 'img');
##
## next, last or return are safe here.
## Tree will be deleted automatically.
##
}

package Sentry;

sub new {
my $class = shift;
my $tree = shift;
return bless {tree => $tree}, $class;
}

sub DESTROY {
my $self = shift;
$self->{tree}->delete;
}

注意现在循环末尾不需要再显示调用$tree->delete了。这个魔法很简单。当程序运行到循环的作用域之外时,因为$sentry没有收到任何循环引用,因而它可以被回收。这时Sentry包的DESTROY方法被调用,它会删除$tree对象。这样一切迎刃而解,无论你如何跳出循环代码块,内存都会被回收。

最后,其实你不需要自己来写这个Sentry类。可以使用由Adam Kennedy编写的Object::Destroyer模块。从名字上你可以猜到:它是一个用来销毁其他对象的对象:

##
## An of-the-CPAN solution with Object::Destroyer
##
use HTML::TreeBuilder;
use Object::Destroyer 2.0;

foreach my $filename (@ARGV) {
my $tree = HTML::TreeBuilder->new;
my $sentry = Object::Destroyer->new($tree, 'delete');
$tree->parse_file($filename);

next unless $tree->look_down('_tag', 'img');
##
## You can safely return, die, next or last here.
##
}

由于不同模块的释放方法的名称可能不同,构造器的第二个参数用来指定释放方法的名称。

英文原文: http://www.perl.com/pub/a/2007/06/07/better-code-through-destruction.html