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