如果你对其他语言中的对象并不熟悉的话, 那么其他有关perl对象的文件可能使你感到恐惧, 比如 perlobj , 这是基础性的参考文件, 和 perltoot, 这是介绍perl对象的特性的教程.
所以, 让我们走另一条路,假定你没有任何关于对象的概念. 你需要了解子程序 (perlsub), 引用 (perlref et. seq.), 和 包(或模块) (perlmod), 如果还不清楚的话,先把他们搞清楚.
If we could talk to the animals...如果我们能和动物交谈
让我们让动物讲会儿话:
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 与上同
@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" 模式, 在稍大些的程序中应尽量避免. 那为什么要这样呢? 因为我们要调用的子程序和它所在的包似乎是不可分的.
真的是这样吗?
Introducing the method invocation arrow 调用方法时的箭头符号
现在,我们说 "Class->method" 是调用了包(或模块)"Class"中的 "method" 方法。(Here, "Class" is used in its "category" meaning, not its "scholastic" meaning.) 不是很准确,不过我们会一步一步的来做. 现在,可以这样做:
# 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" 预编译指令时也可以正常工作了.
Invoking a 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" 时就简单得多了
实际上这并不困难, 不过在这之前我们应该对箭头符号了解的更多一些.
The extra parameter of method invocation 方法调用时的额外参数
语句:
Class->method(@args)
这样调用函数 "Class::method":
Class::method("Class", @args); (如果子程序找不到,"继承,inheritance" 开始起作用,这在后面会讲到). 这意味着我们得到的第一个参数是类名(如果没有给出其他参数,它就是调用时的唯一参数).所以我们可以象这样重写 "Sheep" speaking 子程序:
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 都会得到与子程序相关的正确的值. 但是,还是有很多相似的结构. 可以再简单些吗? 是的. 可以通过在一个类中调用其它的方法来实现.
Calling a second method to simplify things 调用另一个方法以简化操作
我们在 "speak" 中调用 "sound". 这个方法提供声音的内容.
{ package Cow;
sub sound { "moooo" }
sub speak {
my $class = shift;
print "a $class goes ", $class->sound, "!\n"
}
} 现在, 当我们调用 "Cow->speak" 时, 我们在 "speak" 中得到 "Cow" 的类 $class. 他会选择 "Cow->sound" 方法, 然后返回 "moooo". 那如果是 "Horse" 呢?
{ package Horse;
sub sound { "neigh" }
sub speak {
my $class = shift;
print "a $class goes ", $class->sound, "!\n"
}
} 仅仅包名和声音有变化. 因此我们可以在Cow和Horse中共用 "speak" 吗? 是的,通过继承实现!
Inheriting the windpipes 继承气管
我们创建一个公共函数包,命名为 "Animal",在其中定义 "speak":
{ package Animal;
sub speak {
my $class = shift;
print "a $class goes ", $class->sound, "!\n"
}
} 然后,在每个动物那里 "继承,inherits" "Animal" 类, 同时赋予每个动物各自的声音:
{ package Cow;
@ISA = qw(Animal);
sub sound { "moooo" }
} 注意增加的数组 @ISA . 我们马上讲到它.
现在当我们调用 "Cow->speak" 时会发生什么?
首先, Perl构造参数列表. 在这种情况下, 只有 "Cow". 然后Perl 查找 "Cow::speak". 但是找不到, 所以Perl检查继承数组 @Cow::ISA. 找到了, 那里只有一个 "Animal"
Perl 然后在 "Animal" 中查找 "speak", "Animal::speak". 找到了, 然后调用该子程序, 参数在一开始就被固定了.
在子程序 "Animal::speak" 中, $class 是 "Cow" (第一个参数). 在我们调用 "$class->sound" 时, 首先寻找 "Cow->sound" , 找到了, 因此不用查看 @ISA. 成功!
关于@ISA应该注意的几点问题
神奇的 @ISA 变量 (读作 "is a" 而不是 "ice-uh"), 声明了 "Cow" 是一个("is a") "Animal"。 注意它是一个数组,而不是一个单值, 因为在个别情况下, 需要在几个父类中寻找方法.
如果 "Animal" 也有一个 @ISA, 我们也要查看它. 寻找是递归的,深度优先,在每个 @ISA 中从左到右寻找. 一般地,每个 @ISA 只有一个元素(多元素意味着多继承和多重的头痛), 这样我们可以得到一个漂亮的继承树.
如果使用 "use strict", @ISA会引起抱怨, 因为它不是含有显式包名的变量, 也不是字典变量 ("my"). 我们不能把它用做"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); 这就精简多了.
Overriding the methods 方法重载
让我们添上一只老鼠, 它的声音差不多听不到:
# 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 "[but you can barely hear it!]\n";
}
} Mouse->speak;
输出为:
a Mouse goes squeak!
[but you can barely hear it!] 在这里, "Mouse" 有它自己的speak 函数, 所以 "Mouse->speak" 不会调用"Animal->speak". 这叫做重载 "overriding". 实际上, 我们甚至不用说"Mouse" 是 "Animal", 因为 "speak" 所用到的所有方法在 "Mouse" 中都有定义.
但是有些代码与 "Animal->speak" 的相同 , 这在程序维护时是个问题. 我们能不能让 "Mouse" 与其它 "Animal" 作相同的事,但是给它加上特殊的部分呢? 可以!
首先,我们可以直接调用 "Animal::speak" 方法:
# Animal package from before
{ package 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" 那就会出问题. 因此, 这可能不是一个好方法.
Starting the search from a different place 从其它地方开始寻找
较好的解决办法是让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", 在找不到时寻找 "Animal" 的继承链.且第一个参数是 $class, 所以 "speak" 和"Mouse::sound" 都会被正确地调用.
但这还不是最好的方法.我们还必须调整 @ISA 的元素顺序. 更糟糕的是, 如果 "Mouse" 有多个父类在 @ISA, 我们还要知道哪个类定义了 "speak". 那么,有没有更好的办法呢?
The SUPER way of doing things 使用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
Where we're at so far...到现在为止我们学了些什么
我们已经看到了箭头符号语法:
Class->method(@args);
和它的等价形式:
$a = "Class";
$a->method(@args);
它们构造这样一个参数列表:
("Class", @args) 并调用
Class::method("Class", @Args); 但是,如果找不到 "Class::method", 程序会查看 @Class::ISA (递归的) 找到一个包含 "method" 的包,然后执行它.
使用这种简单的语法, 我们可以有类方法,(多)继承,重载,以及其它扩展. 使用我们已经学到的东西, 我们可以析出公共的代码,以各种不同的形式重用同一工具. 这是对象能够提供的核心内容, 但是对象还能够提供实例数据, 这一点我们还没有涉及.
A horse is a horse, of course of course -- or is it? 马就是马——真的是这样吗?
我们从 "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!
但是我们所有的马都是相同的. 如果我增加一个子程序, 所有的马都会共享它. 这在创建相同的马时确实不错, 但是我们如何能够区分不同的马呢? 比如, 假设我想给我的第一匹马起个名字. 应该有办法使得它的名字和别的马的名字不同.
这可以通过创建一个 "实例,instance" 来实现. 实例是由类创建的. 在Perl中, 任何引用都可以是实例, 就让我们从最简单的引用开始吧,一个标量引用:
my $name = "Mr. Ed";
my $talking = \$name;
现在 $talking 是指向实例特有数据( $name )的引用。把这个引用变成真正的实例的是一个特殊的操作符,叫做 "bless":
bless $talking, Horse;
这个操作符把包名 "Horse" 中的所有信息存放到引用所指向的东西中. 这时,我们说 $talking 是 "Horse" 的一个实例 . 也就是说, 它是一匹独特的马. 引用并没有改变, 还可以用于间接引用操作符.
Invoking an instance method 调用实例方法
箭头符号可以用于实例. 那么, 听听 $talking 的声音吧:
my $noise = $talking->sound;
要调用 "sound", Perl 首先注意到 $talking 是一个 blessed 引用 (因此是一个实例). 它会构造一个参数列表, 现在只有 $talking. (在后面我们会看到参数们在实例变量之后, 与使用类时相似.)
然后,是真正有意思的部分: Perl 查找实例所属的类, 这里是 "Horse", 在其中寻找对应的方法. 这里, "Horse::sound" 直接可以找到(不用使用继承), 最后这样调用:
Horse::sound($talking)
注意这里的第一个参数还是实例本身, 而不像前面我们学到的是类名. 最后返回值是 "neigh", 它被赋值给 $noise 变量.
如果找不到 Horse::sound, 会在 @Horse::ISA 列表中查找. 类方法与实例方法的唯一区别是调用时的第一个参数是实例(一个blessed引用)还是一个类名(一个字符串).
Accessing the instance data 访问实例数据
因为我们得到的第一个参数是实例,我们可以访问实例特有的数据. 我们可以取得马的名字:
{ package Horse;
@ISA = qw(Animal);
sub sound { "neigh" }
sub name {
my $self = shift;
$$self;
}
} 现在,我们调用名字:
print $talking->name, " says ", $talking->sound, "\n";
在 "Horse::name" 中, @_ 数组仅含有 $talking, shift 将 $talking 赋给了 $self. (传统上我们在处理实例方法时总是把第一个元素赋给 $self, 所以你也应该这么做, 除非你有不这样做的充分理由.) 然后, $self 被标量化,成为 "Mr. Ed", 这就行了. 输出是:
Mr. Ed says neigh.
How to build a horse 如何创建一匹马
当然啦,如果我们手工创建所有的马, 我们会出很多错误. 不仅如此,我们还亵渎了面向对象编程的特性,因为在那种情况下马的"内脏"也可见了. 如果你是兽医的话,这倒正好, 可是如果你仅仅是个爱马者呢? 所以,我们让 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" 操作符不仅将 $name 实例化, 且将指向 $name 的引用作为返回值返回. 这样, 我们就创建了一匹马.
这里,我们调用了构造器 "named", 它的参数就是特定的 "Horse" 的名字. 你可以使用不同的构造器用不同的名字建立不同的对象(比如记录它的谱系或生日). 但是, 你会发现多数使用Perl的人更喜欢把构造器命名为 "new", 并使用不同的方法解释 "new" 的参数. 两种都挺好,只要你能创建对象就行. (你会自己创建一个,对吗?)
Inheriting the constructor 继承构造器
但是那个方法中有没有什么对于 "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(0xaca42ac) goes neigh!
为什么?因为 "Animal::speak" 希望它的第一个参数是类名, 而不是实例. 当实例被传入时,我们希望使用的是字符串而不是实例本身,显示的结果不是我们所希望的.
Making a method work with either classes or instances 使方法同时支持类和实例
我们需要做的是让方法检测它是被实例调用的还是被类调用的. 最直接的方法是使用 "ref" 操作符. 它在参数是实例时返回字符串,在参数是类名时返回 "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
} 在这儿, "?:" 操作符决定是选择间接引用(dereference)还是派生字符串. 现在我们可以同时使用类或实例了. 注意我修改了第一个参数为 $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" 本来就可以工作. 那么现在就一切完成了!
Adding parameters to a method 给方法加参数
让我们训练动物们吃饭:
{ 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 = shift;
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");
More interesting instances 更多有趣的实例
如果实例需要更多的数据该怎么办呢? 更多的项目产生更有趣的实例, 每个项目可以是一个引用或者甚至是一个对象. 最简单的方法是把它们存放到哈希中. 哈希中的关键词叫做'实例变量"(instance variables)或者"成员变量"(member variables),相应的值也就是变量的值。
但是我们怎么把马放到哈希中呢? 回忆到对象是被实例化(blessed)的引用. 我们可以简单地创建一个祝福了的哈希引用,同时相关的的内容也作些修改就可以了.
让我们创建一只有名字有颜色的绵羊:
my $bad = bless { Name => "Evil", Color => "black" }, Sheep; 那么 "$bad->{Name}" 是 "Evil", "$bad->{Color}" 是 "black". 但是我们想通过 "$bad->name" 存取绵羊的名字name, 这有点的问题,因为现在它期望一个标量引用. 别担心,因为修正它很简单:
## in Animal
sub name {
my $either = shift;
ref $either ?
$either->{Name} :
"an unnamed $either";
} "named" 当然还是创建标量的绵羊, 如下修正就好了:
## in Animal
sub named {
my $class = shift;
my $name = shift;
my $self = { Name => $name, Color => $class->default_color };
bless $self, $class;
} 默认颜色 "default_color" 是什么? 嗯, 如果 "named" 只有一个参数name, 我们还是希望有个颜色, 所以我们设定一个类初始化颜色. 对绵羊来说, 白色比较好:
## in Sheep
sub default_color { "white" } 为了避免为每个类定义颜色, 我们可以在 "Animal" 中定义一个 "缺省的缺省,backstop" 的颜色:
## in Animal
sub default_color { "brown" } 现在, 因为只有 "name" 和 "named" 与对象的 "结构,structure" 相关, 其余的部分可以保持不变, 所以 "speak" 工作正常.
A horse of a different color 一匹不同颜色的马
但是如果所有的马都是棕色的,也挺烦人的. 所以我们可以写个方法来改变马的颜色.
## 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
Summary 总结
现在我们讲了类方法,构造器,实例方法,实例数据,甚至还有存取器(accessor). 但是这些还仅仅是开始. 我们还没有讲到以两个函数 getters,setters 形式出现的存取器,析构器(destructor),间接对象(indirect object notation),子类(subclasses that add instance data),per-class data,重载(overloading),"isa" 和 "can" 测试,公共类("UNIVERSAL" class),等等. 这有待其它文档去讲解了. 无论如何,希望本文使你对对象有所了解.