Perl传统的面向对象编程
2013-07-26 17:36:27 阿炯

本站赞助商链接,请多关照。 NAME
perlobj - Perl 对象

说明
首先你必须懂得在 Perl 中,什么叫做“引用”,如果你还不懂,那么请参考
perlref。 其次,如果你仍然觉得下文出现的引用过于复杂的话,那么请先阅读
perltoot 和 perltooc 这两个 Perl 的面向对象编程初级教程。

首先让我们来看看有关 Perl 面向对象编程的三个基本定义:

1.  一个“对象”是指一个“有办法知道它是属于哪个类”的简单引用。

2.  一个“类”是指一个“有办法给属于它的对象提供一些方法”的简单的包。

3.  一个“方法”是指一个“接受一个对象或者类名称作为第一个参数”的简单的
子程序。

我们暂时不考虑从更深一层的角度来讲,以上说法是否正确。

 对象仅仅只是引用
和 C++ 不同,Perl 没有为“构造函数”提供任何特殊的语法(译者注:在 C++
中,和类名称相同的类方法被称为“构造函数”,创建对象时被自动调用)。Perl
中,构造器(译者注:因为 Perl 不强调“函数”这个概念,因此从下文中一律译
为“构造器”)只是一个会返回一个“经过 bless 处理”的引用的子程序,这个
经过 bless 处理的引用就是人们所说的“对象”,而 bless 的作用就是用来说明
这个对象是隶属于哪个“类”。

下面就是一个典型的构造器的例子:

package Critter;
sub new { bless {} }

new 这个词并没有任何特殊的含义,如果你喜欢,你也可以写成这样:

package Critter;
sub spawn { bless {} }

这样做不会使 C++ 程序员误以为 "new" 有什么特殊的含义,因此或许更加合理一
些。我们建议你给你的构造器起名时尽量选择能够准确反应它在你的解决方案中的
意义的名字。而不要拘泥于 new 或者其它那些千篇一律的名字。例如在 Perl/Tk
中,组件的构造器就叫做“create”。

和 C++ 相比,Perl 的构造器有一点不同,那就是它不会自动调用基类的构造器。
因为 hash 可以轻易地表示“名字=>值”这样的属性对,因此通常我们用一个匿名
hash 引用来储存对象的各个属性。在上例中,用一对大括号 "{}" 可以生成一个
空的匿名 hash 引用,然后 bless() 函数给它打上一个印记,让它变成一个
Critter 类的对象,最后返回这个对象。这么做只是为了图个方便,因为对象自身
知道它是被 bless 过的,并且 bless {} 正好是 sub new 的最后一个语句(也是
唯一的语句),所以可以直接做为返回值,不需要显式地 return。

实际上,sub new { bless {} } 写全了相当于下面的代码段:

sub new {
my $self = {};
bless $self;
return $self;
}

有时候你经常会见到更复杂一些的构造器,比如它可能会调用另外一个方法去做一
些构造工作:

sub new {
my $self = {};
bless $self;
$self->initialize();    # 注意这里
return $self;
}

如果你小心地处理继承的话(这种情况经常碰到,参见
""模块的创建、使用和重用" in perlmodlib),那么你可以用两个参数来调用
bless,因此你的构造器就可以实现继承:

sub new {
my $class = shift;
my $self = {};
bless $self, $class;
$self->initialize();
return $self;
}

如果你希望用户不仅能够用 "CLASS->new()" 这种形式来调用你的构造函
数,还能够以 "$obj->new()" 这样的形式来调用的话,那么就这么做:

sub new {
my $this = shift;
my $class = ref($this) || $this;
my $self = {};
bless $self, $class;
$self->initialize();
return $self;
}

需要注意的是,这样作并不会发生任何拷贝动作。如果你希望拷贝一个对象,那
么你需要自己写代码处理。接下来 bless 的第二个参数 $class 所属的
initialize() 方法将被调用。

在类的内部,所有的方法都把对象当作一个普通的引用来使用。而在类的外部,用
户只能看到一个经过封装的对象,所有的值都是不透明的,只能通过类的方法来访
问。

虽然理论上我们可以在构造器中重新 bless 一个对象到别的类。对一个对象再次
进行 bless,将导致这个对象术语新类,而忘记原先的老类。我们应该保持一个对
象始终只属于一个,所以我们不建议这么做。但是如果有谁真的这么做,那纯粹是
自找麻烦。

澄清一下:对象是经过 bless 的,引用变量则没有。对象知道它被 bless 到了哪
个类,而引用变量不知道。bless 处理的实际上是引用指向的对象,而不是引用变
量自身。考虑下面的例子:

$a = {};
$b = $a;
bless $a, BLAH;
print "\$b is a ", ref($b), "\n";

结果显示 $b 也被 bless 到 BLAH 类了,由此可见,bless() 操作的是对象而不
是引用变量。

  一个类只是一个简单的包
和 C++ 不同,Perl 并不为类定义提供任何特殊语法。实际上类只是一个包而已。
你可以把一个包当作一个类用,并且把包里的函数当作类的方法来用。

不过,有一个特殊的数组,叫做 @ISA,它说明了“当 Perl 在当前包中找不到想
要的方法时,应当继续从哪儿去找”。这就是 Perl 实现“继承”的关键。@ISA
中的每个元素都是一个别的包的名字。当类找不到方法时,它会从 @ISA 数组中
依次寻找(深度优先)。类通过访问 @ISA 来知道哪些类是它的基类。

所有的类都有一个隐含的基类(祖先类):"UNIVERSAL"。"UNIVERSAL" 类为它
的子类提供几个通用的类方法。参见 "默认的 UNIVERSAL 方法" 得到更多说明。

如果在基类中找到了缺失的方法,那么为了提高效率,它会被缓存到当前类。每
当修改了 @ISA 或者定义了新的子程序时,缓存会失效,这将导致 Perl 重新做
一次查找。

如果在当前类、当前类所有的基类、还有 UNIVERSAL 类中都找不到请求的方法,
这时会再次查找名为 AUTOLOAD() 的一个方法。如果找到了 AUTOLOAD,那么就会
调用,同时设定全局变量 $AUTOLOAD 的值为缺失的方法的全限定名称。

如果还不行,那么 Perl 就宣告失败并出错。

如果你不想继承基类的 AUTOLOAD,很简单,只需要一句

sub AUTOLOAD;

就行了。然后调用 AUTOLOAD 时就会失败。

Perl 类只有方法继承。数据继承由程序员自己实现。基本上,对 Perl 来讲这不
是一个什么大问题:因为我们大多数时候都用匿名 hash 来储存对象数据,而每
一层的基类都可以往 hash 表中加入自己的属性,因此子类自然就可以继承基类
的属性。唯一的问题发生在基类和子类使用了同一个名字作为 hash 键值时。不
防假设基类已经使用了 'city' 这个键名,这时子类中也想用 'city' 这个键,
那么很明显将会覆盖,由于子类在设计时无法知道父类中是否已经使用了 'city'
所以似乎这的确是一个问题。有一个变通方法就是,每一层类都优先考虑使用自
己的包名称作为 hash 键的前缀:

sub bump {
my $self = shift;
$self->{ __PACKAGE__ . ".count"}++;
}

这样你就可以在父类和子类中访问同一个属性的不同版本。

  一个方法就是一个简单的子程序
和 C++ 不同,Perl 不提供任何特殊的语法来定义方法。(不过 Perl 提供了一
个特殊的语法用来调用方法,稍后再讲)。方法把它被调用时的对象或者类名称
当作它的第一个参数。有两种不同的调用方法的途径,分别成为“调用类方法”
和“调用实例方法”。

类方法把类名当作第一个参数。它提供针对类的功能,而不是针对某个具体的对
象的功能。构造器通常是一个类方法,参见 perltoot 或者 perltooc。
大多数类方法简单地忽略第一个参数,因为方法知道自己处在什么类里面,也不
关心它是通过什么类来调用的。(调用类和所处类不一定相同,例如基类的方法
被子类调用时,方法的所处类是基类,而调用类是子类,类方法和对象方法都是
如此。) 举个常见的例子,下面的类方法可以通过名字来查询对象:

sub find {
my ($class, $name) = @_;
$objtable{$name};
}

实例方法把对象作为它的第一个参数。因此典型的做法是把第一个参数 shift
到一个名为“self”或者“this”的变量中。然后再把它当作一个引用来用:

sub display {
my $self = shift;
my @keys = @_ ? @_ : sort keys %$self;
foreach $key (@keys) {
print "\t$key => $self->{$key}\n";
}
}

  调用方法
出于历史遗留的原因,Perl 提供了两种不同的形式去调用一个方法。最简单的
形式是采用箭头符号:

my $fred = Critter->find("Fred");
$fred->display("Height", "Weight");

你可以早就熟悉了引用的 "->" 操作符。事实上,因为上面的 $fred
是一个指向了对象的引用,因此你也可以把箭头操作符理解为另外一种形式的
解引用。

出现在箭头左边的引用或者类名,将作为第一个参数传递给箭头右边的方法。
所以上面的代码就分别相当于这样:

my $fred = Critter::find("Critter", "Fred");
Critter::display($fred, "Height", "Weight");

Perl 怎么知道箭头右边的子程序是哪个包里的呢?答案是通过查看箭头左边的
内容。箭头左边必须是一个对象,或者是一个标识类名的字符串。这两种情况
都行。如果类里没有这个方法,那么 Perl 就从基类中进行检索。

如果必要,你还*可以*强制 Perl 检索其它类:

my $barney = MyCritter->Critter::find("Barney");
$barney->Critter::display("Height", "Weight");

这个例子中,"MyCritter" 类是 "Critter" 类的子类,并且定义了自己的 find()
和 display() 方法。通过加前缀 "Critter::" 可以强制 Perl 执行 "Critter"
的方法而不是 "MyCritter" 自己的方法。

上面的例子还有一种特殊情形,那就是你可以用 "SUPER" 伪类来告诉 Perl
通过当前包的 @ISA 数组来检索究竟应该使用哪个类。

package MyCritter;
use base 'Critter';    # sets @MyCritter::ISA = ('Critter');

sub display {
my ($self, @args) = @_;
$self->SUPER::display("Name", @args);
}

注意:"SUPER" 表示 *当前包* 的 超类 而不是 *对象* 的 超类。
而且,"SUPER" 符号仅仅只是一个方法名称的 修饰符,因此不能把它当作
类名称使用在其它地方。记住:"SUPER" 不是类名称,只是修饰符。例如:

something->SUPER::method(...);      # OK
SUPER::method(...);                 # WRONG
SUPER->method(...);                 # WRONG

最后一点,箭头左边的类名或者对象,也可以用返回类名或者对象的表达式来代
替。所以下面这句是合法的:

Critter->find("Fred")->display("Height", "Weight");

这句也是合法的:

my $fred = (reverse "rettirC")->find("Fred");

  间接对象语法
另外一种调用方法的方式称为“间接对象”语法。这条语法早在 Perl4 时代就
已经引进,那时还没有对象这个概念。它也可以用在文件句柄上:

   print STDERR "help!!!\n";

同样的语法可以调用对象或者类的方法:

   my $fred = find Critter "Fred";
   display $fred "Height", "Weight";

注意在对象/类名称与参数之间不能有逗号,这种语法告诉 Perl 你想要调用一
个对象方法而不是普通的子程序。

但是如果没有参数怎么办?(译者注:这时方法后面只有一个对象/类名称,因
此不能一眼看出到底是想调用一个方法,还是用对象/类名称做参数调用一个普
通的子程序)。这时,Perl 只能猜测你的想法,更糟糕的是,Perl 是在“编
译时”就进行猜测!通常 Perl 可以作出正确的判断,但是有时 Perl 会把一
个函数调用编译成一个类方法,或者把一个类方法编译成一个函数调用。这可
能会导致出现难以察觉的错误。

例如,有一个 "new" 方法的间接调用--C++ 程序员通常喜欢这么做--可
能被编译成一个子程序调用,前提是如果碰巧在当前作用域有一个子程序也叫
"new"。你的代码最终会调用当前包的 "new" 子程序,而不是你想要的类方法。

TODO The compiler tries to cheat by remembering bareword "require"s, but
the grief when it messes up just isn't worth the years of debugging it
will take you to track down such subtle bugs.

这个语法还有一个问题:间接对象仅限于一个名称、或者一个标量、或者一个
块,之所以这么做是因为优先级的问题。如果不加这个限制的话,将导致 Perl
在分析你的程序时需要多做很多向前扫描工作,比如解引用之类的。
这个诡异的规则同样适用于 "print" 和 "printf"。

请看下面的两行

move $obj->{FIELD};                 # 很可能是错的!
move $ary[$i];                      # 很可能是错的!

上面两行从 Perl 理解的角度来看,相当于:

$obj->move->{FIELD};                # 看这儿!
$ary->move([$i]);                   # 你真的想这样吗?

而你真正希望的也许是:

$obj->{FIELD}->move();              # 这么做多好
$ary[$i]->move;                     # 这可能才是你的意思。

要想用间接对象语法正确的表达你的意图,你得加上花括号:

move {$obj->{FIELD}};
move {$ary[$i]};

即使是这样,还是会存在隐患(考虑如果当前包就有一个名为 "move" 的函
数,那么很显然 Perl 会把它理解成函数调用而不是间接对象调用)。因此,
我们大力推荐你只使用 "->"!。不管怎样,你仍然会看到很多以前
遗留下来的间接对象语法,因此熟悉它们还是很有必要的。

  默认的 UNIVERSAL 方法
"UNIVERSAL" 包为它的子类提供如下几个方法:

isa(CLASS)
如果调用 "isa" 的对象是隶属于 "CLASS" 或者它的子类, 那么 "isa" 返回
*真值*。

你也可以用传递两个参数的办法直接调用 "UNIVERSAL::isa":第一个参数是
一个对象(甚至是普通的引用),这个办法可以用来检查一个对象是不是属于
指定的类型。例如:

if(UNIVERSAL::isa($ref, 'ARRAY')) {
#...
}

要想确定一个引用是不是一个 bless 过的对象,你可以这么写:

print "It's an object\n" if UNIVERSAL::isa($val, 'UNIVERSAL');

can(METHOD)
"can" 检查一个对象是不是拥有一个叫做 "METHOD" 的方法。如果有,那么
将返回那个方法(实际上就是子程序)的引用。如果没有,那么返回 *undef*

也可以用两个参数来直接调用 "UNIVERSAL::can"。当第一个参数不是一个对
象或者是类名的时候,它返回 *undef*,所以我们也可以用这个办法知道一个
引用是不是一个对象。

print "It's still an object\n" if UNIVERSAL::can($val, 'can');

你也可以用 Scalar::Util 模块的 "blessed" 函数来达到同样的目的:

use Scalar::Util 'blessed';

my $blessing = blessed $suspected_object;

如果 $suspected_object 是一个对象,那么 "blessed" 返回对象所属的类名
称,不然返回 "undef"。

VERSION( [NEED] )
"VERSION" 返回一个类的版本号。如果提供了 NEED 参数,那么它还会检查
当前版本号(就是类里面的那个 $VERSION 变量)是不是小于 NEED,如果小
于,它会导致 Perl 程序 die。此方法通常作为一个类方法来调用。

use 语句中会自动调用此方法,请看下面:

use A 1.2 qw(some imported subs);

上面的语句相当于隐含地调用了:

A->VERSION(1.2);

注意: "can" 直接在 Perl 内部实现,"isa" 也是,并且还采用了缓冲
技术。因此当你的程序动态地修改 @ISA 数组时,可能会出现稀奇古怪的问题。

你也可以通过 Perl 程序或者 XS 程序自己给 UNIVERSAL 类添加方法,并且
不需要 "use UNIVERSAL" 就可以在你的程序中使用新加的方法。

  析构器
当对象的最后一个引用释放时,对象会自动析构。(如果你把对象储存在全局
变量中,那么一直到你的程序退出时才会析构)。如果你想在析构的时候做些
什么,那么你可以在类中定义一个名为“DESTROY”的方法。它将在适合的时机
自动调用,并且按照你的意思执行额外的清理动作。Perl 会把对象的引用作为
唯一的参数传递给 DESTROY。注意这个引用是只读的,也就是说你不能通过访问
$_[0] 来修改它。(译者注:参见 perlsub)但是对象自身(比如 "${$_[0]"
或者 "@{$_[0]}" 还有 "%{$_[0]}" 等等)还是可写的,

如果你在析构器返回之前重新 bless 了对象引用,那么 Perl 会在析构器返回
之后接着调用你重新 bless 的那个对象的 DESTROY 方法。这可以让你有机会
调用基类或者你指定的其它类的析构器。需要说明的是,DESTROY 也可以手工
调用,但是通常没有必要这么做。

在当前对象释放后,包含在当前对象中的其它对象会自动释放(假如别的地方没
有什么引用指向它们的话)。

  摘要
以上就是所有的内容了 ^_^。
你现在需要做的就是马上出去买本书,关于“面向对象设计模式”的那种(相信
很好买到,因为这种书现在满大街都是)然后一头扎进去,啃上至少 6 个月再
出来 ^_^

  Two-phased 垃圾回收
为了更多的目的,Perl 采用了一套快速、简便,基于引用计数的垃圾回收机制。
这意味着有一些额外的解引用操作发生在某个层次,因此如果你不用 C 编译器 的
"-O" 开关编译你的 Perl 的话,性能会有些损失,如果你的 Perl *已经* 是用
"cc -O" 编译过的了,那就没什么问题。

还有个很严重的问题:有时候引用计数根本就不可能为 0,也就是说内存永远
不会释放。例如下面的代码就有这个问题:

{
my $a;
$a = \$a;
}

虽然 $a 已经完全超出作用域了,但是它还是不能释放。当创建递归数据结构
时,你必须明确打破这种*自引用*,否则内存就会泄漏。例如,下面就是一个
引用了自身的节点(类似的代码可能会出现在树型结构中):

sub new_node {
my $class = shift;
my $node  = {};
$node->{LEFT} = $node->{RIGHT} = $node;
$node->{DATA} = [ @_ ];
return bless $node => $class;
}

如果你创建这样的节点,那么内存就会无法自动释放,除非你自己打断自引用
的结构。换句话说,这不是一个特性,所以你别指望它。

差不多就这些。

当一个解释器线程最后快要退出的时候(通常发生在你的程序结束时),就会
进行垃圾回收,然后所有这个线程拥有的对象都会释放。这一点对于嵌入式的
Perl 应用或者多线程程序非常重要。下面这个程序演示了 Perl 的 two-phased
垃圾回收:

#!/usr/bin/perl
package Subtle;

sub new {
my $test;
$test = \$test;
warn "创建 " . \$test;
return bless \$test;
}

sub DESTROY {
my $self = shift;
warn "销毁 $self";
}

package main;

warn "开始运行";
{
my $a = Subtle->new;
my $b = Subtle->new;
$$a = 0;  # break selfref
warn "块结束之前";
}

warn "块结束之后";
warn "程序结束...";
exit;

好比我们把它保存成 /foo/test,那么输出结果就应该是下面这样的:

开始运行 at /foo/test line 18.
创建 SCALAR(0x8e5b8) at /foo/test line 7.
创建 SCALAR(0x8e57c) at /foo/test line 7.
块结束之前 at /foo/test line 23.
销毁 Subtle=SCALAR(0x8e5b8) at /foo/test line 13.
块结束之后 at /foo/test line 26.
程序结束... at /foo/test line 27.
销毁 Subtle=SCALAR(0x8e57c) during global destruction.

注意看“global destruction”这个地方!这里就是线程的垃圾回收阶段。
甚至可以回收自引用的数据。

对象总是要析构,并且是在引用还没有析构的时候就进行析构,这样就可以阻止
把一个已经析构的引用进行对象析构。普通引用仅仅只有当析构层次大于 0 时
才开始析构。

//TODO: You can test the higher levels of global destruction by setting
the PERL_DESTRUCT_LEVEL environment variable, presuming "-DDEBUGGING"
was enabled during perl build time. See "PERL_DESTRUCT_LEVEL" in
perlhack for more information.

一个更加完整的垃圾回收机制将在不久的将来完成。

在此期间,最好的解决方案就是创建一个非递归结构的包容器类,该包容器类包
含有一个指针指向自引用的数据结构。然后为该被包容的对象类定义一个 DESTROY
方法,用 DESTROY 手工打破自引用的结构的循环。

参见
Perl 有几个入门级的面向对象编程教程,它们是 perltoot, perlboot 和
perltooc。你还可以在 perlbot 里看到关于对象的技巧、陷阱、和提示。
另外,perlmodlib 中还有一些制作自己的类和模块的指南。

翻译者及翻译声明
本文由 flw ("flw@cpan.org") 翻译,翻译成果首次出现在 *中国 Perl 协会*
http://www.perlchina.org) 的协作开发平台上。

PerlChina.org 本着“在国内推广 Perl” 的目的,组织人员翻译本文。读者可
以在遵守原作者许可协议、尊重原作者及译作者劳动成果的前提下,任意发布或
修改本文。

如果你对本文有任何意见,欢迎来信指教。本人非常欢迎与各位交流。
    
上节系perlobj.pod的中文翻译,为ChinaUnix版主flw的作品,感谢原作者。



Perl类是仅是一个包而已,当你看到Perl文档中提到“类”时,把它看作“包”就行了。构造函数是类的子程序,它返回与类名相关的一个引用。将类名与引用相结合称为“祝福”一个对象,因为建立该结合的函数名为bless(),其语法为:
 bless YeReference [,classname]
 YeReference是对被“祝福”的对象的引用,classname是可选项,指定对象获取方法的包名,其缺省值为当前包名。

Perl类的方法只不过是一个Perl子程序而已,也即通常所说的成员函数。Perl的方法定义不提供任何特殊语法,但规定方法的第一个参数为对象或其被引用的包。下面通过一个示例来简单地演示Perl面向对象的用法。

1、说明
本程序演示了在Perl中如何进行面向对象编程:演示程序包含两个文件:queryper.pl 和 queryper.pm,把 queryper.pl 和 queryper.pm 放在当前目录下,运行"perl queryper.pl"即可以看到结果。queryper.pm定义了名为queryper的类。queryper.pl中创建了queryper类的实例,并测试了queryper的成员方法。

2、queryper.pm的内容
package queryper;
use strict;

sub new {
 my $class = shift();
 print("CLASS = $class\n");
 my $self = {};
 $self->{"name"} = shift();
 $self->{"age"} = shift();
 bless $self, $class;
return $self;
}

sub getname {
 my ($self) = @_;
  return $self->{"name"};
}

sub setname {
 my ($self, $name) = @_;
 $self->{"name"} = $name;
}

sub getage {
 my ($self) = @_;
 return $self->{"age"};
}

sub setage {
 my ($self, $age) = @_;
 $self->{"age"} = $age;
}

1;

3、queryper.pl的内容
#!/usr/bin/perl -w
use strict;
use queryper;

my $tom = queryper->new("Tom", "27");
my $kiss = queryper->new("Kiss", "29");


my @querypers = ($tom, $kiss);
for my $p (@querypers) {
 printf("name: %s\tage: %s\n", $p->getname(), $p->getage());
}

4、程序运行结果
CLASS = queryper
CLASS = queryper
name: Tom      age: 27
name: Kiss      age: 29

5、当调用queryper->new("Tom", "27")时发生了哪些事情
下面我们将重点分析new函数代码:

当调用queryper->new("Tom", "27")时,有3个参数被传递给queryper中的new函数,它们分别是: 类的名称"queryper"(这是一个隐含参数)、"Tom"、"27"。在调用new方法时多用于设定一些初始化的信息,像到数据库的连接,文件句柄的开启等操作。当然也不一定要用new函数,也可以用init函数来做此类工作,这不是规定的。

语句my $class = shift();获得第一个参数

语句my $self = {};创建一个哈希表对象的引用

语句$self->{"name"} = shift(); 把传进来的第二个参数存入到哈希表中

语句$self->{"age"} = shift(); 把传进来的第三个参数存入到哈希表中

语句bless $self, $class,我把bless理解为"把类型信息和变量绑定" :
$self是一个指针,在这里指向了一个哈希表对象的引用
$class是一个字符串,在这里是queryper类的名字

在perl中,根据类的名字可以查找到这个类所有的成员方法和成员变量,因此类的名称就代表了类的类型信息.

所谓bless就是把 queryper类的类型信息赋予哈希表对象的引用

语句'return $self;'则返回哈希表对象的引用


学习Perl面向对象

本系列:
Perl面向对象(1)之从代码复用开始
Perl面向对象(2)之对象
Perl面向对象(3)之解构--对象销毁


第3篇依赖于第2篇,第2篇依赖于1篇。本块转自骏马金龙的博客空间,感谢原作者。

Perl面向对象(1)之从代码复用开始


Perl面向对象的三个准则

1.类就是包
2.对象就是一个数据结构的引用,是知道自己属于哪个类的引用
2.1.可以是数据结构引用(如hash结构、数组结构),也可以是子程序引用
3.方法就是子程序

最初代码

3种动物牛Cow、羊Sheep、马Horse发出的声音各不相同。在lib目录下创建三个各自的文件,分别定义它们的叫声子程序:
lib/Cow.pm中:
#!/usr/bin/env perl
use strict;
use warnings;
package Cow;

sub speak {
    print "a Cow goes moooo!\n";
}
1;

lib/Sheep.pm中:
#!/usr/bin/env perl
use strict;
use warnings;
package Sheep;

sub speak {
    print "a Sheep goes baaaah!\n";
}
1;

lib/Horse.pm中:
#!/usr/bin/env perl
use strict;
use warnings;
package Horse;

sub speak {
    print "a Horse goes neigh!\n";
}
1;

然后定义一个文件speak.pl,使用这3个模块,分别调用这3个子程序:
#!/usr/bin/env perl
use strict;
use warnings;

use lib "lib";
use Cow;
use Sheep;
use Horse;

Cow::speak();
Sheep::speak();
Horse::speak();

使用箭头的调用方法

上面使用包名的完全限定方式调用子程序(或访问其它属性),这其实是有一定限制的,比如无法直接使用包名作为变量:
foreach my $who (qw(Cow Horse Sheep)){
    $who::speak();   # 这是错的
    eval "$who"."::speak()";  # 这是正确的
}

上面通过eval的二次解析功能,先将变量$who替换,然后再调用对应的方法。

但这种写法无比丑陋。可以使用另外一种访问其它包中的子程序(或其它属性):瘦箭头。

foreach my $who (qw(Cow Horse Sheep)){
    $who->speak();
}

其实这是面向对象的调用方式。通过这种方式调用其它包的子程序,传递给子程序的第一个参数将总是对象名或类名。在Perl中,类就是包,所以下面几个调用方式是等价的:
瘦箭头调用方式                完全限定包名调用方式
---------------------------------------------------
Cow->speak(args)     ==    Cow::speak('Cow',args)
Sheep->speak(args)   ==    Sheep::speak('Sheep',args)
Horse->speak(args)   ==    Horse::speak('Horse',args)

因此,当使用瘦箭头调用子程序的方式时,如果这个子程序需要处理参数,必须要考虑隐含的第一个参数。所以,修改lib/{Cow,Sheep,Horse}.pm中的speak()子程序:

lib/Cow.pm中:
#!/usr/bin/env perl
use strict;
use warnings;
package Cow;

sub speak {
    my $class = shift;  # 将第一个参数保存起来
    print "a $class goes moooo!\n";  # 插入第一个参数的变量
}
1;

lib/Sheep.pm中:
#!/usr/bin/env perl
use strict;
use warnings;
package Sheep;

sub speak {
    my $class = shift;  # 将第一个参数保存起来
    print "a $class goes baaaah!\n";  # 插入第一个参数的变量
}
1;

lib/Horse.pm中:
#!/usr/bin/env perl
use strict;
use warnings;
package Horse;

sub speak {
    my $class = shift;  # 将第一个参数保存起来
    print "a $class goes neigh!\n";  # 插入第一个参数的变量
}
1;

这样一来,将硬编码的Cow、Sheep和Horse使用共同的$class进行替换,增加了speak()的可移植性和共性,从而为面向对象的代码复用带来便捷性。

初步理解类和对象

所谓的类,就像是一个模板;所谓对象,就像是通过模板生成的具体的事物。类一般具有比较大的共性,对象一般是具体的,带有自己的特性。

类与对象的关系,例如人类和人,鸟类和麻雀,交通工具和自行车。其中人类、鸟类、交通工具类都是一种类型称呼,它们中的任何一种都具有像模板一样的共性。例如人类的共性是能说话、有感情、双脚走路、能思考等等,而根据这个人类模板生成一个人,这个具体的人是人类的实例,是一个人类对象,每一个具体的人都有自己的说话方式、感情模式、性格、走路方式、思考能力等等。

类与类的关系。有的类的范畴太大,模板太抽象,它们可以稍微细化一点,例如人类可以划分为男性人类和女性人类,交通工具类可以划分为烧油的、电动的、脚踏的。一个大类按照不同的种类划分,可以得到不同标准的小类。无论如何划分,小类总是根据大类的模板生成的,具有大类的共性,又具有自己的个性。

在面向对象中,小类和大类之间的关系称之为继承,小类称之为子类,大类称之为父类。

类具有属性,属性一般包括两类:像名词一样的属性,像动词一样的行为。例如,人类有父母(parent),parent就是名词,人类能吃饭(eat),eat这种行为就是动词。鸟类能飞(fly),fly的行为就是动词,鸟类有翅膀(wing),wing就是名词。对于面向对象来说,名词就是变量,动词行为就是方法(也就是子程序)。

当子类继承了父类之后,父类有的属性,子类可以直接拥有。因为子类一般具有自己的个性,所以子类可以定义自己的属性,甚至修改从父类那里继承来的属性。例如,人类中定义的eat属性是一种非常抽象的、共性非常强的动词行为,如果女性人类继承人类,那么女性人类的eat()可以直接使用人类中的eat,也可以定义自己的eat(比如淑女地吃)覆盖从人类那里继承来的eat(没有形容词的吃),女性人类还可以定义人类中没有定义的跳舞(dance)行为,这是女性人类的特性。子类方法覆盖父类方法,称之为方法的重写(override),子类定义父类中没有的方法,称为方法的扩展(extend)。

无论是对象与类还是子类与父类,它们的关系都可以用一种"is a"来描述,例如"自行车 is a 交通工具"(对象与类的关系)、"笔记本 is a 计算机"(子类与父类的关系)。

辅助子程序让代码更具共性

为了构造更通用的speak,将它们的不同点抽取出来:动物名称、叫声。

动物名称这里和类名(包名)相同,前面已经替换成了共同的$class,动物的叫声是随动物种类不同而不同的,无法直接实现它们的共性。但可以定义一个辅助性的同名子程序sound(),用来返回各种动物的叫声:

lib/Cow.pm中:
#!/usr/bin/env perl
use strict;
use warnings;
package Cow;

sub sound { "moooo"; }

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

1;

lib/Sheep.pm中:
#!/usr/bin/env perl
use strict;
use warnings;
package Sheep;

sub sound { "baaaah"; }

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

1;

lib/Horse.pm中:
#!/usr/bin/env perl
use strict;
use warnings;
package Horse;

sub sound { "neigh"; }

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

1;

如此一来,lib/{Cow,Horse,Sheep}.pm中的所有speak()子程序都完全相同。

继承

显然,将这3个类中的共同部分抽取出来放进一个通用的模块中进行复用更好。例如放进lib/Animal.pm模块文件中:
#!/usr/bin/env perl
use strict;
use warnings;
package Animal;

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

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

1;

为了让Cow、Horse、Sheep直接使用Animal中的speak()子程序,需要让Cow、Horse、Sheep去继承Animal。其中Animal类称为父类(base class/super class/parent class),Cow、Horse、Sheep称为子类(subclass/child class)。类在继承的同时会继承父类中的方法。在面向对象中,方法就是子程序。所以,子类Cow、Horse、Sheep可以直接使用父类Animal中的speak()方法。

于是修改lib/{Cow,Sheep,Horse}.pm文件。

lib/Cow.pm中:
#!/usr/bin/env perl
use strict;
use warnings;

package Cow;
use Animal;          # 先装载Animal
our @ISA=qw(Animal); # 继承Animal

sub sound {"moooo"}

1;

lib/Horse.pm中:
#!/usr/bin/env perl
use strict;
use warnings;

package Horse;
use Animal;
our @ISA=qw(Animal);

sub sound { "neigh" }

1;

lib/Sheep.pm中:
#!/usr/bin/env perl
use strict;
use warnings;

package Sheep;
use Animal;
our @ISA=qw(Animal);

sub sound { "baaaah" }
1;

上面的三个模块文件中,完全没有定义speak(),只是使用our @ISA=qw(Animal);的方式声明了各自的类继承Animal类。但在speak.pl中可以直接调用speak()方法:
#!/usr/bin/env perl
use strict;
use warnings;

use lib "lib";
use Cow;
use Sheep;
use Horse;

foreach my $who (qw(Cow Horse Sheep)){
    $who->speak();
}

上面父类Animal中,还定义了一个sound(),这是可选的,因为各个子类都定义了属于自己的sound()。但强烈建议在Animal中也定义好,因为在当前Animal类中的speak()方法中调用了该方法,且Animal所有子类都重写了sound(),它代表了一种共性。

换个角度,一般是从父类开始写程序的,sound()作为具有共性的方法,应该要先定义在父类中。那么子类中的sound()是重写父类sound()而来,所以父类的sound()可以非常抽象,甚至不提供任何功能,仅仅充当一个占位符。

在此实例中,Animal中的sound()也确实没有提供任何和叫声有关的功能,仅仅只是做了一层检测,当调用到了父类的sound()时,将报错。这表示子类没有定义属于自己的sound(),也就是没有重写父类的sound()。对于这种抽象的方法,每个子类都应该去重写,定义属于自己的特性。

关于@ISA

继承的方式有3种:

1.使用base模块:use base qw(Animal);
2.使用parent模块:use parent qw(Animal);
3.使用@ISA数组:

use Animal;
our @ISA = qw(Animal);

它们之间并没有多大区别,但需要注意的是,@ISA只是声明继承的一种方式,算是比较古老的写法,parent模块是在perl v5.10.1才引入的功能,大概是2000年左右,因此如果是此版本之前的perl,需要使用base模块,或者安装parent模块。

base和parent模块的本质还是@ISA。@ISA表示的是is a的关系,这是典型的对象与类、子类与父类的关系解释。

当调用一个方法时,如果在自己的类中找不到,将从@ISA数组中定义的父类中寻找,能找到则直接调用,不能找到则报错。例如上面三个子类都没有定义speak(),当在speak.pl文件中调用这3个模块中的speak()时,perl将首先搜索各类(或包)中的speak,因为找不到,所以找父类Animal的speak(),能找到,所以成功调用。

重写父类方法

子类要实现自己独有的特性,除了定义父类中没有的属性之外,还可以重写从父类继承的方法。例如Cow、Horse、Sheep中的sound()就重写了父类Animal的sound()。

虽然理论上父类中的speak()可以重写,但很可能是没有必要重写甚至不应该重写的,因为重写可能会带来破坏,使得能使用父类的地方不能使用子类(参考"里氏替换原则")。

另外,强烈建议尽量扩展父类的行为,而不是修改父类的行为。

例如,新添加一个老鼠子类,它的speak()除了叫一声外,还多叫一声。lib/Mouse.pm文件内容如下:
#!/usr/bin/env perl

use strict;
use warnings;

package Mouse;
use Animal;
our @ISA =qw(Animal);

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

现在,在speak.pl中调用Mouse的speak()。

use Mouse;
Mouse->speak();

上面执行并没有问题。但问题出现了,如果Animal中的goes单词修改成了says,那么Mouse中的goes也得改成says。这种方式并不合理,所以,必须得保证子类的speak()和父类的speak()能保持以执行,而不能在子类种对任何共性的部分进行硬编码。所以将Mouse的speak()的第一个print,改为调用Animal中的speak()。

#!/usr/bin/env perl
use strict;
use warnings;

package Mouse;
use Animal;
our @ISA =qw(Animal);

sub sound {"jiji"}
sub speak {
    my $class = shift;
    Animal::speak($class);
    print "jiji\n";
}
1;

上面通过完全限定的包名进行speak()的调用,注意传递了一个参数$class给speak(),因为父类Animal中的speak()要求一个参数。

但是问题又再次出现,假如Animal继承自Dot::Animal,而Animal自身没有speak(),上面的写法就会出错。稍微好一点的写法是使用面向对象的调用方式:
sub speak {
    my $class = shift;
    $class->Animal::speak();
    print "jiji\n";
}

虽然看上去很丑,但确实是可以正常工作的,它明确指定从Animal类中搜索。但Animal::是被硬编码到代码中的,像硬编码的行为能避免则避免。

访问父类方法(SUPER)

将上面的代码再改一改:
sub speak {
    my $class = shift;
    $class->SUPER::speak();
    print "jiji\n";
}

这样就解决了硬编码的问题。SUPER::表示从@ISA父类列表中搜索。


Perl面向对象(2)之对象

已有的代码结构

现在有父类Animal,子类Horse,它们的代码分别如下:
lib/Animal.pm中:
#!/usr/bin/env perl

use strict;
use warnings;

package Animal;

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

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

1;

lib/Horse.pm中:
#!/usr/bin/env perl

use strict;
use warnings;

package Horse;
use parent qw(Animal);

sub sound { "neigh" }

1;

一个perl程序speak.pl文件:
#!/usr/bin/env perl

use strict;
use warnings;
use lib "lib";
use Horse;

Horse->speak();

执行上面的speak.pl,将输出:
a Horse goes neigh

上面使用Horse->speak()的方式调用speak()方法,它首先调用到父类Animal中的speak(因为Horse类中没有重写该方法),然后Animal中的speak又重新回调Horse类中的sound()。

这个speak是所有Horse都共享的,如果想要定义每个Horse对象都私有的数据呢?比如为每个Horse对象命名。这里Horse的名字就是Horse类的实例数据(在其它编程语言中常称之为成员变量),是每个对象独有的。

bless创建实例数据:对象

在Perl的面向对象编程中,一个对象表示的就是一个对内置类型的引用,比如标量引用、数组引用、hash引用。也就是说,所谓对象就是一个指向内置数据结构的引用,这个数据结构可以认为是每个对象私有的成员变量。

强烈建议使用hash引用的方式,不过此处先以标量引用的方式开始本文的介绍。

修改speak.pl文件:
#!/usr/bin/env perl

use strict;
use warnings;
use lib "lib";
use Horse;

my $name = "baima";
my $bm_horse = \$name;
bless $bm_horse,'Horse';

bless的语法为:
bless REFERENCE,CLASS;

它表示为CLASS类设置一个唯一标识符,并返回这个唯一标识符,这个唯一标识符是一个数据结构的引用,这个唯一标识符也被称为对象。也就是说,对象就是一个引用,所以我们常常会使用my $obj = bless REF,CLASS;来返回一个对象。另一方面,bless表示将一个数据结构和类进行关联,表示这个数据结构(也许是空的,也许是经过一定初始化的)已经附加在类上,当创建这个类的实例(对象)时,它将返回一个引用,对这个数据结构的引用,换句话说,这个对象就已经拥有了这个数据结构。

如下图所示:


上面的Class是类,引用变量$obj_ref是这个类的唯一标识符,它指向一个数据结构,这个数据结构是这个类的属性。使用Horse进行具体化,Horse是类,$bm_horse是这个类的唯一标识符,这个引用指向值为"baima"的标量数据结构,所以$bm_horse是一个对象,"baima"就是它独有的属性。

也就是说,bless $bm_horse,'Horse';已经创建了一个名为$bm_horse的对象,而"baimai"这个属性是只属于这一个对象的数据。

调用实例方法

bless生成一个引用后,这个引用是类的实例,可以通过这个引用变量去调用类的方法。

#!/usr/bin/env perl
use strict;
use warnings;
use lib "lib";
use Horse;

my $name = "baima";
my $bm_horse = \$name;
bless $bm_horse,'Horse';

print $bm_horse->sound(),"\n";

上面通过对象去调用类方法,它首先搜索出sound()在何处(即类中还是父类中),然后将参数传递给sound()。传递的参数列表中,第一个参数是实例的名称,也就是$bm_horse,就像通过类名去调用类方法时,传递的第一个参数是类名一样。所以,下面两个是等价的:
$bm_horse->sound();
Horse::sound($bm_horse);

实际上,bless最初的目的就是通过一个引用来关联正确的类,以便perl能正确地找到所调用的方法,免去通过硬编码类名的麻烦。

再调用speak()试试:
$bm_horse->speak();

它将输出:
a Horse=SCALAR(0xc78610) goes neigh!

这是因为$bm_horse是一个指向标量数据结构的引用,speak()方法中将其赋值给$class,$class也仍然是引用,而且speak()中并没有去解除这个引用,所以如此输出。至于解决方法,留待后文。

访问实例数据

因为实例的名称是每个对象的唯一标识符,而现在可以通过传递给方法的第一个参数获取实例的名称,借此名称,可以进一步地获取到该实例的其它数据。

现在,在lib/Horse.pm文件中添加一个name方法:
sub name {
    my $self = shift;
    $$self;
}

注意上面$$self,因为$self是对象名,而对象名总是一个引用变量,因此将其解除引用。

然后在speak.pl中调用这个方法:
#!/usr/bin/env perl

use strict;
use warnings;
use lib "lib";
use Horse;

my $name = "baima";
my $bm_horse = \$name;
bless $bm_horse,'Horse';

print $bm_horse->name()," says ",$bm_horse->sound(),"\n";

该print将输出:

baima says neigh

perl中几乎都使用$self作为类名或对象名的代名词,就像java中的"this"一样。实际上,你可以使用任何变量名称,但约定俗成地,大家都喜欢用self。

通过构造器构造对象

前面构造Horse对象是在独立的speak.pl文件中实现的,这样生成Horse对象的方式是手动的,是完全私有的,构造对象时的实例数据(即name属性)也是完全暴露的。当在多个文件中都这样构造Horse对象,迟早会出错。

于是,在类文件lib/Horse.pm中定义一个构造方法new(),每次要构造Horse对象的时候只需调用这个方法即可。

#!/usr/bin/env perl

use strict;
use warnings;

package Horse;
use parent qw(Animal);

sub new {
    my $class = shift;
    my $name = shift;
    belss \$name,$class;
}

sub name {
    my $self = shift;
    $$self;
}

sub sound { "neigh" }

1;

上面在Horse.pm中定义了一个new()方法,该方法里面包含了bless语句,且作为new()方法的最后一个语句,表示构造一个对象并返回这个对象的唯一标识符:引用变量表示的对象名。因此,这个new()方法被称之为构造方法:用于构造该类的实例。

方法名new()可以随意,例如hire(),named()等都可以,但面向对象编程语言中,基本上都使用new这个词语来表示创建新对象,所以,也建议采用约定俗成的new(),如果使用其它方法名作为构造方法,请做好注释。

现在,只要调用Horse中的这个new()方法,就表示在当前包中构建一个Horse的实例(bless的返回值):
my $bm_horse = Horse->new("baima");

注意,bless返回的是对象引用,所以赋值给变量$bm_horse,这时$bm_horse将代表这个对象,是这个对象的唯一标识符。

上面调用new()的过程中,首先找到类方法new(),然后传递参数列表('Horse',"baima"),new()方法中,bless将baima这个数据结构附加到Horse类中,并返回指向该数据结构的引用。以后,通过$bm_horse就能找到这个数据结构,因为这个数据结构是对象$bm_horse的实例数据。

继承构造方法

在上面lib/Horse.pm中的构造方法new()中是否有Horse所特有的个性内容?完全没有。无论是Horse、Cow还是Sheep的构造方法都是通用的,所以将共性的代码抽取到父类Animal中。

lib/Animal.pm中:
#!/usr/bin/env perl

use strict;
use warnings;

package Animal;

sub new {
    my $class = shift;
    my $name = shift;
    bless \$name,$class;
}

sub name {
    my $self = shift;
    $$self;
}

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

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

1;

lib/Horse.pm中:
#!/usr/bin/env perl

use strict;
use warnings;

package Horse;
use parent qw(Animal);

sub sound { "neigh" }

1;

如此一来,无论是Horse、Cow还是Sheep都继承父类Animal中的构造方法new()以及name()。注意,上面name()方法也抽取到了Animal类中,因为它也是共性的,不过本小节暂时用不到该方法,下一小节会修改该方法。

现在,在speak.pl文件中构建一个Horse对象:
my $bm_horse = Horse->new("baima");

然后通过这个对象调用speak方法:
$bm_horse->speak();

它将输出:
a Horse=SCALAR(0xc78610) goes neigh!

这个实验前文已经验证过了。之所以会如此,是因为传递给speak()的第一个参数是对象的引用变量,而speak()中并没有去解除这个引用。再次看看speak()的代码:
sub speak {
    my $class = shift;
    print "a $class goes ",$class->sound(),"!\n";
}

speak()中的$class期待的其实是一个类名,而不是对象名,因为类名是具体的字符串,而非引用变量。例如,使用Horse->speak()就不会出现上面的问题。

如何解决父类中的这种问题,使其能同时处理类名和对象名?

让方法能同时处理类和对象

为了让父类中的方法能同时处理类名和对象名,可以加入一个额外的方法对类名和对象名进行判断。如何判断是类名还是对象名?只需使用ref即可,如果ref能返回一个值,表示这是一个引用,说明这是对象,ref返回false,则说明这不是引用,也就是类名。

之前因为name()方法因为共性的原因被抽取到Animal.pm后并没有使用过,这里派上用场了。

lib/Animal.pm中:
#!/usr/bin/env perl

use strict;
use warnings;

package Animal;

sub new {
    my $class = shift;
    my $name = shift;
    bless \$name,$class;
}

sub name {
    my $self = shift;
    ref $self ? $$self : "an unamed Class $self";  # 修改此行
}

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

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

1;

这样speak()就变得共性化,既能处理类名,也能处理对象名。

my $bm_horse = Horse->new("baima");
$bm_horse->speak(); # 传递对象名
Horse->speak(); # 传递类名

将输出如下结果:
baima goes neigh!
an unamed Class Horse goes neigh!

之所以加入新的方法,是因为在speak()中类名和对象名是相互独立的,也就是无法共性的,要么是类名,要么是对象名。为了让一段代码共性化,解决方法就是添加额外的代码将非共性内容化解掉,这些额外的代码可以直接加在speak()内部,也可以放进一个新定义的方法中,然后在speak()中调用这个方法。这是一种编程思想。

使用hash数据结构:添加额外的成员变量

经常地,perl使用hash作为对象的数据结果,这个数据结构中可以存储不同的数据、引用,甚至是对象,其中hash的key常作为实例数据(成员变量)。

再次说明,perl面向对象时最常用的对象数据结构是hash,但标量、数组也一样可以,至少很少用。

想要使用hash数据结构,只需将一个hash结构bless到类上即可。它表示这个hash数据结构附加在类上,bless返回一个引用,这个引用就是对象,所以这个对象指向这个数据结构,从而对象拥有这个数据结构。

例如,绑定一个空的hash结构:
bless {},$class;

上面的bless将一个匿名hash附加到类中。

对于父类Animal来说,由于已经有了name的属性,现在如果想要加上一个color属性,就可以将这两个成员属性放进一个hash结构中:
lib/Animal.pm中:
#!/usr/bin/env perl

use strict;
use warnings;

package Animal;

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

sub name {
    my $self = shift;
    ref $self
        ? $self->{Name}      # 此处需要修改,因为$self不再是标量引用变量,而是hash引用变量
        : "an unamed Class $self";
}

sub default_color {
    die "You have to override default_color method in subclasses";
}

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

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

1;

上面将一个包含key:Name和Color的hash数据结构bless到类上,其中Name成员变量通过构造对象时传递参数赋值,Color则调用各类自己的默认颜色方法default_color(),各个子类必须重写该方法。这是显然的,我们可以为某一子类动物设置默认毛色,但不能为所有动物设置同一种默认毛色。

然后修改lib/Horse.pm和lib/Sheep.pm,重写default_color():
lib/Horse.pm中:
#!/usr/bin/env perl

use strict;
use warnings;

package Horse;
use parent qw(Animal);

sub sound { 'neigh' }

sub default_color {
    'black'
}

1;

lib/Sheep.pm中:
#!/usr/bin/env perl

use strict;
use warnings;
package Sheep;
use parent qw(Animal);

sub sound { 'baaaah' }

sub default_color {
    'white'
}

1;

然后,speak.pl中构造Horse对象和Sheep对象,并访问自己的成员属性:
my $bm_horse = Horse->new("baima");
my $by_sheep = Sheep->new("xiaoyang");

print $bm_horse->{Name},"\n";
print $bm_horse->{Color},"\n";
print $by_sheep->{Name},"\n";
print $by_sheep->{Color},"\n";

结果:
baima
black
xiaoyang
white

子类重写构造方法

从父类中继承构造方法时,创建的对象的数据结构是完全一致的。如果某个子类想要多添加一些固定的数据元素,可以让子类重写父类的构造方法。

但需要注意的是,重写方法时,一般都强烈建议只对父类方法进行扩展,而不应该否定父类方法,完全修改父类方法(抽象方法除外)。

例如,现在父类Animal中的构造方法如下:
sub new {
    my $class = shift;
    my $name = shift;
    my $self = {
        Name  => $name,
        Color => $class->default_color(),
    };
    bless $self,$class;
}

想要为子类Horse添加一种固定的属性,马的类型是战马、比赛用的马还是普通的马。于是,在Horse类中:
package Horse;
use parent qw(Animal);

sub new {
    my $self = shift->SUPER::new(@_);
    $self->{Type} = "Racehorse";
    $self;
}

注意,上面Horse中的构造方法new()中并没有给bless语句。当调用Horse->new()构建对象的时候,首先调用父类的new(),父类的new会关联一个hash结构并返回这个hash结构,这个hash结构又赋值给$self,为此hash结构添加一种元素后,子类的new()返回$self,使得这个hash结构成为子类对象的数据结构。

为了后面的实验,本节所修改的Horse内容请删除。

设置和获取实例数据:setter & getter

上面设置Color的时候只能通过方法default_color()设置默认的毛色,但马有黑马、棕色马、条纹马等等,所以需要能手动设置各种颜色。此外,还要更及时获取到当前最新的成员变量值,比如获取某Horse对象的名称和颜色。这就是俗称的setter和getter方法的作用。

在此示例中,Name属性是直接通过构造方法传值设置的,在逻辑上它唯一标识这个对象(对我们而言,对perl而言是通过对象引用来唯一识别的),所以Name属性不应该允许重新设置。再者,因为设置和获取各对象的属性的代码是共性的,所以直接将这两类方法写到父类Animal中。

lib/Animal.pm中新加的代码片段:
sub set_color {
    my $self = shift;
    $self->{Color} = shift;
}

sub get_color {
    my $self = shift;
    $self->{Color};
}

sub get_name {
    shift->{Name};
}

现在可以为每个Horse或Sheep对象都设置对象自己的颜色,并且能获取颜色和名称:
my $bm_horse = Horse->new("baima");
my $by_sheep = Sheep->new("xiaoyang");

$bm_horse->set_color("white-and-black");
print $bm_horse->get_color(),"\n";
print $by_sheep->get_name(),"\n";

结果如下:
white-and-black
xiaoyang

注意上面get_name()中的一种简写方式:shift->{NAME},shift没有给参数,所以它的操作对象是@_,它等价于(shift @_)->{Name},也等价于:
my $self = shift;
$self->{Name};

关于setter返回值的问题

在为setter方法进行编码的时候,需要考虑它的返回值,一般来说有以下4种返回值类型:
(1).set成功后的值
(2).set之前的值
(3).返回对象自身
(4).返回成功/失败布尔值

这4种返回值各有优缺点,但无论如何都请注释好返回值的类型,并且设计好之后就别再修改。

第(1)种是最通用、最常见也最简单的行为,传递什么参数给setter,就返回什么参数值,正如set_color()一样:
sub set_color {
    my $self = shift;
    $self->{Color} = shift;
}

一般来说,这种setter方法是放在空上下文(void context)中执行的,但在perl中也可以直接输出它:print set_color("COLOR")。

第(2)种要返回设置之前的值,也很简单,只需使用一个临时变量存储一下原始值并返回该变量即可:
sub set_color {
    my $self = shift;
    my $temp = $self->{Color};
    $self->{Color} = shift;
    $temp;
}

这里有一点点小优化。因为是set,所以它可能是在空上下文中执行的,也就是说这时返回之前的值是多余的。可以通过wantarray来判断一下,wantarray函数用于检查执行上下文,如果在列表上下文中则返回true,标量上下文中则返回false,空上下文中则返回undef。

sub set_color {
    my $self = shift;
    if(defined wantarray){
        # 非空上下文,返回值有用
        my $temp = $self->{Color};
        $self->{Color} = shift;
        $temp;
    } else {
        # 空上下文,无需返回值
        $self->{Color} = shift;
    }
}

第(3)种返回对象自身:
sub set_color {
    my $self = shift;
    $self->{Color} = shift;
    $self;
}

一般来说不会用到这种情况。但有时候有奇效,例如可以形成对象链。例如,Person有4个成员变量:Name,Age,Height,Weight,它们的setter方法都返回对象自身,那么可以:
my $people = Person->set_name("abc")->set_age(23)->set_height(168)->set_weigth(60);

# 格式化一下:
my $people =
    Person->set_name("abc")
          ->set_age(23)
          ->set_height(168)
          ->set_weigth(60);

第(4)种返回布尔值有时候非常有效,特别是对于经常更新出错的情况。如果是前3种返回值方式,会抛出异常,需要判断并使用die进行终止。

别暴露实例数据

在面向对象编程中,常使用一个术语don't look inside box来表示不要暴露对象的成员数据。

通过$obj_ref->{KEY}的方式可以在类的外部访问或设置类的数据结构(成员变量),这是违反对象封装原则的,它将每个对象的内部属性都暴露出来了。对象就像是一个黑盒子,$obj_ref->{KEY}就像是将锁链撬开一样。

面向对象的目的之一是让Animal或Horse的维护者可以对它们的方法能独立地做出合理的修改,并且修改后那些已经导出的接口仍然能够正常工作。为什么直接访问hash结构违反了这个原则?当Animal的Color属性不再使用颜色的名称作为它的值时,而是使用RGB三原色的方式来存储颜色呢?

在此示例中,以一个虚构的模块Color::Conversions来修改颜色数据的格式,该模块有两个函数rgb_to_name()和name_to_rgb(),用于转换RGB和颜色的字符串名称,其中name_to_rgb()返回的是一个包含RGB三原色的数组引用。

可以修改set_color()和get_color()方法:
use Color::Conversions qw(rgb_to_name name_to_rgb);

sub set_color {
    my $self = shift;
    my $color_name = shift;
    $self->{Color} = name_to_rgb($color_name);
}

sub get_color {
    my $self = shift;
    rgb_to_name($self->{Color});
}

现在我们可以照旧使用setter和getter,但内部其实已经改变了,这些改变对使用者来说是透明的。此外,我们还可以添加额外的接口,使得我们可以直接设置RGB格式的颜色:
sub set_color_rgb {
    my $self = shift;
    $self->{Color} = [@_];
}

sub get_color_rgb {
    my $self = shift;
    @{ $self->{Color} };
}

如果我们在类的外面直接使用$bm_horse->{Color},将无法直接查看,因为它是一个RGB三原色元素列表的引用,而非直接显示出来的RGB元素值或颜色名称。

这正是面向对象编程所鼓励的行为,对于perl而言,只需将成员变量对应的值设置为一个引用即可。以关联hash数据结构的Animal类为例:
Animal
|--------------------------|
|->  KEY1 => $ref_value1   |
|->  KEY2 => $ref_value2   |
|->  KEY2 => $ref_value2   |
|--------------------------|

为了让数据通过引用的方式隐藏起来,且能通过getter方法查找出来,需要合理设计setter和getter方法。例如,让setter以普通的字符串为参数,但却将其存储到一个引用中,让getter以引用为参数,但却返回人眼可识别的内容。

简化setter和getter的书写

对于面向对象来说,这两个方法写的实在太频繁了。perl一切从简的原则,自然也要将其简化书写:
sub get_color { $_[0]->{Color} }
sub set_color { $_[0]->{Color} = $_[1] }

或者:
sub get_color { shift->{Color} }
sub set_color { pop->{Color} = pop }

合并getter和setter

如果不考虑默认传递的类名或对象名参数,getter方法通常是不含参数的,setter方法通常是包含参数的。通过这个特性,可以将getter和setter合并起来:
sub get_set_color {
    my $self = shift;
    if(@_) {
        # 有参数,说明是setter
        $self->{Color} = shift;
    } else {
        # 没有参数,说明是getter
        $self->{Color};
    }
}

或者简写的:
sub get_set_color { @_ > 1 ? pop->{Color} = pop : shift->{Color} }

使用时:
# 设置颜色
$bm_horse->get_set_color("blue");

# 获取颜色
print $bm_hore->get_set_color(),"\n";

限制方法类别:类方法和实例方法

在perl中所有的方法都是子程序,没有额外的功能来区分一个方法是类方法还是实例方法。对我们来说,如果传递的第一个参数为类名的是类方法,如果传递的第一个参数为对象引用名的方法是实例方法。

好在perl提供了ref函数,可以通过检查引用的方式来检查这是类方法还是实例方法。

use Carp qw(croak);

sub instance_method {
    ref(my $self = shift) or croak "this is a instance method";
    ...CODE...
}

sub class_method {
    ref(my $class = shift) and croak "this is a Class method";
    ...CODE...
}

这里使用croak替换了die,这样报错的时候可以直接告诉错误所在的行数。

私有方法(private method)

私有方法是指类中不应该被外界访问的方法,它可以在类自身其它地方调用,但不应该被对象或其它外界访问以免破坏数据。

例如,类Class1中的方法get_total()内部调用一个私有方法_get_nums(),通过该私有方法返回的数组来获取一个包含数值的数组。如果有一个子类Class2继承了Class1,且重写了_get_nums()使其返回一个数组引用而非数组,这时对象要调用的get_total()整段代码就废了。

package Class1;

sub new {
    my ($class,$args) = @_;
    return bless $args,$class;
}

sub get_total {
    my $self =  shift;
    my @nums = $self->_pri_sub;   # 期待该私有方法返回一个列表
    my $total = 0;
    foreach (@nums) {
        $total += $_;
    }
    return $total;
}

sub _pri_sub {
    my $self = shift;
    ...some codes...
    return @nums;      # 返回一个数组
}

1;

一般来说,这样的问题并不常发生,因为程序毕竟是程序员写的,遵循规范的情况下,大家都知道这是什么意思。但如果程序比较庞大,也许无意中就重写了一个私有方法。面向对象,一个最基本的规则就是保护数据不被泄漏、不被破坏。

但perl中所有的方法都是public(公共的),谁都能访问,并没有提供让方法私有化的功能。只是以一种呼吁式的规范,让大家约定俗成地使用下划线"_"作为方法名的前缀来表示这是一个私有方法(例如sub _name {})。但这只是一种无声的声明"这是私有方法,外界请别访问",perl并不限制我们从外界去访问下划线开头的方法。

要实现方法的真正私有化,可以将匿名子程序赋值给一个变量来实现,或者通过闭包的方式实现。

sub get_total {
    my $self =  shift;
    my @nums = $self->$_pri_sub(ARGS);
    my $total = 0;
    foreach (@nums) {
        $total += $_;
    }
    return $total;
}

my $_pri_sub = sub {
    my $self = shift;
    ...some codes...
    return @nums;
}

需要注意的是,上面$self->$_pri_sub()中箭头的右边是一个变量,在其它面向对象语言中是无法将变量作为方法名的,但perl支持。

因为$_pri_sub是词法变量,构造的对象无法取得这样的数据。但通过一些高级技术,对象还是能够取得这个方法,要想完全私有化,通过闭包实现:
sub get_total {
    my $self =  shift;
    my $_pri_sub = sub {
        my $self = shift;
        ...some codes...
        return @nums;
    }
    my @nums = $self->$_pri_sub(ARGS);
    my $total = 0;
    foreach (@nums) {
        $total += $_;
    }
    return $total;
}

祖先类:UNIVERSAL

UNIVERSAL是一切类的祖先,所有的类都继承于它。它提供了3个方法:isa()、cat()和VERSION(),在v5.10.1和之后,还提供了另一个方法DOES()。

1.isa()用于判断某个给定的对象(或类)与某个类是否满足is a的关系,也就是"对象是否是某个类的实例,类1是否是类2的子类"。

$object_or_class->isa(CLASS);

2.can()用于判断某个给定的对象(或类)是否能够使用某方法。

$object_or_class->can($method_name);

需注意,can()方法检测结果为真的时候,其返回值是该方法的引用。也就是说,可以直接将can()赋值给一个方法的引用变量,避免多次书写。以下是等价的写法:
if(my $method = $obj->can($method_name)){
    $obj->$method;
}

if($obj->can($method_name)){
    $obj->$methdo_name;
}

3.VERSION()用于返回对象(或类)的版本号。

设置了our $VERSION=...;之后,既可以通过继承自UNIVERSAL的VERSION()方法获取版本号,也可以通过对象获取VERSION变量值:

1
2
$obj->VERSION();
$obj->VERSION;

多重继承

Perl支持多重继承。假设Class3多重继承Class1和Class2:
package Class3;

# 1.
use base qw(Class1 Class2);
# 2.
use parent qw(Class1 Class2);
# 3.
use Class1;
use Class2;
our @ISA = qw(Class1 Class2);

但无论是哪种语言,都强烈建议不要使用多重继承。

假设Class1和Class2都直接继承自UNIVERSAL类,现在Class3多重继承Class1和Class2。

UNIVERSAL
    |    |
Class1  Class2
    |    |
Class3

假设Class2有方法eat(),Class1没有,且Class3没有写eat(),那么Class3的实例调用eat()方法的时候,会调用到Class2的eat()吗?

默认情况下,Perl搜索方法的规则是从左搜索,深度优先。意味着use base qw(Class1 Class2)时,当Class3自身找不到eat()时,将先搜索左边的Class1,搜索完没发现eat(),将搜索Class1的父类UNIVERSAL。也就是说永远也不会去搜索Class2。

实际上,搜索父类时是搜索@ISA中的元素,所以是从左开始搜索。

但是可以使用CPAN上的C3或者mro模块,它们实现从左搜索,广度优先的搜索规则。也就是说,对于use base qw(Class1 Class2),当Class3自身找不到eat()时,将先找左边的Class1的eat(),找不到再找右边的Class2的eat(),还找不到的话最后找父类的eat()。


Perl面向对象(3)之解构--对象销毁

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

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

关于DESTROY

当Perl中对象的最后一个引用要消失时,Perl将自动调用DESTROY方法。Perl处理DESTROY的方式和普通方法一样:
1.先从本类中搜索,搜索不到再搜索父类
2.传递的第一个参数为类名或对象名

和普通方法不同的是,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\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中:
#!/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好呢?

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

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