perltie - 如何在簡單變數中隱藏物件類別
tie VARIABLE, CLASSNAME, LIST
$object = tied VARIABLE
untie VARIABLE
在 Perl 發布 5.0 版之前,程式設計師可以使用 dbmopen() 將標準 Unix dbm(3x) 格式的磁碟資料庫神奇地連接到程式中的 %HASH。但是,Perl 不是使用某個特定的 dbm 函式庫建置,就是使用另一個,而無法同時使用兩個,而且無法將此機制擴充到其他套件或變數類型。
現在可以了。
tie() 函式會將變數繫結到一個類別 (套件),該類別將提供該變數存取方法的實作。執行此神奇操作後,存取繫結變數會自動觸發適當類別中的方法呼叫。類別的複雜性隱藏在神奇的方法呼叫背後。方法名稱全部使用大寫字母,這是 Perl 用來表示它們是隱含呼叫而非明確呼叫的慣例,就像 BEGIN() 和 END() 函式一樣。
在 tie() 呼叫中,變數
是變數名稱,用於被附魔。類別名稱
是實作正確類型物件的類別名稱。清單
中的任何其他引數都會傳遞給該類別的適當建構函式方法,也就是 TIESCALAR()、TIEARRAY()、TIEHASH() 或 TIEHANDLE()。(通常這些引數類似於傳遞給 C 的 dbminit() 函式的引數。)「new」方法傳回的物件也會由 tie() 函式傳回,如果您想存取 類別名稱
中的其他方法,這會很有用。(您實際上不必傳回對正確「類型」(例如 HASH 或 類別名稱
)的參考,只要它是正確附魔的物件即可。)您也可以使用 tied() 函式來擷取對底層物件的參考。
與 dbmopen() 不同,tie() 函式不會為您使用
或需要
模組,您需要自行明確執行此操作。
實作附魔純量的類別應定義下列方法:TIESCALAR、FETCH、STORE,以及可能是 UNTIE 和/或 DESTROY。
讓我們依序檢視每個方法,使用一個純量附魔類別作為範例,讓使用者可以執行類似下列動作
tie $his_speed, 'Nice', getppid();
tie $my_speed, 'Nice', $$;
現在,每當存取這兩個變數時,都會擷取並傳回其目前的系統優先順序。如果設定這些變數,則會變更程序的優先順序!
我們將使用 Jarkko Hietaniemi <jhi@iki.fi> 的 BSD::Resource 類別(未包含)來存取系統的 PRIO_PROCESS、PRIO_MIN 和 PRIO_MAX 常數,以及 getpriority() 和 setpriority() 系統呼叫。以下是類別的前言。
package Nice;
use Carp;
use BSD::Resource;
use strict;
$Nice::DEBUG = 0 unless defined $Nice::DEBUG;
這是類別的建構函式。這表示預期會傳回對它正在建立的新純量(可能是匿名)的附魔參考。例如
sub TIESCALAR {
my $class = shift;
my $pid = shift || $$; # 0 means me
if ($pid !~ /^\d+$/) {
carp "Nice::Tie::Scalar got non-numeric pid $pid" if $^W;
return undef;
}
unless (kill 0, $pid) { # EPERM or ERSCH, no doubt
carp "Nice::Tie::Scalar got bad pid $pid: $!" if $^W;
return undef;
}
return bless \$pid, $class;
}
此附魔類別選擇傳回錯誤,而不是在建構函式失敗時引發例外。雖然這是 dbmopen() 的運作方式,但其他類別可能不希望如此寬容。它會檢查全域變數 $^W
,以查看是否要發出一些雜訊。
每次存取附魔變數(讀取)時,都會觸發此方法。除了 self 參考之外,它不會取得任何引數,而 self 參考是表示我們正在處理的純量的物件。由於在這個案例中,我們只為附魔純量物件使用 SCALAR 參考,因此簡單的 $$self 允許方法取得儲存在那裡的真實值。在我們下面的範例中,那個真實值是我們已將變數附魔到的程序 ID。
sub FETCH {
my $self = shift;
confess "wrong type" unless ref $self;
croak "usage error" if @_;
my $nicety;
local($!) = 0;
$nicety = getpriority(PRIO_PROCESS, $$self);
if ($!) { croak "getpriority failed: $!" }
return $nicety;
}
這次我們決定在 renice 失敗時爆炸(引發例外),否則我們無法傳回錯誤,而且這可能是正確的作法。
每次設定 (指派) 繫結變數時,都會觸發此方法。除了自我參照之外,它還預期一個 (且僅一個) 參數:使用者嘗試指派的新的值。不用擔心從 STORE 傳回值;指派語意傳回已指派值已由 FETCH 實作。
sub STORE {
my $self = shift;
confess "wrong type" unless ref $self;
my $new_nicety = shift;
croak "usage error" if @_;
if ($new_nicety < PRIO_MIN) {
carp sprintf
"WARNING: priority %d less than minimum system priority %d",
$new_nicety, PRIO_MIN if $^W;
$new_nicety = PRIO_MIN;
}
if ($new_nicety > PRIO_MAX) {
carp sprintf
"WARNING: priority %d greater than maximum system priority %d",
$new_nicety, PRIO_MAX if $^W;
$new_nicety = PRIO_MAX;
}
unless (defined setpriority(PRIO_PROCESS,
$$self,
$new_nicety))
{
confess "setpriority failed: $!";
}
}
當發生 解除繫結
時,會觸發此方法。如果類別需要知道何時不會再進行呼叫,這會很有用。(當然除了 DESTROY 之外。)有關更多詳細資訊,請參閱下方的 "解除繫結的陷阱"。
當繫結變數需要銷毀時,會觸發此方法。與其他物件類別一樣,這種方法很少有必要,因為 Perl 會自動為您釋放已死物件的記憶體,您知道這不是 C++。我們僅會在這裡使用 DESTROY 方法來進行除錯。
sub DESTROY {
my $self = shift;
confess "wrong type" unless ref $self;
carp "[ Nice::DESTROY pid $$self ]" if $Nice::DEBUG;
}
這大概就是全部了。實際上,這還不止這些,因為為了完整性、健全性和整體美觀,我們在此做了幾件不錯的事。當然可以有更簡單的 TIESCALAR 類別。
實作繫結一般陣列的類別應定義下列方法:TIEARRAY、FETCH、STORE、FETCHSIZE、STORESIZE、CLEAR,以及可能是 UNTIE 和/或 DESTROY。
FETCHSIZE 和 STORESIZE 用於提供 $#array
和等效的 scalar(@array)
存取。
如果要讓 perl 具有對應 (但小寫) 名稱的運算子在繫結陣列上運作,則需要 POP、PUSH、SHIFT、UNSHIFT、SPLICE、DELETE 和 EXISTS 方法。Tie::Array 類別可用作基礎類別,以根據上述基本方法實作前五個方法。Tie::Array 中 DELETE 和 EXISTS 的預設實作僅會 croak
。
此外,當 perl 在真實陣列中預先擴充配置時,將會呼叫 EXTEND。
在這個討論中,我們將實作一個陣列,其元素在建立時具有固定大小。如果您嘗試建立大於固定大小的元素,您將會發生例外。例如
use FixedElem_Array;
tie @array, 'FixedElem_Array', 3;
$array[0] = 'cat'; # ok.
$array[1] = 'dogs'; # exception, length('dogs') > 3.
類別的前置碼如下
package FixedElem_Array;
use Carp;
use strict;
這是類別的建構函式。這表示它預期會傳回一個受祝福的參考,新的陣列(可能是匿名 ARRAY 參考)會透過這個參考存取。
在我們的範例中,只是為了讓您知道您真的不必傳回 ARRAY 參考,我們會選擇 HASH 參考來代表我們的物件。HASH 非常適合當成一般記錄類型:{ELEMSIZE}
欄位會儲存允許的最大元素大小,而 {ARRAY}
欄位會保留真正的 ARRAY 參考。如果類別外的某人嘗試取消對傳回物件的參考(無疑會認為它是 ARRAY 參考),他們會失敗。這只是要讓您知道您應該尊重物件的隱私。
sub TIEARRAY {
my $class = shift;
my $elemsize = shift;
if ( @_ || $elemsize =~ /\D/ ) {
croak "usage: tie ARRAY, '" . __PACKAGE__ . "', elem_size";
}
return bless {
ELEMSIZE => $elemsize,
ARRAY => [],
}, $class;
}
每次存取(讀取)綁定的陣列中的個別元素時,都會觸發這個方法。它會取得一個自參考以外的引數:我們嘗試擷取其值的索引。
sub FETCH {
my $self = shift;
my $index = shift;
return $self->{ARRAY}->[$index];
}
如果使用負陣列索引從陣列讀取,索引會在傳遞至 FETCH 之前,透過呼叫 FETCHSIZE 轉換成正值。您可以在綁定陣列類別中將變數 $NEGATIVE_INDICES
指定為 true 值,以停用這個功能。
您可能已經注意到,FETCH 方法(等)的名稱對所有存取都是相同的,即使建構函式的名稱不同(TIESCALAR 與 TIEARRAY)。雖然理論上您可以讓同一個類別提供服務給多種綁定類型,但實際上這會很麻煩,而且最簡單的方法是讓每個類別只綁定一種類型。
每次設定(寫入)綁定陣列中的元素時,都會觸發這個方法。它會取得兩個自參考以外的引數:我們嘗試儲存資料的索引,以及我們嘗試放入其中的值。
在我們的範例中,undef
實際上是 $self->{ELEMSIZE}
個空白,所以我們還有點工作要做
sub STORE {
my $self = shift;
my( $index, $value ) = @_;
if ( length $value > $self->{ELEMSIZE} ) {
croak "length of $value is greater than $self->{ELEMSIZE}";
}
# fill in the blanks
$self->STORESIZE( $index ) if $index > $self->FETCHSIZE();
# right justify to keep element size for smaller elements
$self->{ARRAY}->[$index] = sprintf "%$self->{ELEMSIZE}s", $value;
}
負索引的處理方式與 FETCH 相同。
傳回與物件 this 關聯的綁定陣列中項目的總數目。(等同於 scalar(@array)
)。例如
sub FETCHSIZE {
my $self = shift;
return scalar $self->{ARRAY}->@*;
}
將與物件 this 關聯的綁定陣列中項目的總數目設定為 count。如果這會讓陣列變大,則類別的 undef
對應應傳回給新位置。如果陣列變小,則應刪除超過 count 的項目。
在我們的範例中,'undef' 實際上是包含 $self->{ELEMSIZE}
個空白的元素。觀察
sub STORESIZE {
my $self = shift;
my $count = shift;
if ( $count > $self->FETCHSIZE() ) {
foreach ( $count - $self->FETCHSIZE() .. $count ) {
$self->STORE( $_, '' );
}
} elsif ( $count < $self->FETCHSIZE() ) {
foreach ( 0 .. $self->FETCHSIZE() - $count - 2 ) {
$self->POP();
}
}
}
資訊呼叫,陣列可能會長到有 count 個項目。可用於最佳化配置。此方法不需要做任何事。
在我們的範例中,沒有理由實作此方法,所以我們讓它成為空操作。此方法僅與綁定陣列實作相關,其中有可能性讓陣列的配置大小大於 Perl 程式設計師檢查陣列大小時所看到的。許多綁定陣列實作沒有理由實作它。
sub EXTEND {
my $self = shift;
my $count = shift;
# nothing to see here, move along.
}
注意:通常讓這等同於 STORESIZE 是個錯誤。Perl 可能會偶爾呼叫 EXTEND,而不想要實際直接變更陣列大小。如果此方法是空操作,任何綁定陣列都應該能正確運作,即使它們可能不會像實作此方法時一樣有效率。
驗證綁定陣列 this 中索引 key 處的元素是否存在。
在我們的範例中,我們將確定如果元素僅包含 $self->{ELEMSIZE}
個空白,它就不存在
sub EXISTS {
my $self = shift;
my $index = shift;
return 0 if ! defined $self->{ARRAY}->[$index] ||
$self->{ARRAY}->[$index] eq ' ' x $self->{ELEMSIZE};
return 1;
}
從綁定陣列 this 中刪除索引 key 處的元素。
在我們的範例中,已刪除的項目是 $self->{ELEMSIZE}
個空白
sub DELETE {
my $self = shift;
my $index = shift;
return $self->STORE( $index, '' );
}
清除 (移除、刪除、...) 與物件 this 相關聯的繫結陣列中的所有值。例如
sub CLEAR {
my $self = shift;
return $self->{ARRAY} = [];
}
將 LIST 的元素附加到陣列。例如
sub PUSH {
my $self = shift;
my @list = @_;
my $last = $self->FETCHSIZE();
$self->STORE( $last + $_, $list[$_] ) foreach 0 .. $#list;
return $self->FETCHSIZE();
}
移除陣列的最後一個元素並傳回。例如
sub POP {
my $self = shift;
return pop $self->{ARRAY}->@*;
}
移除陣列的第一個元素 (將其他元素向下移動) 並傳回。例如
sub SHIFT {
my $self = shift;
return shift $self->{ARRAY}->@*;
}
在陣列的開頭插入 LIST 元素,將現有元素向上移動以騰出空間。例如
sub UNSHIFT {
my $self = shift;
my @list = @_;
my $size = scalar( @list );
# make room for our list
$self->{ARRAY}[ $size .. $self->{ARRAY}->$#* + $size ]->@*
= $self->{ARRAY}->@*
$self->STORE( $_, $list[$_] ) foreach 0 .. $#list;
}
在陣列上執行等同於 splice
的操作。
offset 是可選的,預設為零,負值從陣列的結尾開始倒數。
length 是可選的,預設為陣列的其餘部分。
LIST 可以是空的。
傳回在 offset 處的原始 length 元素的清單。
在我們的範例中,如果有一個 LIST,我們將使用一個小捷徑
sub SPLICE {
my $self = shift;
my $offset = shift || 0;
my $length = shift || $self->FETCHSIZE() - $offset;
my @list = ();
if ( @_ ) {
tie @list, __PACKAGE__, $self->{ELEMSIZE};
@list = @_;
}
return splice $self->{ARRAY}->@*, $offset, $length, @list;
}
將在發生 untie
時呼叫。(請參閱下方的「untie
Gotcha」)
當繫結變數需要被銷毀時,將觸發此方法。與純量繫結類別一樣,這在會執行自己的垃圾回收的語言中幾乎不需要,所以這次我們將省略它。
雜湊是第一個被繫結的 Perl 資料類型 (請參閱 dbmopen())。實作繫結雜湊的類別應定義下列方法:TIEHASH 是建構函式。FETCH 和 STORE 存取鍵值對。EXISTS 回報雜湊中是否存在鍵,而 DELETE 刪除一個鍵。CLEAR 透過刪除所有鍵值對來清空雜湊。FIRSTKEY 和 NEXTKEY 實作 keys() 和 each() 函式以遍歷所有鍵。SCALAR 在繫結雜湊在純量環境中被評估時觸發,而在 5.28 之後,則由布林環境中的 keys
觸發。UNTIE 在發生 untie
時呼叫,而 DESTROY 在繫結變數被垃圾回收時呼叫。
如果這看起來很多,那麼請隨時從標準 Tie::StdHash 模組繼承大部分方法,僅重新定義有意義的方法。請參閱 Tie::Hash 以取得詳細資料。
請記住,Perl 區分雜湊中不存在的鍵和雜湊中存在的鍵,但其對應值為 undef
。這兩種可能性可以使用 exists()
和 defined()
函數進行測試。
以下是有些有趣的繫結雜湊類別的範例:它提供一個代表特定使用者點檔案的雜湊。您可以使用檔案名稱(減去點)編入雜湊索引,並取得該點檔案的內容。例如
use DotFiles;
tie %dot, 'DotFiles';
if ( $dot{profile} =~ /MANPATH/ ||
$dot{login} =~ /MANPATH/ ||
$dot{cshrc} =~ /MANPATH/ )
{
print "you seem to set your MANPATH\n";
}
以下是使用我們繫結類別的另一個範例
tie %him, 'DotFiles', 'daemon';
foreach $f ( keys %him ) {
printf "daemon dot file %s is size %d\n",
$f, length $him{$f};
}
在我們的繫結雜湊 DotFiles 範例中,我們使用一個正規雜湊作為包含多個重要欄位的物件,其中只有 {LIST}
欄位會是使用者認為的真實雜湊。
以下是 Dotfiles.pm 的開頭
package DotFiles;
use Carp;
sub whowasi { (caller(1))[3] . '()' }
my $DEBUG = 0;
sub debug { $DEBUG = @_ ? shift : 1 }
對於我們的範例,我們希望能夠發出除錯資訊,以協助在開發期間進行追蹤。我們也保留一個方便的函數在內部,以協助印出警告;whowasi() 會傳回呼叫它的函數名稱。
以下是 DotFiles 繫結雜湊的方法。
這是類別的建構函式。這表示它預期會傳回一個經過祝福的參考,新的物件(可能是但未必是一個匿名雜湊)會透過它來存取。
以下是建構函式
sub TIEHASH {
my $class = shift;
my $user = shift || $>;
my $dotdir = shift || '';
croak "usage: @{[&whowasi]} [USER [DOTDIR]]" if @_;
$user = getpwuid($user) if $user =~ /^\d+$/;
my $dir = (getpwnam($user))[7]
|| croak "@{[&whowasi]}: no user $user";
$dir .= "/$dotdir" if $dotdir;
my $node = {
USER => $user,
HOME => $dir,
LIST => {},
CLOBBER => 0,
};
opendir(DIR, $dir)
|| croak "@{[&whowasi]}: can't opendir $dir: $!";
foreach $dot ( grep /^\./ && -f "$dir/$_", readdir(DIR)) {
$dot =~ s/^\.//;
$node->{LIST}{$dot} = undef;
}
closedir DIR;
return bless $node, $class;
}
值得一提的是,如果您要對 readdir 的傳回值進行檔案測試,最好先加上目錄。否則,由於我們沒有在這裡執行 chdir(),它會測試錯誤的檔案。
每次存取繫結雜湊中的元素(讀取)時,都會觸發這個方法。除了 self 參考之外,它還有一個參數:我們嘗試擷取其值的鍵。
以下是 DotFiles 範例的擷取。
sub FETCH {
carp &whowasi if $DEBUG;
my $self = shift;
my $dot = shift;
my $dir = $self->{HOME};
my $file = "$dir/.$dot";
unless (exists $self->{LIST}->{$dot} || -f $file) {
carp "@{[&whowasi]}: no $dot file" if $DEBUG;
return undef;
}
if (defined $self->{LIST}->{$dot}) {
return $self->{LIST}->{$dot};
} else {
return $self->{LIST}->{$dot} = `cat $dir/.$dot`;
}
}
透過呼叫 Unix cat(1) 命令,可以輕鬆撰寫,但手動開啟檔案(且效率稍高)可能會更具可攜性。當然,由於點檔案是一個 Unix 概念,因此我們不必太擔心。
這個方法會在綁定雜湊中的元素被設定(寫入)時觸發。除了自參考外,它會接收兩個參數:我們嘗試儲存內容的索引,以及我們嘗試放入該索引的值。
在我們的 DotFiles 範例中,我們會小心不讓他們覆寫檔案,除非他們呼叫 tie() 回傳的原始物件參考上的 clobber() 方法。
sub STORE {
carp &whowasi if $DEBUG;
my $self = shift;
my $dot = shift;
my $value = shift;
my $file = $self->{HOME} . "/.$dot";
my $user = $self->{USER};
croak "@{[&whowasi]}: $file not clobberable"
unless $self->{CLOBBER};
open(my $f, '>', $file) || croak "can't open $file: $!";
print $f $value;
close($f);
}
如果他們想要覆寫某個內容,他們可能會這樣說
$ob = tie %daemon_dots, 'daemon';
$ob->clobber(1);
$daemon_dots{signature} = "A true daemon\n";
取得底層物件參考的另一種方法是使用 tied() 函式,因此他們可能會使用以下方式設定 clobber
tie %daemon_dots, 'daemon';
tied(%daemon_dots)->clobber(1);
clobber 方法很簡單
sub clobber {
my $self = shift;
$self->{CLOBBER} = @_ ? shift : 1;
}
當我們從雜湊中移除元素時,這個方法會被觸發,通常是使用 delete() 函式。同樣地,我們會小心檢查他們是否真的想要覆寫檔案。
sub DELETE {
carp &whowasi if $DEBUG;
my $self = shift;
my $dot = shift;
my $file = $self->{HOME} . "/.$dot";
croak "@{[&whowasi]}: won't remove file $file"
unless $self->{CLOBBER};
delete $self->{LIST}->{$dot};
my $success = unlink($file);
carp "@{[&whowasi]}: can't unlink $file: $!" unless $success;
$success;
}
DELETE 回傳的值會變成呼叫 delete() 的回傳值。如果你想要模擬 delete() 的正常行為,你應該回傳 FETCH 會為這個金鑰回傳的任何內容。在這個範例中,我們選擇回傳一個值,告訴呼叫者檔案是否已成功刪除。
當整個雜湊要被清除時,這個方法會被觸發,通常是透過將空清單指定給它。
在我們的範例中,那會移除使用者所有的點檔案!這是一件非常危險的事,他們必須將 CLOBBER 設定為大於 1 的值才能執行。
sub CLEAR {
carp &whowasi if $DEBUG;
my $self = shift;
croak "@{[&whowasi]}: won't remove all dot files for $self->{USER}"
unless $self->{CLOBBER} > 1;
my $dot;
foreach $dot ( keys $self->{LIST}->%* ) {
$self->DELETE($dot);
}
}
當使用者對特定雜湊使用 exists() 函式時,這個方法會被觸發。在我們的範例中,我們會查看 {LIST}
雜湊元素
sub EXISTS {
carp &whowasi if $DEBUG;
my $self = shift;
my $dot = shift;
return exists $self->{LIST}->{$dot};
}
當使用者要透過雜湊進行反覆運算時,這個方法會被觸發,例如透過 keys()、values() 或 each() 呼叫。
sub FIRSTKEY {
carp &whowasi if $DEBUG;
my $self = shift;
my $a = keys $self->{LIST}->%*; # reset each() iterator
each $self->{LIST}->%*
}
FIRSTKEY 總是在純量內容中呼叫,它應該只回傳第一個金鑰。values() 和 each() 在清單內容中會呼叫回傳金鑰的 FETCH。
此方法會在 keys()、values() 或 each() 迭代期間觸發。它有一個第二個引數,是最後存取的鍵。如果您關心順序或從多個序列呼叫迭代器,或沒有真正將項目儲存在雜湊中,這會很有用。
NEXTKEY 始終在標量內容中呼叫,它應只傳回下一個鍵。values() 和 each() 在清單內容中,會呼叫 FETCH 以取得傳回的鍵。
對於我們的範例,我們使用真正的雜湊,所以我們只會執行簡單的動作,但我們必須間接透過 LIST 欄位。
sub NEXTKEY {
carp &whowasi if $DEBUG;
my $self = shift;
return each $self->{LIST}->%*
}
如果繫結雜湊底層的物件不是真正的雜湊,而且您沒有可用的 each
,那麼在到達鍵清單尾端時,您應傳回 undef
或空清單。請參閱 each 的自訂文件
以取得更多詳細資料。
當雜湊在標量內容中評估時,以及在 5.28 之後,在布林內容中由 keys
呼叫時,會呼叫此方法。為了模擬未繫結雜湊的行為,此方法必須傳回一個值,當用作布林時,表示繫結雜湊是否視為空值。如果此方法不存在,perl 會做出一些有根據的猜測,並在雜湊位於迭代中時傳回 true。如果不是這種情況,會呼叫 FIRSTKEY,如果 FIRSTKEY 傳回空清單,結果將為 false 值,否則為 true。
但是,您不應盲目依賴 perl 總是執行正確的動作。特別是,當您透過重複呼叫 DELETE 直到它為空來清除雜湊時,perl 會錯誤地傳回 true。因此,建議您在想要完全確定雜湊在標量內容中表現良好時,提供您自己的 SCALAR 方法。
在我們的範例中,我們可以只對由 $self->{LIST}
參照的底層雜湊呼叫 scalar
sub SCALAR {
carp &whowasi if $DEBUG;
my $self = shift;
return scalar $self->{LIST}->%*
}
注意:在 perl 5.25 中,未繫結雜湊上 scalar %hash 的行為已變更為傳回鍵的計數。在此之前,它會傳回包含雜湊儲存區設定資訊的字串。請參閱 "Hash::Util 中的 bucket_ratio" 以取得向下相容性路徑。
當發生 untie
時,會呼叫此方法。請參閱下方的 "untie
Gotcha"。
當繫結雜湊即將超出範圍時,會觸發此方法。除非您嘗試新增除錯或有輔助狀態要清除,否則您不需要它。以下是極為簡單的函式
sub DESTROY {
carp &whowasi if $DEBUG;
}
請注意,當用於大型物件(例如 DBM 檔案)時,keys() 和 values() 等函式可能會傳回龐大的清單。您可能偏好使用 each() 函式來反覆處理此類物件。範例
# print out history file offsets
use NDBM_File;
tie(%HIST, 'NDBM_File', '/usr/lib/news/history', 1, 0);
while (($key,$val) = each %HIST) {
print $key, ' = ', unpack('L',$val), "\n";
}
untie(%HIST);
這部分現在已實作完成。
實作繫結檔案句柄的類別應定義下列方法:TIEHANDLE、至少一個 PRINT、PRINTF、WRITE、READLINE、GETC、READ,以及可能是 CLOSE、UNTIE 和 DESTROY。類別也可以提供:BINMODE、OPEN、EOF、FILENO、SEEK、TELL,如果句柄上使用對應的 perl 運算子。
當繫結 STDERR 時,其 PRINT 方法會被呼叫來發出警告和錯誤訊息。此功能會在呼叫期間暫時停用,表示您可以在 PRINT 內部使用 warn()
,而不會啟動遞迴迴圈。而且就像 __WARN__
和 __DIE__
處理常式一樣,STDERR 的 PRINT 方法可能會被呼叫來報告剖析器錯誤,因此 "perlvar 中的 %SIG" 下提到的注意事項適用。
當 perl 嵌入在其他程式中時,這一切特別有用,其中輸出至 STDOUT 和 STDERR 可能必須以某種特殊方式重新導向。請參閱 nvi 和 Apache 模組以取得範例。
當繫結句柄時,tie
的第一個引數應以星號開頭。因此,如果您要繫結 STDOUT,請使用 *STDOUT
。如果您已將其指定給一個純量變數(例如 $handle
),請使用 *$handle
。tie $handle
繫結純量變數 $handle
,而不是其內部的句柄。
在我們的範例中,我們將建立一個大聲的句柄。
package Shout;
這是類別的建構函式。這表示它預期會傳回某種形式的祝福參考。參考可以用來儲存一些內部資訊。
sub TIEHANDLE { print "<shout>\n"; my $i; bless \$i, shift }
當透過 syswrite
函式寫入句柄時,將會呼叫此方法。
sub WRITE {
$r = shift;
my($buf,$len,$offset) = @_;
print "WRITE called, \$buf=$buf, \$len=$len, \$offset=$offset";
}
每當使用 print()
或 say()
函式列印繫結的句柄時,將會觸發此方法。除了其自我參考之外,它也預期傳遞給列印函式的清單。
sub PRINT { $r = shift; $$r++; print join($,,map(uc($_),@_)),$\ }
say()
的作用就像 print()
,只不過 $\ 會定位到 \n
,因此您不需要在 PRINT()
中執行任何特殊動作來處理 say()
。
每當使用 printf()
函式列印繫結的句柄時,將會觸發此方法。除了其自我參考之外,它也預期傳遞給 printf 函式的格式和清單。
sub PRINTF {
shift;
my $fmt = shift;
print sprintf($fmt, @_);
}
當透過 read
或 sysread
函式從句柄讀取時,將會呼叫此方法。
sub READ {
my $self = shift;
my $bufref = \$_[0];
my(undef,$len,$offset) = @_;
print "READ called, \$buf=$bufref, \$len=$len, \$offset=$offset";
# add to $$bufref, set $len to number of characters read
$len;
}
當透過 <HANDLE>
或 readline HANDLE
讀取句柄時,會呼叫此方法。
根據 readline
,在純量內容中,它應傳回下一行,或傳回 undef
表示沒有更多資料。在清單內容中,它應傳回所有剩餘行,或傳回空清單表示沒有更多資料。傳回的字串應包含輸入記錄分隔符號 $/
(請參閱 perlvar),除非它是 undef
(表示「slurp」模式)。
sub READLINE {
my $r = shift;
if (wantarray) {
return ("all remaining\n",
"lines up\n",
"to eof\n");
} else {
return "READLINE called " . ++$$r . " times\n";
}
}
當呼叫 getc
函數時,會呼叫此方法。
sub GETC { print "Don't GETC, Get Perl"; return "a"; }
當呼叫 eof
函數時,會呼叫此方法。
從 Perl 5.12 開始,會傳遞一個額外的整數參數。如果未帶參數呼叫 eof
,則為零;如果 eof
給定檔案句柄作為參數,例如 eof(FH)
,則為 1
;在非常特殊的情況下,如果綁定的檔案句柄是 ARGV
且 eof
呼叫時參數清單為空,例如 eof()
,則為 2
。
sub EOF { not length $stringbuf }
當透過 close
函數關閉句柄時,會呼叫此方法。
sub CLOSE { print "CLOSE called.\n" }
與其他類型的繫結一樣,當發生 untie
時,會呼叫此方法。發生這種情況時,適當地「自動 CLOSE」會很恰當。請參閱以下「untie
Gotcha」。
與其他類型的繫結一樣,當綁定的句柄即將被銷毀時,會呼叫此方法。這對於除錯和可能的清理很有用。
sub DESTROY { print "</shout>\n" }
以下是使用我們小範例的方法
tie(*FOO,'Shout');
print FOO "hello\n";
$a = 4; $b = 6;
print FOO $a, " plus ", $b, " equals ", $a + $b, "\n";
print <FOO>;
您可以為所有繫結類型定義一個 UNTIE 方法,該方法會在 untie() 時呼叫。請參閱以下「untie
Gotcha」。
untie
Gotcha 如果您打算使用從 tie() 或 tied() 傳回的物件,而且繫結的目標類別定義了一個解構函數,則有一個微妙的 gotcha 您必須注意。
作為設定,請考慮這個(公認相當做作的)繫結範例;它所做的只是使用檔案來記錄指定給純量的值。
package Remember;
use v5.36;
use IO::File;
sub TIESCALAR {
my $class = shift;
my $filename = shift;
my $handle = IO::File->new( "> $filename" )
or die "Cannot open $filename: $!\n";
print $handle "The Start\n";
bless {FH => $handle, Value => 0}, $class;
}
sub FETCH {
my $self = shift;
return $self->{Value};
}
sub STORE {
my $self = shift;
my $value = shift;
my $handle = $self->{FH};
print $handle "$value\n";
$self->{Value} = $value;
}
sub DESTROY {
my $self = shift;
my $handle = $self->{FH};
print $handle "The End\n";
close $handle;
}
1;
以下是使用此繫結的範例
use strict;
use Remember;
my $fred;
tie $fred, 'Remember', 'myfile.txt';
$fred = 1;
$fred = 4;
$fred = 5;
untie $fred;
system "cat myfile.txt";
這是執行時的輸出
The Start
1
4
5
The End
到目前為止都很好。有在注意的人應該會發現,到目前為止都還沒有使用到綁定的物件。因此,讓我們在 Remember 類別中新增一個額外的方法,以允許在檔案中加入註解;比方說,像這樣
sub comment {
my $self = shift;
my $text = shift;
my $handle = $self->{FH};
print $handle $text, "\n";
}
以下是修改過的前一個範例,使用 comment
方法(需要綁定的物件)
use strict;
use Remember;
my ($fred, $x);
$x = tie $fred, 'Remember', 'myfile.txt';
$fred = 1;
$fred = 4;
comment $x "changing...";
$fred = 5;
untie $fred;
system "cat myfile.txt";
執行這段程式碼時,不會有任何輸出。原因如下
當一個變數被綁定時,它會與物件產生關聯,而這個物件是 TIESCALAR、TIEARRAY 或 TIEHASH 函式的傳回值。這個物件通常只有一個參照,也就是綁定變數的隱式參照。當呼叫 untie() 時,該參照會被銷毀。然後,就像上述第一個範例一樣,會呼叫物件的解構函式 (DESTROY),這對於沒有更多有效參照的物件來說是正常的;因此檔案會被關閉。
然而,在第二個範例中,我們在 $x 中儲存了另一個對綁定物件的參照。這表示當呼叫 untie() 時,仍然會有一個對物件的有效參照存在,因此當時不會呼叫解構函式,因此檔案不會被關閉。沒有輸出的原因是檔案緩衝區尚未刷新到磁碟。
現在您知道問題出在哪裡了,可以採取哪些措施來避免它?在引入可選的 UNTIE 方法之前,唯一的方法是古老的 -w
旗標。它會找出您呼叫 untie() 且仍有對綁定物件的有效參照的所有執行個體。如果在這個近頂部的第二個指令碼上方 use warnings 'untie'
或使用 -w
旗標執行,Perl 會印出此警告訊息
untie attempted while 1 inner references still exist
要讓指令碼正常運作並消除警告,請確保在呼叫 untie() 之前 沒有對綁定物件的有效參照
undef $x;
untie $fred;
現在有了 UNTIE,類別設計師可以決定類別功能的哪些部分實際上與 untie
相關,哪些部分與正在銷毀的物件相關。對於給定的類別來說,有意義的部分取決於是否保留內部參照,以便可以在物件上呼叫與繫結無關的方法。但在大多數情況下,將原本會放在 DESTROY 中的功能移到 UNTIE 方法中可能是比較合理的做法。
如果存在 UNTIE 方法,則不會出現以上警告。相反,UNTIE 方法會傳遞「額外」參考計數,並可在適當時發出自己的警告。例如,要複製沒有 UNTIE 的情況,可以使用這個方法
sub UNTIE
{
my ($obj,$count) = @_;
carp "untie attempted while $count inner references still exist"
if $count;
}
請參閱 DB_File 或 Config,以取得一些有趣的 tie() 實作。許多 tie() 實作的良好起點是其中一個模組 Tie::Scalar、Tie::Array、Tie::Hash 或 Tie::Handle。
scalar(%hash)
提供的正常回傳值不可用。這表示在布林文脈中使用 %tied_hash 不會正常運作(目前這總是測試為 false,無論 hash 是否為空或 hash 元素)。[這段落需要根據 5.25 中的變更進行檢閱]
無法對綁定的陣列或 hash 進行區域化。離開範圍後,不會還原陣列或 hash。
透過 scalar(keys(%hash))
或 scalar(values(%hash)
) 計算 hash 中的項目數量沒有效率,因為它需要使用 FIRSTKEY/NEXTKEY 遍歷所有項目。
綁定的 hash/陣列切片會造成多個 FETCH/STORE 配對,沒有切片操作的綁定方法。
無法輕易將多層級資料結構(例如 hash 的 hash)綁定到 dbm 檔案。第一個問題是,除了 GDBM 和 Berkeley DB 之外,所有其他資料庫都有大小限制,但除此之外,您還會有如何將參考表示在磁碟上的問題。一個嘗試滿足這個需求的模組是 DBM::Deep。請根據 perlmodlib 中的說明,查看您最近的 CPAN 網站以取得原始碼。請注意,儘管名稱為 DBM::Deep,但它並未使用 dbm。另一個較早嘗試解決此問題的方法是 MLDBM,它也可用於 CPAN,但有一些相當嚴重的限制。
綁定的檔案控制代碼仍不完整。目前無法攔截 sysopen()、truncate()、flock()、fcntl()、stat() 和 -X。
Tom Christiansen
TIEHANDLE 由 Sven Verdoolaege <skimo@dns.ufsia.ac.be> 和 Doug MacEachern <dougm@osf.org> 撰寫
UNTIE 由 Nick Ing-Simmons <nick@ing-simmons.net> 撰寫
SCALAR 由 Tassilo von Parseval <tassilo.von.parseval@rwth-aachen.de> 撰寫
Tying Arrays 由 Casey West <casey@geeknest.com> 撰寫