内容

名称

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() 调用中,VARIABLE 是要被增强的变量的名称。CLASSNAME 是实现正确类型对象的类的名称。LIST 中的任何其他参数都会传递给该类的相应构造函数方法 - 意味着 TIESCALAR()、TIEARRAY()、TIEHASH() 或 TIEHANDLE()。(通常这些是可能传递给 C 的 dbminit() 函数的参数。)由“new”方法返回的对象也会被 tie() 函数返回,这在您想要访问 CLASSNAME 中的其他方法时会很有用。(您实际上不必返回对正确“类型”(例如,HASH 或 CLASSNAME)的引用,只要它是经过适当祝福的对象即可。)您也可以使用 tied() 函数检索对底层对象的引用。

与 dbmopen() 不同,tie() 函数不会为您 userequire 模块 - 您需要自己显式地执行此操作。

绑定标量

实现绑定标量的类应该定义以下方法:TIESCALAR、FETCH、STORE,以及可能还有 UNTIE 和/或 DESTROY。

让我们依次看看它们,以一个用于标量的绑定类为例,它允许用户执行类似的操作

tie $his_speed, 'Nice', getppid();
tie $my_speed,  'Nice', $$;

现在,每当访问这两个变量中的任何一个时,都会检索并返回其当前系统优先级。如果设置了这些变量,则会更改进程的优先级!

我们将使用 Jarkko Hietaniemi <[email protected]> 的 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 类名,LIST

这是类的构造函数。这意味着它应该返回对它正在创建的新标量(可能是匿名的)的祝福引用。例如

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 此

每次访问(读取)绑定变量时,都会触发此方法。除了自身引用之外,它不接受任何参数,自身引用是表示我们正在处理的标量的对象。因为在这种情况下,我们只使用 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: $!";
    }
}
解除绑定此

当发生 untie 时,将触发此方法。如果类需要知道何时不再进行调用,这将很有用。(当然,除了 DESTROY。)有关更多详细信息,请参阅下面的 "untie 陷阱"

销毁此

当需要销毁绑定变量时,将触发此方法。与其他对象类一样,这种方法很少需要,因为 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 classname, 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];
}

如果使用负数组索引从数组中读取,则索引将在内部通过调用 FETCHSIZE 转换为正索引,然后再传递给 FETCH。您可以在绑定数组类中将变量 $NEGATIVE_INDICES 赋值为真值来禁用此功能。

您可能已经注意到,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, '' );
}
CLEAR 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 发生时将被调用。(参见下面 "The 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

每次访问绑定哈希中的元素(读取)时,都会触发此方法。它除了自身引用之外,还接受一个参数:我们要获取其值的键。

以下是 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 this, key, value

每次在绑定哈希中设置(写入)元素时,都会触发此方法。除了自身引用之外,它还接受两个参数:我们要尝试存储内容的索引和我们要尝试放置的值。

在我们的 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 this, key

当我们从哈希中删除元素时,通常使用 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 this

当要清除整个哈希时,通常是将空列表分配给它,会触发此方法。

在我们的示例中,这将删除用户的所有点文件!这是一件非常危险的事情,他们必须将 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 this, key

当用户对特定哈希使用 exists() 函数时,会触发此方法。在我们的示例中,我们将查看此处的 {LIST} 哈希元素

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

当用户要遍历哈希时,例如通过 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 this, lastkey

此方法在 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 返回空列表,则结果为假值,否则为真值。

但是,您不应该盲目依赖 perl 始终做正确的事情。特别是,当您通过反复调用 DELETE 直到它为空来清除哈希时,perl 会错误地返回 true。因此,建议您在想要绝对确保您的哈希在标量上下文中表现良好时,提供自己的 SCALAR 方法。

在我们的示例中,我们可以直接对 $self->{LIST} 引用的底层哈希调用 scalar

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

注意:在 Perl 5.25 中,未绑定哈希上的标量 %hash 的行为已更改为返回键的数量。在此之前,它返回一个包含有关哈希桶设置信息的字符串。有关向后兼容路径,请参阅 "Hash::Util 中的 bucket_ratio"

解除绑定

当发生 untie 时,会调用此方法。请参阅下面 "untie 陷阱"

销毁

当绑定哈希即将超出范围时,会触发此方法。除非您要添加调试或清理辅助状态,否则您实际上不需要它。这是一个非常简单的函数

    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。如果在句柄上使用相应的 Perl 运算符,该类还可以提供:BINMODE、OPEN、EOF、FILENO、SEEK、TELL。

当 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()函数将绑定句柄打印到时,将触发此方法。除了自身引用之外,它还期望传递给printf函数的格式和列表。

sub PRINTF {
    shift;
    my $fmt = shift;
    print sprintf($fmt, @_);
}
读取此内容,列表

当通过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;
}
读取一行此内容

当通过<HANDLE>readline HANDLE从句柄中读取时,将调用此方法。

根据readline,在标量上下文中,它应该返回下一行,或者在没有更多数据时返回undef。在列表上下文中,它应该返回所有剩余的行,或者在没有更多数据时返回一个空列表。返回的字符串应包含输入记录分隔符$/(参见perlvar),除非它是undef(这意味着“全部读取”模式)。

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 this

与其他类型的绑定一样,当发生untie时,将调用此方法。在这种情况下,可能需要“自动关闭”。请参阅下面的"The 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() 时被调用。请参阅下面的"The untie Gotcha"

The untie Gotcha

如果您打算使用从 tie() 或 tied() 返回的对象,并且绑定的目标类定义了析构函数,那么您必须注意一个微妙的陷阱。

作为设置,请考虑这个(不可否认相当人为的)绑定示例;它所做的只是使用一个文件来记录分配给标量的值。

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 相关,哪些与对象被销毁相关。对于给定类来说,什么才是合理的,取决于内部引用是否被保留,以便在对象上调用与 tie 无关的方法。但在大多数情况下,将原本在 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 无法正常工作(目前,这始终测试为假,无论哈希是否为空或哈希元素)。[ 此段需要根据 5.25 中的更改进行审查 ]

本地化绑定的数组或哈希不起作用。退出范围后,数组或哈希不会恢复。

通过 scalar(keys(%hash))scalar(values(%hash)) 计算哈希中条目的数量效率低下,因为它需要使用 FIRSTKEY/NEXTKEY 遍历所有条目。

绑定的哈希/数组切片会导致多个 FETCH/STORE 对,没有用于切片操作的 tie 方法。

您无法轻松地将多级数据结构(例如哈希的哈希)绑定到 dbm 文件。第一个问题是,除了 GDBM 和 Berkeley DB 之外,所有 dbm 都有大小限制,但除此之外,您还遇到了如何在磁盘上表示引用的问题。一个试图解决此需求的模块是 DBM::Deep。如 perlmodlib 中所述,请查看您附近的 CPAN 站点以获取源代码。请注意,尽管名称如此,DBM::Deep 并没有使用 dbm。另一个较早的解决此问题的尝试是 MLDBM,它也可以在 CPAN 上获得,但它有一些相当严重的限制。

绑定文件句柄仍不完整。目前无法捕获 sysopen()、truncate()、flock()、fcntl()、stat() 和 -X。

作者

Tom Christiansen

TIEHANDLE 由 Sven Verdoolaege <[email protected]> 和 Doug MacEachern <[email protected]> 编写

UNTIE 由 Nick Ing-Simmons <[email protected]> 编写

SCALAR 由 Tassilo von Parseval <[email protected]> 编写

绑定数组由 Casey West <[email protected]> 编写