perlobj - Perl 物件參考
本文件提供 Perl 物件導向功能的參考。如果您正在尋找 Perl 物件導向程式設計的簡介,請參閱 perlootut。
為了瞭解 Perl 物件,您首先需要瞭解 Perl 中的參考。有關詳細資訊,請參閱 perlreftut。
本文件從頭開始描述 Perl 的所有物件導向 (OO) 功能。如果您只是想撰寫一些自己的物件導向程式碼,那麼您可能更適合使用 perlootut 中描述的 CPAN 物件系統之一。
如果您想撰寫自己的物件系統,或需要維護從頭實作物件的程式碼,這份文件將協助您了解 Perl 如何執行物件導向。
物件導向 Perl 有幾個基本原則
物件只是一個資料結構,它知道自己屬於哪個類別。
類別只是一個套件。類別提供預期對物件執行的方法。
方法只是一個子常式,它預期物件的參考 (或套件名稱,針對類別方法) 作為第一個引數。
讓我們深入探討每個原則。
與支援物件導向的許多其他語言不同,Perl 沒有提供任何特殊語法來建構物件。物件只是已與特定類別明確關聯的 Perl 資料結構 (雜湊、陣列、純量、檔案控制代碼等)。
這個明確關聯是由內建的 bless
函數建立的,它通常在類別的 建構函式 子常式中使用。
以下是簡單的建構函式
package File;
sub new {
my $class = shift;
return bless {}, $class;
}
new
名稱沒有特殊意義。我們可以將建構函式命名為其他名稱
package File;
sub load {
my $class = shift;
return bless {}, $class;
}
OO 模組的現代慣例是始終使用 new
作為建構函式的名稱,但沒有這麼做的要求。任何將資料結構祝福成類別的子常式都是 Perl 中有效的建構函式。
在前面的範例中,{}
程式碼會建立一個空匿名雜湊的參考。然後 bless
函數會取得該參考,並將雜湊與 $class
中的類別關聯。在最簡單的情況下,$class
變數最後會包含字串「File」。
我們也可以使用變數來儲存作為物件祝福的資料結構的參考
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
return $self;
}
一旦我們祝福 $self
參考的雜湊,我們就可以開始呼叫其方法。如果您想將物件初始化放在其自己的獨立方法中,這會很有用
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
$self->_initialize();
return $self;
}
由於物件也是雜湊,您可以將其視為一個雜湊,使用它來儲存與物件關聯的資料。通常,類別內的程式碼可以將雜湊視為可存取的資料結構,而類別外的程式碼應始終將物件視為不透明的。這稱為封裝。封裝表示物件的使用者不必知道它是如何實作的。使用者只需呼叫物件上已記錄的方法即可。
但請注意,與大多數其他 OO 語言不同,Perl 絕不會以任何方式確保或強制執行封裝。如果您希望物件實際上是不透明的,您需要自行安排。這可以用各種方式來完成,包括使用 "內外物件" 或 CPAN 中的模組。
當我們祝福某個東西時,我們不會祝福包含該東西參考的變數,也不會祝福變數儲存的參考;我們祝福的是變數參考的東西 (有時稱為參照物)。以下程式碼最能說明這一點
use Scalar::Util 'blessed';
my $foo = {};
my $bar = $foo;
bless $foo, 'Class';
print blessed( $bar ) // 'not blessed'; # prints "Class"
$bar = "some other value";
print blessed( $bar ) // 'not blessed'; # prints "not blessed"
當我們對變數呼叫 bless
時,我們實際上是在祝福變數所指的底層資料結構。我們並未祝福參考本身,或包含該參考的變數。這就是為什麼第二次呼叫 blessed( $bar )
會傳回 false 的原因。在那個時間點, $bar
已不再儲存對物件的參考。
有時你會看到較舊的書籍或文件提到「祝福參考」或將物件描述為「祝福參考」,但這是錯誤的。被祝福為物件的並非參考;而是參考所指的東西(即所指物)。
Perl 沒有提供任何類別定義的特殊語法。套件僅僅是一個包含變數和子程式的命名空間。唯一的差別在於,在類別中,子程式可能會預期物件的參考或類別名稱作為第一個引數。這純粹是約定俗成,因此類別可能包含方法和子程式,而這些方法和子程式不會對物件或類別進行操作。
每個套件都包含一個稱為 @ISA
的特殊陣列。@ISA
陣列包含該類別的父類別清單(如果有的話)。當 Perl 執行方法解析時,會檢查這個陣列,我們稍後會介紹這個主題。
從套件呼叫方法表示它必須被載入,當然,因此你通常會想要載入模組並同時將它加入 @ISA
。你可以使用 parent 實用指令碼以單一步驟執行此操作。(在較舊的程式碼中,你可能會遇到 base 實用指令碼,但現在不建議使用,除非你必須使用同樣不建議使用的 fields 實用指令碼。)
無論父類別如何設定,套件的 @ISA
變數都將包含這些父類別的清單。這只是一個純量清單,每個純量都是對應於套件名稱的字串。
所有類別都隱含地繼承自 UNIVERSAL 類別。UNIVERSAL 類別是由 Perl 核心實作的,並提供多種預設方法,例如 isa()
、can()
和 VERSION()
。UNIVERSAL
類別絕不會出現在套件的 @ISA
變數中。
Perl 僅提供方法繼承作為內建功能。屬性繼承則留給類別實作。有關詳細資訊,請參閱 "撰寫存取器" 部分。
Perl 沒有提供任何特殊語法來定義方法。方法只是一個常規子例程,並使用 sub
宣告。方法的特殊之處在於它預期會收到物件或類別名稱作為其第一個引數。
Perl 確實 提供特殊語法來呼叫方法,即 ->
算子。我們稍後會更詳細地介紹這一點。
您撰寫的大多數方法預期會對物件進行操作
sub save {
my $self = shift;
open my $fh, '>', $self->path() or die $!;
print {$fh} $self->data() or die $!;
close $fh or die $!;
}
在物件上呼叫方法的寫法為 $object->method
。
方法呼叫(或箭頭)算子的左手邊是物件(或類別名稱),右手邊是方法名稱。
my $pod = File->new( 'perlobj.pod', $data );
$pod->save();
在解除參考時也會使用 ->
語法。它看起來像同一個算子,但這兩個是不同的運算。
當您呼叫方法時,箭頭左手邊的東西會傳遞為方法的第一個引數。這表示當我們呼叫 Critter->new()
時,new()
方法會收到字串 "Critter"
作為其第一個引數。當我們呼叫 $fred->speak()
時,$fred
變數會傳遞為 speak()
的第一個引數。
就像任何 Perl 子例程一樣,傳遞至 @_
中的所有引數都是原始引數的別名。這包括物件本身。如果您直接指定給 $_[0]
,您會變更持有物件參考的變數的內容。我們建議您不要這麼做,除非您確切知道自己在做什麼。
Perl 會透過查看箭頭的左手邊來得知方法所在的套件。如果左手邊是套件名稱,它會在該套件中尋找方法。如果左手邊是物件,則 Perl 會在物件已祝福進入的套件中尋找方法。
如果左手邊既不是套件名稱也不是物件,則方法呼叫會導致錯誤,但請參閱 "方法呼叫變異" 部分以了解更細微的差別。
我們已經討論過特殊的 @ISA
陣列和 parent 實用程式。
當一個類別從另一個類別繼承時,在父類別中定義的任何方法都可供子類別使用。如果您嘗試在物件上呼叫未在其自己的類別中定義的方法,Perl 也會在它可能擁有的任何父類別中尋找該方法。
package File::MP3;
use parent 'File'; # sets @File::MP3::ISA = ('File');
my $mp3 = File::MP3->new( 'Andvari.mp3', $data );
$mp3->save();
由於我們沒有在 File::MP3
類別中定義 save()
方法,Perl 將會查看 File::MP3
類別的父類別來尋找 save()
方法。如果 Perl 在繼承階層中的任何地方都找不到 save()
方法,它將會終止。
在這種情況下,它在 File
類別中找到了 save()
方法。請注意,即使該方法是在 File
類別中找到的,傳遞給 save()
的物件仍然是 File::MP3
物件。
我們可以在子類別中覆寫父類別的方法。當我們這樣做時,我們仍然可以使用 SUPER
偽類別呼叫父類別的方法。
sub save {
my $self = shift;
say 'Prepare to rock';
$self->SUPER::save();
}
SUPER
修改器只能用於方法呼叫。您無法將它用於常規子常式呼叫或類別方法
SUPER::save($thing); # FAIL: looks for save() sub in package SUPER
SUPER->save($thing); # FAIL: looks for save() method in class
# SUPER
$thing->SUPER::save(); # Okay: looks for save() method in parent
# classes
SUPER
偽類別會從呼叫所在的套件中解析。它不會根據物件的類別解析。這很重要,因為它允許深層繼承階層中不同層級的方法各自正確地呼叫它們各自的父方法。
package A;
sub new {
return bless {}, shift;
}
sub speak {
my $self = shift;
say 'A';
}
package B;
use parent -norequire, 'A';
sub speak {
my $self = shift;
$self->SUPER::speak();
say 'B';
}
package C;
use parent -norequire, 'B';
sub speak {
my $self = shift;
$self->SUPER::speak();
say 'C';
}
my $c = C->new();
$c->speak();
在此範例中,我們將取得以下輸出
A
B
C
這展示了 SUPER
如何解析。即使物件被祝福到 C
類別中,B
類別中的 speak()
方法仍然可以呼叫 SUPER::speak()
,並預期它會正確地在 B
的父類別(即方法呼叫所在的類別)中尋找,而不是在 C
的父類別(即物件所屬的類別)中尋找。
在極少數情況下,這種基於套件的解析可能會造成問題。如果您將子常式從一個套件複製到另一個套件,SUPER
解析將會根據原始套件進行。
多重繼承通常表示設計問題,但 Perl 始終會給您足夠的繩索讓您自縊,如果您要求的話。
要宣告多個父類別,您只需將多個類別名稱傳遞給 use parent
package MultiChild;
use parent 'Parent1', 'Parent2';
方法解析順序只在多重繼承的情況下才有關係。在單一繼承的情況下,Perl 只會查看繼承鏈來尋找方法
Grandparent
|
Parent
|
Child
如果我們在 Child
物件上呼叫一個方法,而該方法未在 Child
類別中定義,Perl 將會在 Parent
類別中尋找該方法,然後在必要時在 Grandparent
類別中尋找。
如果 Perl 在這些類別中找不到該方法,它將會終止並顯示錯誤訊息。
當一個類別有多個父類別時,方法查詢順序會變得更複雜。
預設情況下,Perl 會對方法進行深度優先的由左至右搜尋。這表示它會從 @ISA
陣列中的第一個父類別開始,然後搜尋其所有父類別、祖父母類別等。如果找不到方法,它會轉到原始類別的 @ISA
陣列中的下一個父類別,並從那裡開始搜尋。
SharedGreatGrandParent
/ \
PaternalGrandparent MaternalGrandparent
\ /
Father Mother
\ /
Child
因此,根據上述圖表,Perl 會搜尋 Child
、Father
、PaternalGrandparent
、SharedGreatGrandParent
、Mother
,最後是 MaternalGrandparent
。這可能會是個問題,因為現在我們會在檢查完所有衍生類別(即在嘗試 Mother
和 MaternalGrandparent
之前)就先查看 SharedGreatGrandParent
。
可以使用 mro pragma 要求不同的方法解析順序。
package Child;
use mro 'c3';
use parent 'Father', 'Mother';
這個 pragma 讓你可以切換到「C3」解析順序。簡單來說,「C3」順序確保共用父類別絕不會在子類別之前被搜尋,因此 Perl 現在會搜尋:Child
、Father
、PaternalGrandparent
、Mother
、MaternalGrandparent
,最後是 SharedGreatGrandParent
。不過請注意,這並非「廣度優先」搜尋:所有 Father
祖先(除了共用祖先之外)都會在考慮任何 Mother
祖先之前被搜尋。
C3 順序也讓你可以在同層類別中使用 next
偽類別呼叫方法。有關此功能的更多詳細資訊,請參閱 mro 文件。
當 Perl 搜尋方法時,它會快取查詢結果,以便日後呼叫該方法時不需要再次搜尋。變更類別的父類別或將子常式新增到類別中,會使該類別的快取失效。
mro pragma 提供了一些函式,可直接操作方法快取。
正如我們先前提到的,Perl 沒有提供特殊的建構函式語法。這表示類別必須實作自己的建構函式。建構函式只是一個類別方法,用於傳回新物件的參考。
建構函式也可以接受定義物件的其他參數。讓我們為我們先前使用的 File
類別撰寫一個真正的建構函式
package File;
sub new {
my $class = shift;
my ( $path, $data ) = @_;
my $self = bless {
path => $path,
data => $data,
}, $class;
return $self;
}
如你所見,我們已將路徑和檔案資料儲存在物件本身中。請記住,在底層,這個物件仍然只是一個雜湊。稍後,我們會撰寫存取器來操作這些資料。
對於我們的 File::MP3
類別,我們可以檢查以確保我們提供的路徑以「.mp3」結尾
package File::MP3;
sub new {
my $class = shift;
my ( $path, $data ) = @_;
die "You cannot create a File::MP3 without an mp3 extension\n"
unless $path =~ /\.mp3\z/;
return $class->SUPER::new(@_);
}
這個建構函式讓其父類別執行實際的物件建構。
屬性是屬於特定物件的一段資料。與大多數物件導向語言不同,Perl 沒有提供宣告和操作屬性的特殊語法或支援。
屬性通常儲存在物件本身中。例如,如果物件是一個匿名雜湊,我們可以使用屬性名稱作為金鑰,將屬性值儲存在雜湊中。
雖然可以在類別外部直接參照這些雜湊金鑰,但建議將所有對屬性的存取都包覆在存取器方法中。
這有幾個優點。存取器可以更容易地變更物件的實作,同時仍保留原始 API。
存取器讓您可以在屬性存取周圍加入額外的程式碼。例如,您可以將預設值套用至建構函式中未設定的屬性,或者您可以驗證屬性的新值是否可接受。
最後,使用存取器會讓繼承變得更簡單。子類別可以使用存取器,而不必知道父類別在內部是如何實作的。
與建構函式一樣,Perl 沒有提供特殊的存取器宣告語法,因此類別必須提供明確撰寫的存取器方法。有兩種常見的存取器類型,唯讀和讀寫。
一個簡單的唯讀存取器僅取得單一屬性的值
sub path {
my $self = shift;
return $self->{path};
}
讀寫存取器將允許呼叫者設定值並取得值
sub path {
my $self = shift;
if (@_) {
$self->{path} = shift;
}
return $self->{path};
}
我們的建構函式和存取器並不聰明。它們不會檢查是否已定義 $path
,也不會檢查 $path
是否為有效的檔案系統路徑。
手動執行這些檢查可能會很快變得乏味。手動撰寫一堆存取器也令人難以置信地乏味。CPAN 上有很多模組可以幫助您撰寫更安全、更簡潔的程式碼,包括我們在 perlootut 中推薦的模組。
除了我們到目前為止看到的 $object->method()
用法之外,Perl 還支援其他幾種呼叫方法的方式。
Perl 允許您使用其完整限定名稱(套件和方法名稱)呼叫方法
my $mp3 = File::MP3->new( 'Regin.mp3', $data );
$mp3->File::save();
當您呼叫完整限定的方法名稱,例如 File::save
時,save
方法的方法解析搜尋會從 File
類別開始,略過 File::MP3
類別可能已定義的任何 save
方法。如果需要,它仍會搜尋 File
類別的父類別。
雖然此功能最常使用於明確呼叫從祖先類別繼承的方法,但沒有技術限制來強制執行這項功能
my $obj = Tree->new();
$obj->Dog::bark();
這會在類別 Tree
的物件上呼叫類別 Dog
的 bark
方法,即使這兩個類別完全無關。請小心使用。
先前描述的 SUPER
偽類別並非與使用完全限定名稱呼叫方法相同。有關詳細資訊,請參閱較早的 "繼承" 區段。
Perl 讓您可以使用包含字串的純量變數作為方法名稱
my $file = File->new( $path, $data );
my $method = 'save';
$file->$method();
這與呼叫 $file->save()
完全相同。這對於撰寫動態程式碼非常有用。例如,它允許您傳遞一個方法名稱,作為參數傳遞給另一個方法。
Perl 也讓您可以使用包含字串的純量作為類別名稱
my $class = 'File';
my $file = $class->new( $path, $data );
同樣地,這允許非常動態的程式碼。
您也可以使用子常式參考作為方法
my $sub = sub {
my $self = shift;
$self->save();
};
$file->$sub();
這完全等同於撰寫 $sub->($file)
。您可能會在野外看到此慣用語與呼叫 can
結合使用
if ( my $meth = $object->can('foo') ) {
$object->$meth();
}
Perl 也讓您在方法呼叫中使用解除參考的純量參考。這是一個很長的說法,讓我們來看一些程式碼
$file->${ \'save' };
$file->${ returns_scalar_ref() };
$file->${ \( returns_scalar() ) };
$file->${ returns_ref_to_sub_ref() };
如果解除參考產生字串或子常式參考,這會有效。
在底層,Perl 檔案處理是 IO::Handle
或 IO::File
類別的執行個體。一旦您開啟一個檔案處理,您就可以在它上面呼叫方法。此外,您可以在 STDIN
、STDOUT
和 STDERR
檔案處理上呼叫方法。
open my $fh, '>', 'path/to/file';
$fh->autoflush();
$fh->print('content');
STDOUT->autoflush();
由於 Perl 允許您對套件名稱和子常式名稱使用裸字,因此它有時會錯誤地解釋裸字的意義。例如,結構 Class->new()
可以解釋為 'Class'->new()
或 Class()->new()
。用英語來說,第二個解釋讀作「呼叫名為 Class() 的子常式,然後在 Class() 的傳回值上呼叫 new() 作為方法」。如果當前命名空間中有子常式名為 Class()
,Perl 始終會將 Class->new()
解釋為第二個替代方案:呼叫 Class()
傳回的物件上的 new()
您可以強制 Perl 使用第一個解釋(即作為名為「Class」的類別上的方法呼叫),有兩種方式。首先,您可以附加 ::
到類別名稱
Class::->new()
Perl 會永遠將此解釋為方法呼叫。
或者,您可以引用類別名稱
'Class'->new()
當然,如果類別名稱在標量中,Perl 也可以執行正確的動作
my $class = 'Class';
$class->new();
除了檔案處理案例之外,不建議使用此語法,因為它可能會讓 Perl 解譯器混淆。請參閱下方以取得更多詳細資料。
Perl 支援另一種稱為「間接物件」符號的方法呼叫語法。此語法稱為「間接」,因為方法在它被呼叫的物件之前。
此語法可用於任何類別或物件方法
my $file = new File $path, $data;
save $file;
我們建議您避免使用此語法,原因有幾個。
首先,閱讀時可能會令人混淆。在上方的範例中,不清楚 save
是 File
類別提供的某個方法,還是只是一個將檔案物件當作其第一個引數的子常式。
當與類別方法一起使用時,問題會更嚴重。由於 Perl 允許子常式名稱寫成裸字,因此 Perl 必須猜測方法後的裸字是類別名稱還是子常式名稱。換句話說,Perl 可以將語法解析為 File->new( $path, $data )
或 new( File( $path, $data ) )
。
為了解析此程式碼,Perl 使用一個啟發法,根據它所見過的套件名稱、目前套件中存在的子常式、它之前所見過的裸字和其他輸入。不用說,啟發法可能會產生非常令人驚訝的結果!
較舊的文件(和一些 CPAN 模組)鼓勵使用此語法,特別是對於建構函式,因此您可能仍然可以在野外找到它。但是,我們鼓勵您避免在新的程式碼中使用它。
您可以透過在裸字後面加上「::」,就像我們之前所見的那樣,強制 Perl 將裸字解釋為類別名稱
my $file = new File:: $path, $data;
間接物件語法僅在啟用 "indirect"
名稱功能時可用。此功能預設啟用,但可依要求停用。此功能存在於較舊的功能版本套件中,但已從 :5.36
套件中移除;因此,use VERSION
宣告為 v5.36
或更高版本也會停用此功能。
use v5.36;
# indirect object syntax is no longer available
bless
、blessed
和 ref
正如我們先前所見,物件只是一個透過 bless
函式祝福為類別的資料結構。bless
函式可以接受一個或兩個參數
my $object = bless {}, $class;
my $object = bless {};
在第一個形式中,匿名雜湊正在被祝福為 $class
中的類別。在第二個形式中,匿名雜湊被祝福為目前的套件。
強烈不建議使用第二個形式,因為它會破壞子類別重複使用父類別建構函式的功能,但您仍可能在現有程式碼中遇到它。
如果您想了解特定純量是否指涉物件,您可以使用 Perl 核心附帶的 Scalar::Util 所匯出的 blessed
函式。
use Scalar::Util 'blessed';
if ( defined blessed($thing) ) { ... }
如果 $thing
指涉物件,則此函式會傳回物件已祝福為的套件名稱。如果 $thing
不包含對已祝福物件的參照,則 blessed
函式會傳回 undef
。
請注意,如果 $thing
已祝福為名為「0」的類別,則 blessed($thing)
也會傳回 false。這是可能的,但相當病態。除非您知道自己在做什麼,否則不要建立名為「0」的類別。
類似地,Perl 的內建 ref
函式會特別處理對已祝福物件的參照。如果您呼叫 ref($thing)
且 $thing
保留對物件的參照,它會傳回物件已祝福為的類別名稱。
如果您只是想檢查變數是否包含物件參照,我們建議您使用 defined blessed($object)
,因為 ref
會傳回所有參照的 true 值,而不只是物件。
所有類別都會自動繼承內建於 Perl 核心中的 UNIVERSAL 類別。此類別提供多種方法,這些方法都可以在類別或物件上呼叫。您也可以選擇在類別中覆寫其中一些方法。如果您這樣做,我們建議您遵循以下所述的內建語義。
如果物件是 $class
中的成員,或 $class
子類別中的成員,則 isa
方法會傳回 true。
如果您覆寫此方法,它絕不能擲回例外。
如果物件宣稱要執行角色 $role
,則 DOES
方法會傳回 true。預設情況下,這等同於 isa
。此方法提供給實作角色的物件系統擴充功能使用,例如 Moose
和 Role::Tiny
。
您也可以在自己的類別中直接覆寫 DOES
。如果您覆寫此方法,它絕不能擲回例外。
can
方法會檢查呼叫它的類別或物件是否具有名為 $method
的方法。這會檢查類別和其所有父類別中的方法。如果方法存在,則會傳回子常式的參考。如果不存在,則會傳回 undef
。
如果您的類別透過 AUTOLOAD
回應方法呼叫,您可能想要覆載 can
以傳回 AUTOLOAD
方法處理的方法的子常式參考。
如果您覆寫此方法,它絕不能擲回例外。
VERSION
方法會傳回類別(套件)的版本號碼。
如果給定 $need
參數,則它會檢查目前的版本(由套件中的 $VERSION 變數定義)是否大於或等於 $need
;如果不是,它會終止。此方法會由 use
的 VERSION
形式自動呼叫。
use Package 1.2 qw(some imported subs);
# implies:
Package->VERSION(1.2);
我們建議您使用此方法存取另一個套件的版本,而不是直接查看 $Package::VERSION
。您正在查看的套件可能已經覆寫 VERSION
方法。
我們也建議使用此方法檢查模組是否有足夠的版本。內部實作使用 version 模組以確保正確比較不同類型的版本號碼。
如果您呼叫一個在類別中不存在的方法,Perl 會擲回一個錯誤。但是,如果該類別或其任何父類別定義了一個 AUTOLOAD
方法,則會呼叫 AUTOLOAD
方法。
AUTOLOAD
會以一般方法呼叫,而呼叫者不會知道差異。您的 AUTOLOAD
方法傳回的任何值都會傳回給呼叫者。
在您的類別中,已呼叫的完整限定方法名稱會在 $AUTOLOAD
套件全域變數中提供。由於這是一個全域變數,如果您想在 strict 'vars'
下不帶套件名稱前綴來參考它,則需要宣告它。
# XXX - this is a terrible way to implement accessors, but it makes
# for a simple example.
our $AUTOLOAD;
sub AUTOLOAD {
my $self = shift;
# Remove qualifier from original method name...
my $called = $AUTOLOAD =~ s/.*:://r;
# Is there an attribute of that name?
die "No such attribute: $called"
unless exists $self->{$called};
# If so, return it...
return $self->{$called};
}
sub DESTROY { } # see below
在沒有 our $AUTOLOAD
宣告的情況下,此程式碼無法在 strict pragma 下編譯。
正如註解所述,這不是實作存取器的理想方式。它很慢,而且太過聰明。但是,您可能會將此視為在較舊的 Perl 程式碼中提供存取器的方式。請參閱 perlootut 以取得 Perl 中物件導向編碼的建議。
如果您的類別確實有一個 AUTOLOAD
方法,我們強烈建議您也在類別中覆寫 can
。您的覆寫 can
方法應傳回您的 AUTOLOAD
回應的任何方法的子常式參考。
當對物件的最後一個參考消失時,物件會被銷毀。如果您只有一個參考儲存在詞彙標量中的物件,則當該標量超出範圍時,物件會被銷毀。如果您將物件儲存在套件全域變數中,則該物件可能不會超出範圍,直到程式結束為止。
如果您想在物件被銷毀時執行某些操作,則可以在類別中定義一個 DESTROY
方法。除非方法為空,否則 Perl 會在適當時機始終呼叫此方法。
這會像任何其他方法一樣被呼叫,物件作為第一個引數。它不會接收任何其他引數。但是,$_[0]
變數在銷毀器中會是唯讀的,因此您無法為它指定值。
如果您的 DESTROY
方法擲回例外,這不會導致任何控制傳遞超出退出方法。例外會以警告的形式報告給 STDERR
,標記為「(in cleanup)」,而 Perl 會繼續執行它之前正在執行的任何操作。
由於 DESTROY
方法可以在任何時候被呼叫,因此您應該將任何可能由 DESTROY
方法中的任何操作設定的任何全域狀態變數區域化。如果您對特定狀態變數有疑問,將其區域化並不會造成任何損害。有五個全域狀態變數,最安全的方法是將所有五個變數區域化
sub DESTROY {
local($., $@, $!, $^E, $?);
my $self = shift;
...;
}
如果您在類別中定義一個 AUTOLOAD
,則 Perl 會呼叫您的 AUTOLOAD
來處理 DESTROY
方法。您可以透過定義一個空的 DESTROY
來防止這種情況,就像我們在自動載入範例中所做的那樣。您還可以在呼叫處理 DESTROY
時檢查 $AUTOLOAD
的值,並在沒有執行任何操作的情況下傳回。
在程式結束前的全域銷毀期間銷毀物件的順序是不可預測的。這表示您的物件包含的任何物件可能已經被銷毀。您應該檢查包含的物件是否已定義,然後再對它呼叫方法
sub DESTROY {
my $self = shift;
$self->{handle}->close() if $self->{handle};
}
你可以使用 ${^GLOBAL_PHASE}
變數來偵測你目前是否處於全域毀損階段
sub DESTROY {
my $self = shift;
return if ${^GLOBAL_PHASE} eq 'DESTRUCT';
$self->{handle}->close();
}
請注意,這個變數是在 Perl 5.14.0 中加入的。如果你想要在舊版的 Perl 中偵測全域毀損階段,你可以使用 CPAN 上的 Devel::GlobalDestruction
模組。
如果你的 DESTROY
方法在全域毀損期間發出警告,Perl 解譯器會在警告中附加字串「during global destruction」。
在全域毀損期間,Perl 會在取消祝福的參照之前,永遠都會對物件進行垃圾回收。請參閱 "perlhacktips 中的「PERL_DESTRUCT_LEVEL」,以取得有關全域毀損的更多資訊。
到目前為止,所有範例都顯示了基於祝福雜湊的物件。但是,可以祝福任何類型的資料結構或參照,包括純量、glob 和子常式。你可能會在檢視外界的程式碼時看到這種情況。
以下是作為祝福純量的模組範例
package Time;
use v5.36;
sub new {
my $class = shift;
my $time = time;
return bless \$time, $class;
}
sub epoch {
my $self = shift;
return $$self;
}
my $time = Time->new();
print $time->epoch();
過去,Perl 社群嘗試了一種稱為「內外顛倒物件」的技術。內外顛倒物件會將其資料儲存在物件參照外部,並以物件的唯一屬性(例如其記憶體位址)作為索引,而不是儲存在物件本身中。這具有強制執行物件屬性封裝的優點,因為其資料並未儲存在物件本身中。
這種技術曾經很流行(並在 Damian Conway 的《Perl 最佳實務》中推薦),但從未獲得普遍採用。CPAN 上的 Object::InsideOut 模組提供了此技術的全面實作,你可能會在外界看到它或其他內外顛倒模組。
以下是使用核心模組 Hash::Util::FieldHash 的技術範例。此模組已新增至核心,以支援內外顛倒物件實作。
package Time;
use v5.36;
use Hash::Util::FieldHash 'fieldhash';
fieldhash my %time_for;
sub new {
my $class = shift;
my $self = bless \( my $object ), $class;
$time_for{$self} = time;
return $self;
}
sub epoch {
my $self = shift;
return $time_for{$self};
}
my $time = Time->new;
print $time->epoch;
偽雜湊功能是 Perl 早期版本中引入的實驗性功能,並已在 5.10.0 中移除。偽雜湊是一個陣列參照,可以使用類似雜湊的名稱鍵來存取。你可能會在使用它的外界程式碼中執行。請參閱 fields 實用指令,以取得更多資訊。
可以在 perlootut 中找到 Perl 中面向物件程式設計的更友善教學。你還應該查看 perlmodlib,以取得建構模組和類別的某些樣式指南。