内容

名称

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;

创建任意数量的 fieldhash。参数必须是哈希引用。在列表上下文中返回转换后的哈希引用,在标量上下文中返回它们的数量。

DESCRIPTION

关于术语的一句话:我将使用术语字段来表示类与对象关联的标量数据片段。用于描述此概念的其他术语包括“对象变量”、“(对象)属性”、“(对象)属性”等等。特别是“属性”在 Perl 程序员中有一定的流行度,但它与 attributes 准则冲突。术语“字段”在这个意义上也有一定的流行度,并且似乎没有与其他 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;

为了练习各种实现,可以使用下面的脚本 below

它设置了一个名为 Name 的类,它是实现类 Name_hashName_id、...、Name_fieldhash 之一的镜像。这决定了运行哪个实现。

该脚本首先验证 Name 类的功能。

在第二步中,演示了实现的自由继承性(或缺乏继承性)。为此,它构造了一个名为 NamedFile 的类,它是 Name 类和标准类 IO::File 的公共子类。这将继承性置于测试中,因为 IO::File 的对象必须是全局引用。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_magic_uvar_xkeyhv_fetch_commonhv_delete_common调用,该函数定义了接口。如果ufuncs结构在uf_valuf_set字段中具有相等的值,则对具有“uvar”魔法的哈希进行调用。如果(以及只要)这些字段包含不同的值,哈希将不受影响。

在调用时,mg_obj字段将保存要访问的哈希键。在返回时,mg_obj中的SV*值将用于替换哈希访问中原始键。第一个参数中的整数索引值将是hv_fetch_common中的action值,或者如果调用来自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中,该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 代码,Jerry Hedden (JDHEDDEN) 在 perl 本身中进行了更改,使其速度更快

版权和许可

版权所有 (C) 2006-2007 (Anno Siegel)

此库是免费软件;您可以根据与 Perl 本身相同的条款重新分发它和/或修改它,无论是 Perl 版本 5.8.7 还是,根据您的选择,您可能拥有的任何更高版本的 Perl 5。