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() 函数不会为您 use
或 require
模块 - 您需要自己显式地执行此操作。
实现绑定标量的类应该定义以下方法: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;
这是类的构造函数。这意味着它应该返回对它正在创建的新标量(可能是匿名的)的祝福引用。例如
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
以查看是否要发出一些噪音。
每次访问(读取)绑定变量时,都会触发此方法。除了自身引用之外,它不接受任何参数,自身引用是表示我们正在处理的标量的对象。因为在这种情况下,我们只使用 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;
这是该类的构造函数。这意味着它应该返回一个祝福的引用,通过该引用可以访问新数组(可能是匿名 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];
}
如果使用负数组索引从数组中读取,则索引将在内部通过调用 FETCHSIZE 转换为正索引,然后再传递给 FETCH。您可以在绑定数组类中将变量 $NEGATIVE_INDICES
赋值为真值来禁用此功能。
您可能已经注意到,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
发生时将被调用。(参见下面 "The 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() 到那里,它将测试错误的文件。
每次访问绑定哈希中的元素(读取)时,都会触发此方法。它除了自身引用之外,还接受一个参数:我们要获取其值的键。
以下是 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 返回空列表,则结果为假值,否则为真值。
但是,您不应该盲目依赖 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
,请使用 *$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
(这意味着“全部读取”模式)。
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
时,将调用此方法。在这种情况下,可能需要“自动关闭”。请参阅下面的"The 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() 时被调用。请参阅下面的"The untie
Gotcha"。
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_File 或 Config,了解一些有趣的 tie() 实现。对于许多 tie() 实现来说,一个好的起点是使用以下模块之一:Tie::Scalar、Tie::Array、Tie::Hash 或 Tie::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]> 编写