目錄

名稱

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;
TIESCALAR 類別名稱,清單

這是類別的建構函式。這表示預期會傳回對它正在建立的新純量(可能是匿名)的附魔參考。例如

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,以查看是否要發出一些雜訊。

FETCH this

每次存取附魔變數(讀取)時,都會觸發此方法。除了 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;
TIEARRAY 類別名稱,LIST

這是類別的建構函式。這表示它預期會傳回一個受祝福的參考,新的陣列(可能是匿名 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;
}
FETCH this, index

每次存取(讀取)綁定的陣列中的個別元素時,都會觸發這個方法。它會取得一個自參考以外的引數:我們嘗試擷取其值的索引。

sub FETCH {
  my $self  = shift;
  my $index = shift;
  return $self->{ARRAY}->[$index];
}

如果使用負陣列索引從陣列讀取,索引會在傳遞至 FETCH 之前,透過呼叫 FETCHSIZE 轉換成正值。您可以在綁定陣列類別中將變數 $NEGATIVE_INDICES 指定為 true 值,以停用這個功能。

您可能已經注意到,FETCH 方法(等)的名稱對所有存取都是相同的,即使建構函式的名稱不同(TIESCALAR 與 TIEARRAY)。雖然理論上您可以讓同一個類別提供服務給多種綁定類型,但實際上這會很麻煩,而且最簡單的方法是讓每個類別只綁定一種類型。

STORE this, index, value

每次設定(寫入)綁定陣列中的元素時,都會觸發這個方法。它會取得兩個自參考以外的引數:我們嘗試儲存資料的索引,以及我們嘗試放入其中的值。

在我們的範例中,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 相同。

FETCHSIZE this

傳回與物件 this 關聯的綁定陣列中項目的總數目。(等同於 scalar(@array))。例如

sub FETCHSIZE {
  my $self = shift;
  return scalar $self->{ARRAY}->@*;
}
STORESIZE this, count

將與物件 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();
    }
  }
}
EXTEND this, count

資訊呼叫,陣列可能會長到有 count 個項目。可用於最佳化配置。此方法不需要做任何事。

在我們的範例中,沒有理由實作此方法,所以我們讓它成為空操作。此方法僅與綁定陣列實作相關,其中有可能性讓陣列的配置大小大於 Perl 程式設計師檢查陣列大小時所看到的。許多綁定陣列實作沒有理由實作它。

sub EXTEND {   
  my $self  = shift;
  my $count = shift;
  # nothing to see here, move along.
}

注意:通常讓這等同於 STORESIZE 是個錯誤。Perl 可能會偶爾呼叫 EXTEND,而不想要實際直接變更陣列大小。如果此方法是空操作,任何綁定陣列都應該能正確運作,即使它們可能不會像實作此方法時一樣有效率。

EXISTS this, key

驗證綁定陣列 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;
}
DELETE this, key

從綁定陣列 this 中刪除索引 key 處的元素。

在我們的範例中,已刪除的項目是 $self->{ELEMSIZE} 個空白

sub DELETE {
  my $self  = shift;
  my $index = shift;
  return $self->STORE( $index, '' );
}
清除 this

清除 (移除、刪除、...) 與物件 this 相關聯的繫結陣列中的所有值。例如

sub CLEAR {
  my $self = shift;
  return $self->{ARRAY} = [];
}
PUSH this, LIST

LIST 的元素附加到陣列。例如

sub PUSH {  
  my $self = shift;
  my @list = @_;
  my $last = $self->FETCHSIZE();
  $self->STORE( $last + $_, $list[$_] ) foreach 0 .. $#list;
  return $self->FETCHSIZE();
}   
POP this

移除陣列的最後一個元素並傳回。例如

sub POP {
  my $self = shift;
  return pop $self->{ARRAY}->@*;
}
SHIFT this

移除陣列的第一個元素 (將其他元素向下移動) 並傳回。例如

sub SHIFT {
  my $self = shift;
  return shift $self->{ARRAY}->@*;
}
UNSHIFT this, LIST

在陣列的開頭插入 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 this, offset, length, 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 this

將在發生 untie 時呼叫。(請參閱下方的「untie Gotcha」)

DESTROY this

當繫結變數需要被銷毀時,將觸發此方法。與純量繫結類別一樣,這在會執行自己的垃圾回收的語言中幾乎不需要,所以這次我們將省略它。

繫結雜湊

雜湊是第一個被繫結的 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} 欄位會是使用者認為的真實雜湊。

USER

此物件所代表的點檔案

HOME

這些點檔案所在的位置

CLOBBER

我們是否應該嘗試變更或移除這些點檔案

LIST

點檔案名稱和內容對應的雜湊

以下是 Dotfiles.pm 的開頭

package DotFiles;
use Carp;
sub whowasi { (caller(1))[3] . '()' }
my $DEBUG = 0;
sub debug { $DEBUG = @_ ? shift : 1 }

對於我們的範例,我們希望能夠發出除錯資訊,以協助在開發期間進行追蹤。我們也保留一個方便的函數在內部,以協助印出警告;whowasi() 會傳回呼叫它的函數名稱。

以下是 DotFiles 繫結雜湊的方法。

TIEHASH classname, LIST

這是類別的建構函式。這表示它預期會傳回一個經過祝福的參考,新的物件(可能是但未必是一個匿名雜湊)會透過它來存取。

以下是建構函式

    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(),它會測試錯誤的檔案。

FETCH this, key

每次存取繫結雜湊中的元素(讀取)時,都會觸發這個方法。除了 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 概念,因此我們不必太擔心。

STORE 這個,金鑰,值

這個方法會在綁定雜湊中的元素被設定(寫入)時觸發。除了自參考外,它會接收兩個參數:我們嘗試儲存內容的索引,以及我們嘗試放入該索引的值。

在我們的 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 這個,金鑰

當我們從雜湊中移除元素時,這個方法會被觸發,通常是使用 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 會為這個金鑰回傳的任何內容。在這個範例中,我們選擇回傳一個值,告訴呼叫者檔案是否已成功刪除。

CLEAR 這個

當整個雜湊要被清除時,這個方法會被觸發,通常是透過將空清單指定給它。

在我們的範例中,那會移除使用者所有的點檔案!這是一件非常危險的事,他們必須將 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 這個,金鑰

當使用者對特定雜湊使用 exists() 函式時,這個方法會被觸發。在我們的範例中,我們會查看 {LIST} 雜湊元素

    sub EXISTS   {
	carp &whowasi if $DEBUG;
	my $self = shift;
	my $dot = shift;
	return exists $self->{LIST}->{$dot};
    }
FIRSTKEY 這個

當使用者要透過雜湊進行反覆運算時,這個方法會被觸發,例如透過 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。

NEXTKEY 這個,最後一個金鑰

此方法會在 keys()、values() 或 each() 迭代期間觸發。它有一個第二個引數,是最後存取的鍵。如果您關心順序或從多個序列呼叫迭代器,或沒有真正將項目儲存在雜湊中,這會很有用。

NEXTKEY 始終在標量內容中呼叫,它應只傳回下一個鍵。values() 和 each() 在清單內容中,會呼叫 FETCH 以取得傳回的鍵。

對於我們的範例,我們使用真正的雜湊,所以我們只會執行簡單的動作,但我們必須間接透過 LIST 欄位。

    sub NEXTKEY  {
	carp &whowasi if $DEBUG;
	my $self = shift;
	return each $self->{LIST}->%*
    }

如果繫結雜湊底層的物件不是真正的雜湊,而且您沒有可用的 each,那麼在到達鍵清單尾端時,您應傳回 undef 或空清單。請參閱 each 的自訂文件 以取得更多詳細資料。

SCALAR this

當雜湊在標量內容中評估時,以及在 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 this

當發生 untie 時,會呼叫此方法。請參閱下方的 "untie Gotcha"

DESTROY this

當繫結雜湊即將超出範圍時,會觸發此方法。除非您嘗試新增除錯或有輔助狀態要清除,否則您不需要它。以下是極為簡單的函式

    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),請使用 *$handletie $handle 繫結純量變數 $handle,而不是其內部的句柄。

在我們的範例中,我們將建立一個大聲的句柄。

package Shout;
TIEHANDLE 類別名稱、清單

這是類別的建構函式。這表示它預期會傳回某種形式的祝福參考。參考可以用來儲存一些內部資訊。

sub TIEHANDLE { print "<shout>\n"; my $i; bless \$i, shift }
WRITE this、清單

當透過 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 this、清單

每當使用 printf() 函式列印繫結的句柄時,將會觸發此方法。除了其自我參考之外,它也預期傳遞給 printf 函式的格式和清單。

sub PRINTF {
    shift;
    my $fmt = shift;
    print sprintf($fmt, @_);
}
READ this、清單

當透過 readsysread 函式從句柄讀取時,將會呼叫此方法。

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;
}
READLINE this

當透過 <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 this

當呼叫 getc 函數時,會呼叫此方法。

sub GETC { print "Don't GETC, Get Perl"; return "a"; }
EOF this

當呼叫 eof 函數時,會呼叫此方法。

從 Perl 5.12 開始,會傳遞一個額外的整數參數。如果未帶參數呼叫 eof,則為零;如果 eof 給定檔案句柄作為參數,例如 eof(FH),則為 1;在非常特殊的情況下,如果綁定的檔案句柄是 ARGVeof 呼叫時參數清單為空,例如 eof(),則為 2

sub EOF { not length $stringbuf }
CLOSE this

當透過 close 函數關閉句柄時,會呼叫此方法。

sub CLOSE { print "CLOSE called.\n" }
UNTIE this

與其他類型的繫結一樣,當發生 untie 時,會呼叫此方法。發生這種情況時,適當地「自動 CLOSE」會很恰當。請參閱以下「untie Gotcha」。

DESTROY this

與其他類型的繫結一樣,當綁定的句柄即將被銷毀時,會呼叫此方法。這對於除錯和可能的清理很有用。

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 this

您可以為所有繫結類型定義一個 UNTIE 方法,該方法會在 untie() 時呼叫。請參閱以下「untie Gotcha」。

The 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_FileConfig,以取得一些有趣的 tie() 實作。許多 tie() 實作的良好起點是其中一個模組 Tie::ScalarTie::ArrayTie::HashTie::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> 撰寫