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.

跋
本頁面中文版由中文 man 手冊頁計劃提供。
中文 man 手冊頁計劃：https://github.com/man-pages-zh/manpages-zh