- 论坛徽章:
- 0
|
Perlboot – Beginner’s Object-Oriented Tutorial
Author : Mars
Date : June-2008
描述
如果你不熟悉其他语言中的对象概念,一些其他的Perl 对象文档肯能会使你有些沮丧,例如perlobj, 它是使用对象的一个基础参考,以及用指南形式向读者介绍Perl对象系统特性的perltoot。
如果可以和动物交谈
让动物交谈片刻:
sub Cow::speak {
print “a Cow goes moooo!\n”;
}
sub Horse::speak {
print “a Horse goes neigh!\n”;
}
sub Sheep::speak {
print “a Sheep goes baaaah!\n”;
}
Cow::speak;
Horse::speak;
Sheep::speak;
结果是:
a Cow goes moooo!
a Horse goes neigh!
a Sheep goes baaaah!
这里没有特别之处。仅是通过完全包名调用的,来自不同包的简单子程序。那么,来创建一个完整的牧场。
# Cow::speak, Horse::speak, Sheep::speak as before
@pasture = qw(Cow Cow Horse Sheep Sheep);
Foreach $animal (@pasture) {
&{$animal.”::speak”};
}
结果是:
a Cow goes moooo!
a Cow goes moooo!
a Horse goes neigh!
a Sheep goes baaaah!
a Sheep goes baaaah!
哇,那个符号解引用相当的脏。我们将指望no strict subs模式,当然对于大程序不推荐这样。为什么那是必要的?因为包名似乎和包中我们想调用的子程序名是不能分开的。
是这样么?
引入方法调用箭头
目前,假设 Class->method调用Class包中的子程序method(这里,“class”用于表示“种类/范畴”,而不表示其在“学校的”意义)。那不完全正确,但我们一步一步做。现在假设如下使用:
# Cow::speak, Horse::speak, Sheep::speak as before
Cow->speak;
Horse->speak;
Sheep->speak;
再一次,结果是:
a Cow goes moooo!
a Horse goes neigh!
a Sheep goes baaaah!
那仍然并不有趣。同样数量的字符,都是常量,没有变量。但是,现在零件可以分离了。看:
$a = “Cow”;
$a->speak; # invokes Cow->speak;
啊!既然包名和子程序名分离了,我们就可以使用变量包名。此刻,即使当use strict refs启用,我们也能使一些东西工作。
调用barnyard(畜棚场,空场)
使用新箭头调用方法,且将其用回barnyard例子:
sub Cow::speak {
print “a Cow goes moooo!\n”;
}
sub Horse::speak {
print “a Horse goes neigh!\n”;
}
sub Sheep::speak {
print “a Sheep goes baaaah!\n”;
}
@pasture = qw(Cow Cow Horse Sheep Sheep);
foreach $animal (@pasture) {
$animal->speak;
}
看! 现在我们让所有动物说话,且不使用符号解引用。
但看看所有的公共代码。每个speak程序都还有相似结构:一个print运算符和一个包含公共文本信息的字符串,仅有两个单词不同。在我们决定把所有的goes改为says的那种情况,如果可以构建公共部分,将会更好。
实际上,在没有太大忙乱的情况下,我们有一种方式做到那样,但我们需要对调用箭头实际为我们做的事情有更多的了解。
方法调用的额外参数
下面的调用:
Class->method(@args)
试图以下列方式调用Class::method:
Class::method(“Class”, @args);
(如果找不到子程序,考虑“继承”,这个稍后接触到)这意味我们使类名作为首个参数(如果没有提供参数的话,则作为唯一参数)。因此,我们可以改写Sheep说话子程序,如下:
sub Sheep::speak {
my $class = shift;
print ‘a $class goes baaaah!\n”;
}
同样地,其他连个动物变为:
sub Cow::speak {
my $class = shift;
print “a $class goes moooo!\n”;
}
sub Horse::speak {
my $class = shift;
print “a $class goes neigh!\n”;
}
每种情况下,$class将获得适合那个子程序的值。但再一次地,我们有许多类似的结构。我们可以更进一步构建么? 是的,通过调用相同类中的另一方法。
调用另一个方法简化事情
假设speak方法调用一个帮助方法sound。此方法为声音本身提供常量文本信息。
{ package Cow;
sub sound { “moooo” }
sub speak {
my $class = shift;
print “a $class goes ”, $class->sound, “!\n”;
}
}
现在,当调用Cow->speak,在speak中,我们获得Cow作为$class。这依次选择返回moooo的Cow->sound方法。但这对于Horse来说有怎样的不同呢?
{ package Horse;
sub sound { “neigh” }
sub speak {
my $class = shift;
print “a $class goes ”, $class->sound, “!\n”
}
}
仅仅包名和具体的声音变化了。因此,我们能用某种方式在Cow和Horse间共享speak的定义么? 是的,通过继承机制。
继承windpipes(气管)
我们将定义一个包含speak定义的公共子程序包,即Animal。
{ package Animal;
sub speak {
my $class = shift;
print “a $class goes ”, $class->sound, “!\n”
}
}
接着,对于每种动物,我们说它“继承自”Animal,但带有具体动物的声音:
{ package Cow;
@ISA = qw(Animal);
sub sound { “moooo” }
}
注意加入的@ISA数组。我们即将接触到。
但当现在我们调用Cow->speak时,会发生什么事情呢?
首先,Perl构造参数列表。这种情况下,参数仅是Cow。接着,Perl寻找Cow::speak。但它不再那里,所以Perl查询继承数组@Cow::ISA。找到信息,@Cow::ISA包含一个名字Animal。
Perl接着查找Animal中的speak替代,以Animal::speak形式。找到,所以Perl调用子程序使用已固定的参数列表。
Animal::speak子程序中,$class的值是Cow(第一个参数)。那么,当我们到$class->sound那一步时,它将寻找Cow->sound,在第一次尝试时就找到了, 不需要查询@ISA。成功!
一些@ISA的注意事项
这个神奇的@ISA变量(发音是“is a”,不是“ice-uh”)声明了Cow是一个Animal。注意到它是一个数组,不是一个简单的单值,这是因为在少有的场合,在多个父类中搜索缺少的方法是有意义的。
如果Animal也包含@ISA,那么我们也会搜索那里。搜索在每个@ISA中是递归的,深度优先,从左到右。代表性地,每个@ISA仅有一个元素(多元素意味多继承和多重头疼),因此我们有一个良好的继承树结构。
当启用use strict是,我们会得到对@ISA的抱怨,由于它不是包含清楚包名的变量,也不是词法变量(“my”)。我们不能将其变为词法变量(它必须属于通过继承机制寻找到的包中),所以有一些直接了当的方法处理。
最容易的方法是写出包名:
@Cow::ISA = qw(Animal);
或者通过暗含指定的包变量:
package Cow;
use vars qw(@ISA);
@ISA = qw(Animal);
如果从外部引入类,通过一个面向对象的模块,则下列代码:
package Cow;
use Animal;
use vars qw(@ISA);
@ISA = qw(Animal);
改为:
package Cow;
use base qw(Animal);
这就比较紧凑了。
方法重写
假设加入老鼠,它的声音很难被听到:
# Animal package from before
{ package Mouse;
@ISA = qw(Animal);
sub sound { “squeak” }
sub speak {
my class = shift;
print “a $class goes ”, $class->sound, “!\n”;
print “[bt you can barely hear it!]”;
}
}
Mouse->speak;
结果是:
a Mouse goes squeak!
[but you can barely hear it!]
这里,Mouse有其自己的发声程序,因此Mouse->speak不是直接调用Animal->speak。这就是方法“重写”。实际上,我们并不需要讲Mouse是一个Animal,因为所有speak需要的方法都在Mouse中定义了。
但我们还是重复了Animal->speak中的一些的代码,这将再次困扰程序的维护。那么,我们可以避免这样么? 我们能说Mouse和Animal一样,但仅仅加入额外的注释么? 当然可以!
首先,可以调用直接调用方法Animal::speak:
# Animal package from before
{pacakge Mouse;
@ISA = qw(Animal);
sub sound { “squeak” }
sub speak {
my $class = shift;
Animal::speak($class);
print “[but you can barely hear it!]\n”;
}
}
既然我们停止使用方法箭头,请注意我们必须包含$class参数(几乎确定的值是“Mouse”)作为Animal::speak的第一个参数。为何不使用方法箭头? 如果在那里调用Animal->speak,方法的第一个参数将是“Animal”而不是“Mouse”,当它调用sound时,它将不是此包中的正确类。
然而,直接调用Animal::speak将是一种混乱行为。如果Animal::speak之前并不存在,且继承于@Animal::ISA提到的一个类,那会怎么样? 由于我们不再使用方法箭头,我们有且只有一个机会找到正确子程序。
也请注意,Animal类名被硬编码到选择程序中。对于某个维护代码的人来说,这是一种混乱,改变<Mouse>的@ISA并未注意到speak中的Animal。因此,这大概不是正确的方向。
从不同的地方启动搜索
一个较好的解决办法是告知Perl从继承链条的高一层搜索:
# same Animal as before
{package Mouse;
# same @ISA, &sound as before
sub speak {
my $class = shift;
$class->Animal::speak;
print “[but you can barely hear it!]\n”;
}
}
它工作了 。使用此句法,我们从Animal开始查找speak,如果没有直接找到speak,则使用Animal所有的继承链。第一个参数仍然是$class,那么查找到的speak方法将把Mouse当作其第一个输入,并且最终于Mouse::sound具体工作。
但这不是最佳方案。我们仍不得不保持对@ISA和初始搜索包的调整。更糟的是,如果Mouse的@ISA里有多个输入,我们不太可能了解哪个实际定义了speak。那么,有更好一些的方法么?
SUPER方式
通过将调用中的Animal类改为SUPER类,就可以自动搜索我们所有的超类(@ISA中的类):
# same Animal as before
{package Mouse;
# same @ISA, &sound as before
sub speak {
my $class = shift;
$class->SUPER::speak;
print “[but you can barely hear it!]\n”;
}
}
所以,SUPER::speak意味着在当前包的@ISA中搜索speak,调用首个找到的。注意,它不在$class的@ISA中搜索。
目前我们到哪里了…
目前为止,我们已经看到过方法箭头语法:
Class->method(@args);
或等价物:
$a = “Class”;
$a->method(@args);
它们构建的参数表是:
(“Class”, @args)
且尝试调用:
Class::method(“Class”, @Args);
然而,如果未找到Class::method,那么将检查(递归地)@Class::ISA以便定外确实包含method的包,接着调用那个子程序作为替代。
使用此简单语法,我们拥有类方法,(多重)继承,重写以及扩展。使用我们目前看到的信息,我们可以重构公共代码,且提供一个好的方法重用带有变化的实现。这是对象提供的核心内容,但对象仅提供实例数据,这是我们未曾涉及的。
一匹马是一匹马,当然当然 – 或者是这样么?
从Animal类和Horse类的代码开始:
{package Animal;
sub speak {
my $class = shift;
print “a $class goes ”, $class->sound, “!\n”
}
}
{package Horse;
@ISA = qw(Animal);
sub sound { “neigh” }
}
这允许我们调用Horse->speak向上岛Animal::speak,调用返回到Horse::sound以得到具体的声音,结果是:
a Horse goes neigh!
但我们所有的Horse对象将不得不完全一样。如果增加一个子程序,所有的horse将自动共享它。对于把所有的horse变得同样来说,这是顺利的,但是我们如何捕获个体马儿的区别呢? 例如,我想对第一匹马命名。一定有个方式保持它的名字和其他马儿的分离。
我们可以通过引入一个新的区别来实现,它叫做“实例”。一个“实例”通常是由一个类创建的。在Perl中,任何引用都可以成为一个实例。那么从最简单的包含马儿名字的引用开始:一个标量引用:
my $name = “Mr. Ed”;
my $talking = \$name;
现在$talking是一个即将成为实例相关数据(名字)的一个引用。将此转换成一个真实对象的最终步骤是通过一个特殊运算符,即bless:
bless $talking, Horse;
此运算符将指定Horse包的信息存入引用指向的东西。此时,我们说$talking是Horse的一个实例(对象)。即,它是一匹具体的马。引用没有改变,且仍可以应用在传统的解引用运算符中。
调用一个实例方法
方法箭头可以用在实例上,也可以用在包名(类名)上。那么,假设获得那个$talking产生的声音:
my $noise = $talking->sound;
为调用sound,Perl首先注意到$talking是一个被bless的引用(因此是一个实例)。接着构建参数列表,这种情况下,只是($talking)。(稍候,我们将看到参数的位置在实例变量之后,像在类中那样。)
现在是有趣的部分:Perl选中bless实例的类,这种情况下是Horse,且使用Horse定位子程序以调用方法。这种情况下,直接找到Horse::sound(没有使用继承),产生最终的子程序调用:
Horse::sound($talking)
注意,这里的第一个参数仍旧是实例,不像之前那样是类名。我们得到的返回值是neigh,那将作为上述$noise变量的最终值。
如果没有找到Horse::sound,我们将向上徘徊到@Horse::ISA列表寻找某个超类中的方法,就像一个类方法那样。类方法和实例方法之间唯一的不同是看第一个参数是否是实例(被bless的引用)或一个类名(一个字符串)。
访问实例数据
由于实例作为第一个参数,我们现在可以访问实例相关的数据。这种情况下,假设添加一个方法获得名字:
{ pakcage Horse;
@ISA = qw(Animal);
sub sound { “neigh” }
sub name {
my $self = shift;
$$self;
}
}
现在我们调用名字:
pring $talking->name, “ says ”, $talking->sound, “\n”;
在Horse::name中,@_数组仅包含$talking,shift操作将其存入$self。(通常为力实例方法将第一个参数shift到指定的变量$self里,请坚持这样直到你有更好的原因不用它。)接着,$self作为一个标量引用被解引用,产生Mr. Ed,我们的工作结束了。结果是:
Mr. Ed says neigh.
如何创建一匹马
当然,如果我们手动创建所有的马儿,有时我们很可能犯错误。我们也违反了面向对象编程的一个属性,因为一匹马的“inside guts(狼吞虎咽)”是可见的。如果你是兽医那还好,但如果你仅想拥有马,就不行了。所以,让Horse类创建新的一匹马:
{ package Horse;
@ISA = qw(Animal);
sub sound { “neigh” }
sub name {
my $self = shift;
$$self;
}
sub named {
my $class = shift;
my $name = shift;
bless \$name, $class;
}
}
现在有了新的named方法,我们可以创建一匹马:
my $talking = Horse->named(“Mr. Ed”);
注意我们回到一个类方法,所以两个传给Horse::named的参数是Horse和Mr. Ed。bless运算符不仅bless了$name,它也返回$name的引用,那作为返回值是不错的。那就是如何创建一匹马。
这里的named被称为构造函数,它快速地指示构造函数的参数作为这个特殊Horse的名字。你可以使用带有不同名字的不同的构造函数,通过不同的方法“产生”对象(像记录它的血统或出生日期)。然而,你会发现多数从更加有限的语言转到Perl的人使用单一的叫做new的构造方法,使用多种解释new参数的方法。任一风格都不错,只要你记录关于产生对象特殊方式的文档。(你过去已经那样做了,不是么?)
继承构造函数
那个方法中有什么关于Horse特殊的地方么? 没有,因此对于创建任何继承于Animal的东西,它都是相同的。所以将它写到那里:
{ package Animal;
sub speak {
my $class = shift;
print “a $class goes ”, $class->sound, “!\n”;
}
sub name {
my $self = shift;
$$self;
}
sub named {
my $class = shift;
my $name = shift;
bless \$name, $class;
}
}
{ package Horse;
@ISA = qw(Animal);
sub sound { “neigh” }
}
如果一个实例引用speak,会发生什么?
my $talking = Horse->named(“Mr. Ed”);
$talking->speak;
我们得到调试值:
a Horse=SCALAR(oxaca42as) goes neigh!
为什么? 因为Animal::speak程序期待一个类名作为其第一个参数,而不是一个实例。但传入实例时,最终将一个bless的引用转成一个字符串,就像我们看到的显示那样。
对于类和实例都起作用的方法
我们所需的就是由方法探测是类还是实例调用它。最直接的方法是通过ref运算符。当用在受bless的引用上时,这将返回一个字符串(类名),当用在一个字符串(像一个类名)时,返回undef。通过首先修改name方法通知改变:
sub name {
my $either = shift;
ref $either
? $$either # it’s an instance, return name
: “an unnamed $either”; # it’s a class, return generic
}
这次?:运算符对于选择解引用或一个来源字符串派得上用场。现在通过一个实例或一个类都可以使用这个方法。注意到已经将第一个参数改变为$either表示意图:
my $talking = Horse->named(“Mr. Ed”);
print Horse->name, “\n”; # prints “an unnamed Horse\n”
print $talking->name, “\n”; # prints “Mr. Ed\n”
现在使用这个特性修复speak:
sub speak {
my $either = shift;
print $either->name, “ goes ”, $either->sound,”\n”;
}
既然sound对于类或者实例都起作用,我们的任务完成了!
向方法中添加参数
训练我们的动物吃东西:
{ package Animal;
sub named {
my $class = shift;
my $name = shift;
bless \$name, $class;
}
sub name {
my $either = shift;
ref $either
? $$either # it’s an instance, return name
: “an unnamed $either”; # it’s a class, return generic
}
sub speak {
my $either = shift;
print $either->name, “ goes ”, $either->sound, “\n”;
}
sub eat {
my $either = shfit;
my $food = shift;
print $either->name, “ eats $food.\n”;
}
}
{ package Horse;
@ISA = qw(Animal);
sub sound { “neigh” }
}
{ package Sheep;
@ISA = qw(Animal);
sub sound { “baaaah” }
}
现在尝试:
my $talking = Horse->named(“Mr. Ed”);
$talking->eat(“hay”);
Sheep->eat(“grass”);
打印出:
Mr. Ed eats hay.
an unnamed Sheep eats grass.
带有参数的实例方法通过实例调用,接着是参数列表。所以,第一个调用如下:
Animal::eat($talking, “hay”);
更多有趣的事例
如果实例需要更多数据怎么办? 多数有趣实例由许多条目组成,每个条目可能是一个引用或甚至其他对象。存储这些的最容易方式是哈希表。哈希表的key作为对象部分的名字(常称为“实例变量”或“成员变量”),value对应相关的值。
但怎样将horse转变为哈希表? 记得对象是一个受bless的引用。只要一切考虑引用的东西相应地改变,我们可以像使用受bless的标量饮用那样使用受bless的哈希表。
让我们的sheep含有名字和颜色:
my $bad = bless { Name=>”Evil”, Color=>”black”}, Sheep;
那么,$bad->{Name}得到Evil,且$bad->{Color}得到black。当我们希望$bad->name访问名字,现在那样是混乱的引为它期待一个标量引用。别担心,很容易修复:
## in Animal
sub name {
my $either = shift;
ref $either ?
$either->{Name} :
“an unnameed $either”;
}
当然named仍创建一个标量sheep,将其修改为:
## in Animal
sub named {
my $class = shift;
my $name = shift;
my $self = { Name=>$name, Color=> $class->default_color };
bless $self, $class;
}
default_color是什么? 如果named仅有一名字,我们仍需设定一个颜色,于是我们有类级别的初始颜色。对于sheep,我们可能定义为白色:
## in Sheep
sub default_color { “white” }
避免为每个额外的类定义一个,我们在Animal中直接定义一个“支撑物”方法作为“default default”。
## in Animal
sub default_color { “brown” }
现在,因为name和named是引用对象“结构”的唯一方法,其他方法可以不变,所以speak仍像之前那样工作。
不同颜色的马
让所有的马儿都是褐色会使人觉得厌烦。所以添加一个或多个方法取得和设置颜色。
## in Animal
sub color {
$_[0]->{Color}
}
sub set_color {
$_[0]->{Color} = $_[1];
}
注意另一种访问参数的方法:使用$_[0],而非shift。(对于一些经常调用的事情,这将节省我们一些时间。) 现在我们可以修整Mr. Ed的颜色:
my $talking = Horse->named(“Mr. Ed”);
$talking->set_color(“black-and-white”);
print $talking->name, “ is colored ”, $talking->color, “\n”;
结果是:
Mr. Ed is colored black-and-white |
|