內容

名稱

Hash::Util::FieldHash - 支援反向類別

語法

### Create fieldhashes
use Hash::Util qw(fieldhash fieldhashes);

# Create a single field hash
fieldhash my %foo;

# Create three at once...
fieldhashes \ my(%foo, %bar, %baz);
# ...or any number
fieldhashes @hashrefs;

### Create an idhash and register it for garbage collection
use Hash::Util::FieldHash qw(idhash register);
idhash my %name;
my $object = \ do { my $o };
# register the idhash for garbage collection with $object
register($object, \ %name);
# the following entry will be deleted when $object goes out of scope
$name{$object} = 'John Doe';

### Register an ordinary hash for garbage collection
use Hash::Util::FieldHash qw(id register);
my %name;
my $object = \ do { my $o };
# register the hash %name for garbage collection of $object's id
register $object, \ %name;
# the following entry will be deleted when $object goes out of scope
$name{id $object} = 'John Doe';

函式

Hash::Util::FieldHash 提供許多函式來支援類別建構的 "反向技術"

id
id($obj)

傳回參考 $obj 的參考位址。如果 $obj 不是參考,則傳回 $obj。

這個函式是 Scalar::Util::refaddr 的替代品,也就是說,它將其引數的參考位址傳回為數字值。唯一的差異是 refaddr() 在給定非參考時傳回 undef,而 id() 則不變地傳回其引數。

id() 也使用快取技術,當物件的 id 經常被要求時,它會更快,但如果只使用一次或兩次,則會變慢。

id_2obj
$obj = id_2obj($id)

如果 $id 是已註冊物件的 id(請參閱 "register"),則傳回物件,否則傳回未定義的值。對於已註冊的物件,這是 id() 的反函式。

register
register($obj)
register($obj, @hashrefs)

在第一個形式中,為函數 id_2obj() 註冊一個物件以供使用。在第二個形式中,它另外標記給定的雜湊參考以進行垃圾回收。這表示當物件超出範圍時,給定雜湊中在 id($obj) 鍵下的任何項目都將從雜湊中刪除。

註冊非參考 $obj 是致命錯誤。以下引數中任何非雜湊參考都將被靜默略過。

使用不同的雜湊參考集多次註冊同一個物件並非錯誤。任何尚未註冊的雜湊參考都將被加入,其他則被略過。

註冊表也暗示執行緒支援。當建立新的執行緒時,所有參考都將被替換為新的參考,包括所有物件。如果雜湊使用物件的參考位址作為鍵,則該連線將會中斷。使用已註冊的物件時,其 id 將會更新在所有已註冊雜湊中。

idhash
idhash my %hash

從引數建立一個 idhash,該引數必須是雜湊。

idhash 的運作方式類似於一般雜湊,但它將用作鍵的參考字串化的方式不同。參考的字串化方式就像對它呼叫 id() 函數一樣,也就是說,其參考位址以十進位數字表示並用作鍵。

idhashes
idhashes \ my(%hash, %gnash, %trash)
idhashes \ @hashrefs

從其雜湊參考引數建立多個 idhash。傳回那些可以轉換的引數或其在純量內容中的數量。

fieldhash
fieldhash %hash;

建立單一 fieldhash。引數必須是雜湊。如果成功,則傳回給定雜湊的參考,否則不傳回任何內容。

簡單來說,fieldhash 是具有自動註冊功能的 idhash。當物件(或任何參考)用作 fieldhash 鍵時,fieldhash 會自動註冊為物件的垃圾回收,就像呼叫 register $obj, \ %fieldhash 一樣。

fieldhashes
fieldhashes @hashrefs;

建立任意數量的 field hash。引數必須是雜湊參考。在清單內容中傳回已轉換的雜湊參考,在純量內容中傳回其數量。

說明

關於術語:我將使用術語欄位來表示類別與物件關聯的標量資料。此概念的其他用詞包括「物件變數」、「(物件)屬性」、「(物件)特徵」等等。尤其是「特徵」在 Perl 程式設計師之間相當流行,但與 attributes pragma 衝突。術語「欄位」在此意義上也相當流行,且似乎不會與其他 Perl 術語衝突。

在 Perl 中,物件是受祝福的參照。與物件關聯資料的標準方式是將資料儲存在物件主體中,也就是參照所指向的資料片段。

因此,如果兩個或多個類別想要存取物件,它們必須同意參照類型,以及物件主體中資料的組織方式。如果不同意類型,則當錯誤的方法嘗試存取物件時,將導致立即死亡。如果不同意資料組織方式,則可能導致一個類別踐踏另一個類別的資料。

此物件模型導致子類別之間緊密結合。如果一個類別想要繼承另一個類別(且兩個類別都存取物件資料),則這些類別必須同意實作細節。繼承只能用於一起維護的類別,無論是在單一來源中或不在單一來源中。

特別是,無法使用此技術撰寫通用類別,也就是可以宣傳自己為「將我放入 @ISA 清單中並使用我的方法」的類別。如果其他類別對於如何使用物件主體有不同的想法,則會有問題。

「範例 1」 中的參照 Name_hash 顯示了使用眾所周知的基於雜湊方式實作簡單類別 Name 的標準實作方式。它也展示了建構 Name 和類別 IO::File(其物件必須是全域參照)的共用子類別 NamedFile 的可預測失敗。

因此,有興趣的技術是將物件資料儲存在不是物件主體的其他地方。

內外顛倒技術

使用內外顛倒類別時,每個類別會為它想要使用的每個欄位宣告一個(通常是字彙)雜湊。物件的參照位址用作雜湊金鑰。根據定義,參照位址對每個物件都是唯一的,因此這可以保證每個欄位都有屬於類別的私有位置,且對每個物件都是唯一的。請參閱 「範例 1」 中的 Name_id,以取得簡單範例。

與物件為雜湊且欄位對應到雜湊金鑰的標準實作方式相比,在此欄位對應到雜湊,而物件決定雜湊金鑰。因此雜湊看起來像是被內外顛倒

物件主體從未被內外顛倒類別檢查過,只使用它的參照位址。這允許實際物件的主體完全是任何東西,而類別的物件方法仍如設計般運作。這是內外顛倒類別的一項主要特色。

內外顛倒的問題

由內而外的類別給予我們繼承的自由,但一如往常,這是有代價的。

最明顯的是,有必要為每個資料存取擷取物件的參考位址。這是一個小不便,但它確實會使程式碼雜亂。

更重要的是(且較不顯而易見)垃圾收集的必要性。當一個正常的物件結束時,儲存在物件主體中的任何東西都會被 Perl 垃圾收集。對於由內而外的物件,Perl 對於由類別儲存在欄位雜湊中的資料一無所知,但這些資料必須在物件超出範圍時刪除。因此,類別必須提供一個 DESTROY 方法來處理這件事。

在存在多個類別的情況下,確保每個相關的解構函式都針對每個物件呼叫並非易事。Perl 會呼叫它在繼承樹上找到的第一個(如果有的話),僅此而已。

相關問題是執行緒安全性。當建立一個新執行緒時,Perl 解譯器會被複製,這意味著使用中的所有參考位址都會被新的位址取代。因此,如果一個類別嘗試存取複製物件的欄位,它的(複製)資料仍會儲存在父執行緒中原始物件現在無效的參考位址下。必須提供一個一般的 CLONE 方法來重新建立關聯。

解決方案

Hash::Util::FieldHash 在幾個層面上解決了這些問題。

除了現有的 Scalar::Util::refaddr() 之外,還提供了 id() 函式。除了名稱簡短之外,在某些情況下它可以快一點(而在其他情況下則慢一點)。如果重要,請進行基準測試。id() 的運作方式還允許將類別名稱用作 進一步說明一般物件

id() 函式會以 id 雜湊的形式整合,意即它會自動對雜湊中使用的每個金鑰呼叫。不需要明確呼叫。

register() 函式同時解決了垃圾收集和執行緒安全性的問題。它會註冊一個物件以及任意數量的雜湊。註冊表示當物件結束時,這個物件的參考位址下的任何雜湊中的條目都會被刪除。這保證了這些雜湊中的垃圾收集。這也表示在執行緒複製時,物件在已註冊雜湊中的條目會被替換為金鑰為複製物件的參考位址的更新條目。因此,物件資料關聯會變成執行緒安全的。

最好在物件初始化以供類別使用時進行物件註冊。這樣,垃圾收集和執行緒安全性會針對每個物件和每個已初始化的欄位建立。

最後,欄位雜湊將所有這些函式整合到一個套件中。除了自動對用作金鑰的每個物件呼叫 id() 函式之外,物件會在第一次使用時註冊到欄位雜湊。基於欄位雜湊的類別會完全垃圾收集且執行緒安全,而不需要進一步的措施。

更多問題

使用由內而外類別發生的另一個問題是序列化。由於物件資料並非位於其通常位置,因此標準常式(如 Storable::freeze()Storable::thaw()Data::Dumper::Dumper())無法自行處理。Data::DumperStorable 都提供必要的掛鉤以讓事情運作,但掛鉤所使用的函式或方法必須由每個由內而外類別提供。

序列化問題的通用解決方案需要另一層註冊,一個將類別和欄位關聯起來的註冊。到目前為止,Hash::Util::FieldHash 的函式並不知道任何類別,我認為這是一個特色。因此,Hash::Util::FieldHash 並未解決序列化問題。

通用物件

基於 id() 函式的類別(以及因此基於 idhash()fieldhash() 的類別)顯示出一個特殊行為,其中類別名稱可以使用為物件。具體來說,設定或讀取與物件相關聯資料的方法會繼續作為類別方法運作,就像類別名稱是一個物件,與所有其他物件不同,擁有自己的資料。此物件可以稱為類別的「通用物件」。

這會運作是因為欄位雜湊會回應非參考的鍵,就像一般的雜湊一樣,並使用提供為雜湊鍵的字串。因此,如果方法以類別方法呼叫,則欄位雜湊會顯示類別名稱,而不是物件,並輕鬆地將其用作鍵。由於真實物件的鍵是小數數字,因此不會發生衝突,欄位雜湊中的插槽可以使用為任何其他插槽。id() 函式會對非參考引數表現出相應行為。

除了忽略屬性之外,可能會想到兩個可能的用途。單例類別可以使用通用物件實作。如有必要,init() 方法可以終止或忽略帶有實際物件(參考)的呼叫,因此只有通用物件會存在。

通用物件的另一種用途是作為範本。這是一個方便的地方,用於儲存實際物件初始化中使用的各種欄位的類別特定預設值。

通常,可以完全忽略此功能。將「物件方法」呼叫為「類別方法」通常會導致錯誤,而且不會在任何地方例行使用。這可能是一個問題,因為具有通用物件的類別不會指出此錯誤。

如何使用欄位雜湊

傳統上,由內而外類別的定義包含一個空區塊,在其中宣告多個詞彙雜湊,並定義基本存取器方法,通常透過 Scalar::Util::refaddr。可以在此區塊外部定義更多方法。必須有一個 DESTROY 方法,而且對於執行緒支援,必須有一個 CLONE 方法。

當使用欄位雜湊時,基本結構保持不變。每個詞彙雜湊將成為一個欄位雜湊。可以從存取器方法中省略對 refaddr 的呼叫。不需要 DESTROY 和 CLONE 方法。

如果您有一個現有的由內而外類別,只需將所有雜湊設為欄位雜湊,而沒有其他變更,就不會有差別。透過呼叫 refaddr 或等效呼叫,欄位雜湊永遠不會看到參考,並像一般雜湊一樣運作。您的 DESTROY(和 CLONE)方法仍然需要。

要讓欄位雜湊開始作用,最簡單的方法就是重新定義 refaddr

sub refaddr { shift }

而不是從 Scalar::Util 匯入。現在應該可以停用 DESTROY 和 CLONE。請注意,在停用之前,DESTROY 會在欄位雜湊的垃圾回收之前被呼叫,因此它會使用一個功能物件被呼叫,並會繼續運作。

不建議將函式 fieldhash 和/或 fieldhashes 匯入到每個將要使用它們的類別中。它們只會被使用一次來設定類別。當類別啟動並執行時,這些函式就不再有用途了。

如果只有少數幾個欄位雜湊要宣告,最簡單的方法是

use Hash::Util::FieldHash;

提早呼叫函式

Hash::Util::FieldHash::fieldhash my %foo;

否則,將函式匯入到一個方便的套件中,例如 HUF 或更通用的 Aux

{
    package Aux;
    use Hash::Util::FieldHash ':all';
}

並呼叫

Aux::fieldhash my %foo;

視需要而定。

垃圾回收雜湊

欄位雜湊中的垃圾回收表示當建立它們的物件消失時,這些條目會「自動」消失。必須記住這一點,特別是在對欄位雜湊進行迴圈時。如果你在迴圈中執行的任何動作都可能導致物件超出範圍,則會從你正在迴圈的雜湊中刪除一個隨機金鑰。這可能會拋出迴圈迭代器,因此最好快取金鑰和/或值的持續快照,並對其進行迴圈。你仍然必須檢查快取條目在你取得它時仍然存在。

當金鑰在欄位雜湊中從一般純量以及參照建立時,垃圾回收可能會令人困惑。一旦參照與欄位雜湊一起使用,就會收集條目,即使它稍後被純量金鑰覆寫(每個正整數都是候選)。即使這時原始條目已被刪除,情況也是如此。事實上,從欄位雜湊中刪除,以及存在性測試構成此意義上的使用,並在參照超出範圍時建立刪除條目的負債。如果你碰巧使用字串或整數建立具有相同金鑰的條目,則會收集該條目。因此,將參照和純量混合用作欄位雜湊金鑰並非完全受支援。

範例

範例顯示一個非常簡單的類別,實作一個 名稱,包含名字和姓氏(沒有中間名)。名稱類別有四個方法

範例顯示此類別實作了 Hash::Util::FieldHash 不同層級的支援。所有支援的組合都已顯示。實作之間的差異通常很小。實作如下

這些範例在以下程式碼中實現,可以複製到檔案 Example.pm 中。

範例 1

use strict; use warnings;

{
    package Name_hash;  # standard implementation: the
                        # object is a hash
    sub init {
        my $obj = shift;
        my ($first, $last) = @_;
        # create an object if called as class method
        $obj = bless {}, $obj unless ref $obj;
        $obj->{ first} = $first;
        $obj->{ last} = $last;
        $obj;
    }

    sub first { shift()->{ first} }
    sub last { shift()->{ last} }

    sub name {
        my $n = shift;
        join ' ' => $n->first, $n->last;
    }

}

{
    package Name_id;
    use Hash::Util::FieldHash qw(id);

    my (%first, %last);

    sub init {
        my $obj = shift;
        my ($first, $last) = @_;
        # create an object if called as class method
        $obj = bless \ my $o, $obj unless ref $obj;
        $first{ id $obj} = $first;
        $last{ id $obj} = $last;
        $obj;
    }

    sub first { $first{ id shift()} }
    sub last { $last{ id shift()} }

    sub name {
        my $n = shift;
        join ' ' => $n->first, $n->last;
    }

    sub DESTROY {
        my $id = id shift;
        delete $first{ $id};
        delete $last{ $id};
    }

}

{
    package Name_idhash;
    use Hash::Util::FieldHash;

    Hash::Util::FieldHash::idhashes( \ my (%first, %last) );

    sub init {
        my $obj = shift;
        my ($first, $last) = @_;
        # create an object if called as class method
        $obj = bless \ my $o, $obj unless ref $obj;
        $first{ $obj} = $first;
        $last{ $obj} = $last;
        $obj;
    }

    sub first { $first{ shift()} }
    sub last { $last{ shift()} }

    sub name {
        my $n = shift;
        join ' ' => $n->first, $n->last;
    }

    sub DESTROY {
        my $n = shift;
        delete $first{ $n};
        delete $last{ $n};
    }

}

{
    package Name_id_reg;
    use Hash::Util::FieldHash qw(id register);

    my (%first, %last);

    sub init {
        my $obj = shift;
        my ($first, $last) = @_;
        # create an object if called as class method
        $obj = bless \ my $o, $obj unless ref $obj;
        register( $obj, \ (%first, %last) );
        $first{ id $obj} = $first;
        $last{ id $obj} = $last;
        $obj;
    }

    sub first { $first{ id shift()} }
    sub last { $last{ id shift()} }

    sub name {
        my $n = shift;
        join ' ' => $n->first, $n->last;
    }
}

{
    package Name_idhash_reg;
    use Hash::Util::FieldHash qw(register);

    Hash::Util::FieldHash::idhashes \ my (%first, %last);

    sub init {
        my $obj = shift;
        my ($first, $last) = @_;
        # create an object if called as class method
        $obj = bless \ my $o, $obj unless ref $obj;
        register( $obj, \ (%first, %last) );
        $first{ $obj} = $first;
        $last{ $obj} = $last;
        $obj;
    }

    sub first { $first{ shift()} }
    sub last { $last{ shift()} }

    sub name {
        my $n = shift;
        join ' ' => $n->first, $n->last;
    }
}

{
    package Name_fieldhash;
    use Hash::Util::FieldHash;

    Hash::Util::FieldHash::fieldhashes \ my (%first, %last);

    sub init {
        my $obj = shift;
        my ($first, $last) = @_;
        # create an object if called as class method
        $obj = bless \ my $o, $obj unless ref $obj;
        $first{ $obj} = $first;
        $last{ $obj} = $last;
        $obj;
    }

    sub first { $first{ shift()} }
    sub last { $last{ shift()} }

    sub name {
        my $n = shift;
        join ' ' => $n->first, $n->last;
    }
}

1;

若要執行各種實作,可以使用 以下 這個指令碼。

它設定一個類別 Name,為實作類別之一 Name_hashName_id、...、Name_fieldhash 的鏡像。這會決定要執行哪個實作。

指令碼會先驗證 Name 類別的功能。

在第二個步驟中,展示了實作的自由繼承性(或缺乏繼承性)。為了這個目的,它建構了一個名為 NamedFile 的類別,它是 Name 和標準類別 IO::File 的共同子類別。這對繼承性進行了測試,因為 IO::File 的物件必須是 globref。NamedFile 的物件應表現得像開啟用於讀取的檔案,並支援 name() 方法。這個類別交會處運作,但 Name_hash 實作除外,在該實作中,物件初始化會因為物件主體不相容而失敗。

範例 2

use strict; use warnings; $| = 1;

use Example;

{
    package Name;
    use parent 'Name_id';  # define here which implementation to run
}


# Verify that the base package works
my $n = Name->init(qw(Albert Einstein));
print $n->name, "\n";
print "\n";

# Create a named file handle (See definition below)
my $nf = NamedFile->init(qw(/tmp/x Filomena File));
# use as a file handle...
for ( 1 .. 3 ) {
    my $l = <$nf>;
    print "line $_: $l";
}
# ...and as a Name object
print "...brought to you by ", $nf->name, "\n";
exit;


# Definition of NamedFile
package NamedFile;
use parent 'Name';
use parent 'IO::File';

sub init {
    my $obj = shift;
    my ($file, $first, $last) = @_;
    $obj = $obj->IO::File::new() unless ref $obj;
    $obj->open($file) or die "Can't read '$file': $!";
    $obj->Name::init($first, $last);
}
__END__

GUTS

為了讓 Hash::Util::FieldHash 運作,對 perl 本身進行了兩項變更。PERL_MAGIC_uvar 已可供雜湊使用,而弱參考現在在弱參考清除後呼叫 uvar get 魔法。第一個功能用於讓欄位雜湊在存取時攔截其鍵。第二個功能會觸發垃圾回收。

雜湊的 PERL_MAGIC_uvar 介面

PERL_MAGIC_uvar get 魔法會從 hv_fetch_commonhv_delete_common 透過函數 hv_magic_uvar_xkey 呼叫,該函數定義了介面。如果 ufuncs 結構在 uf_valuf_set 欄位中具有相等的值,則會對具有「uvar」魔法的雜湊進行呼叫。如果(並且只要)這些欄位包含不同的值,則雜湊不受影響。

在呼叫時,mg_obj 欄位將包含要存取的雜湊鍵。在回傳時,mg_obj 中的 SV* 值將用於取代雜湊存取中原始的鍵。第一個參數中的整數索引值將是來自 hv_fetch_commonaction 值,或者如果呼叫來自 hv_delete_common,則為 -1。

這是適合此呼叫中 ufuncs 結構的 uf_val 欄位的函數範本。uf_setuf_index 欄位無關緊要。

IV watch_key(pTHX_ IV action, SV* field) {
    MAGIC* mg = mg_find(field, PERL_MAGIC_uvar);
    SV* keysv = mg->mg_obj;
    /* Do whatever you need to.  If you decide to
       supply a different key newkey, return it like this
    */
    sv_2mortal(newkey);
    mg->mg_obj = newkey;
    return 0;
}

弱參考呼叫 uvar 魔法

當弱參考儲存在具有「uvar」魔法的 SV 中時,在參考變為過時後,會呼叫 set 魔法。此掛鉤可用於觸發與參考物件相關的進一步垃圾回收活動。

欄位雜湊運作方式

鍵雜湊的三個功能,鍵替換執行緒支援垃圾回收,由稱為物件註冊的資料結構支援。這是一個私有雜湊,其中儲存每個物件。在此意義上,「物件」是指任何已用作欄位雜湊鍵的參考(已祝福或未祝福)。

物件登錄追蹤已用作欄位雜湊金鑰的參考。金鑰是由參考位址產生,就像在欄位雜湊中一樣(儘管登錄不是欄位雜湊)。每個值都是原始參考的弱複本,儲存在本身就是神奇的 SV 中(再次為 PERL_MAGIC_uvar)。神奇結構包含參考已用過的欄位雜湊清單(實際上是另一個雜湊)。當弱參考變為過時,神奇結構會被啟動,並使用清單從所有已用過的欄位雜湊中刪除參考。之後,項目會從物件登錄中移除。隱含地,這會釋放神奇結構及其已使用的儲存空間。

每當參考用作欄位雜湊金鑰時,就會檢查物件登錄,並在必要時建立新項目。然後將欄位雜湊新增至這個參考已使用的欄位清單中。

物件登錄也用於在執行緒複製後修復欄位雜湊。在此,會處理整個物件登錄。對於在那裡找到的每個參考,都會拜訪它已使用的欄位雜湊,並更新項目。

內部函式 Hash::Util::FieldHash::_fieldhash

# test if %hash is a field hash
my $result = _fieldhash \ %hash, 0;

# make %hash a field hash
my $result = _fieldhash \ %hash, 1;

_fieldhash 是用於建立欄位雜湊的內部函式。它需要兩個引數,一個雜湊參照和一個模式。如果模式為布林值 false,則不會變更雜湊,但會測試它是否為欄位雜湊。如果雜湊不是欄位雜湊,則傳回值為布林值 false。如果是,則傳回值會指出欄位雜湊的模式。當以布林值 true 模式呼叫時,它會將給定的雜湊轉換為此模式的欄位雜湊,傳回已建立欄位雜湊的模式。_fieldhash 不會清除給定的雜湊。

目前只有一種類型的欄位雜湊,而且只有模式的布林值會產生差異,但這可能會變更。

作者

Anno Siegel (ANNO) 撰寫 xs 程式碼,而 perl proper 中的變更由 Jerry Hedden (JDHEDDEN) 撰寫,使它更快

著作權和授權

著作權 (C) 2006-2007 by (Anno Siegel)

此函式庫為免費軟體;您可以根據 Perl 本身的條款重新散布或修改它,Perl 版本 5.8.7 或您可取得的任何較新 Perl 5 版本,由您選擇。