Scroll to navigation

PERLBOOT(7) Perl Programmers Reference Guide PERLBOOT(7)

NAME

perlboot - 初學者的面向物件教程

DESCRIPTION 描述

如果你對其他語言中的物件並不熟悉的話, 那麼其他有關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),等等. 這有待其它文件去講解了. 無論如何,希望本文使你對物件有所瞭解.

SEE ALSO 參見

更多資訊可參見 perlobj (這裡有更多的Perl物件的細節,而本文的是基礎), perltoot (面向物件的中級教程), perlbot (更多的技巧), 以及書籍,比如Damian Conway的不錯的書叫做《面向物件的Perl (Object Oriented Perl)》。

某些模組可能對你有用,它們是 Class::Accessor, Class::Class, Class::Contract, Class::Data::Inheritable, Class::MethodMaker 還有 Tie::SecureHash

COPYRIGHT

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