DOKK / manpages / debian 10 / manpages-zh / perlboot.7.zh_CN
PERLBOOT(7) Perl Programmers Reference Guide PERLBOOT(7)

perlboot - 初学者的面向对象教程

如果你对其他语言中的对象并不熟悉的话, 那么其他有关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),等等. 这有待其它文档去讲解了. 无论如何,希望本文使你对对象有所了解.

更多信息可参见 perlobj (这里有更多的Perl对象的细节,而本文的是基础), perltoot (面向对象的中级教程), perlbot (更多的技巧), 以及书籍,比如Damian Conway的不错的书叫做《面向对象的Perl (Object Oriented Perl)》。

某些模块可能对你有用,它们是 Class::Accessor, Class::Class, Class::Contract, Class::Data::Inheritable, Class::MethodMaker 还有 Tie::SecureHash

Copyright (c) 1999, 2000 by Randal L. Schwartz and Stonehenge Consulting Services, Inc. Permission is hereby granted to distribute this document intact with the Perl distribution, and in accordance with the licenses of the Perl distribution; derived documents must include this copyright notice intact.

Portions of this text have been derived from Perl Training materials originally appearing in the Packages, References, Objects, and Modules course taught by instructors for Stonehenge Consulting Services, Inc. and used with permission.

Portions of this text have been derived from materials originally appearing in Linux Magazine and used with permission.

中文版维护人

redcandle <redcandle51@chinaren.com>

中文版最新更新

2001年12月9日星期日

中文手册页翻译计划

http://cmpp.linuxforum.net

本页面中文版由中文 man 手册页计划提供。
中文 man 手册页计划:https://github.com/man-pages-zh/manpages-zh

2003-11-25 perl v5.8.3