通过 CPAN 模块绑定标量、数组及散列变量的示例 |
|
级别: 初级
Teodor Zlatanov (tzz@iglou.com), 程序员, Gold Software Systems
2003 年 1 月 09 日
Ted 以 CPAN 模块作为具体示例,通过其用法和实现,解释了绑定变量的基础知识。范围涵盖了标量、数组和散列变量。
在开始之前,您必须在您的系统上安装 Perl 5.005 或更新的版本(请参阅 参考资料中的链接,通过这些链接获取该版本)。您的系统最好安装了较新的(2000 年或之后的)主流 UNIX(Linux、Solaris 和 BSD 等等),但其它操作系统或许也行。对于较早版本的 Perl 和 UNIX 或者其它操作系统,本文所提供的示例或许可以使用 — 不过在这样的条件下出现无法使用的情况,就作为练习由读者自行解决。
绑定的变量对于所有 Perl 程序员都是一个重要的工具。重用使用 Tie::Scalar
、 Tie::Array
或 Tie::Hash
接口的现有代码确实是再简单不过了,但是不管您是想就此主题编写您自己的程序,还是只想优化对绑定变量的使用,理解内部工作原理都是非常有用的。
让我们来研究三类主要的绑定变量:标量、数组和散列。因为绑定文件句柄比较复杂,所以它们属于比较高级的主题。
对于本文中所提及的每个 CPAN 模块,您都可以使用 CPAN 界面来查看其实现。在 UNIX 提示符下输入“cpan”或“perl -MCPAN -eshell”,您将看到一个二级提示符。例如,输入“look Tie::Scalar::Timeout”来查看 Tie::Scalar::Timeout
模块,您将能够看到该模块的内容。
“绑定(tie)”变量是什么意思?这里,动词“tie(绑定)”是作为“bind(绑定)”的同义词来使用的。绑定变量基本上就是将函数绑定到内部触发器上以读写该变量。这意味着,作为一名程序员,在使用变量时您可以让 Perl 做额外的事情。如果从这个简单的前提出发,那么绑定接口已经演变为 Perl 中的面向对象方法了,它将 OOP 的复杂性隐藏在过程接口后面。
标量变量简单而又不可缺少。它只保存一段数据:一个字符串、一个数字、未定义的值以及对另一个变量的引用。变量前面的“$”告诉 Perl 将该变量作为标量处理。使用标量变量是一件非常容易的事情:
清单 1. 普通标量 my $a = 'Hello'; $a = 'there'; $a = 89.2; |
使用绑定标量变量同样简单。例如,让我们以极佳的 Tie::Scalar::Timeout
模块为例:
use Tie::Scalar::Timeout; tie my $k, 'Tie::Scalar::Timeout', EXPIRES => '+2s'; $k = 123; sleep(3); # $k is now undef |
第一部分,其中调用了 tie()
函数,演示了如何告诉 Perl 变量 $k
实际上绑定到了 Tie::Scalar::Timeout
包。在幕后,Perl 运行 Tie::Scalar::Timeout
模块的 TIESCALAR()
函数(这实质上有些象对一个常规对象调用 new()
)。 TIESCALAR()
返回一个 Tie::Scalar::Timeout
类型的对象,该对象被赋给 $k
。
示例中传递给 Tie::Scalar::Timeout
的特定参数确保了它会在两秒钟之后超时。该模块还提供了其它选项,如在读了确定次数之后就超时。每次读取上个示例中创建的 $k
变量时,都会调用 Tie::Scalar::Timeout
模块中的 FETCH()
方法:
sub FETCH { my $self = shift; # if num_uses isn't set or set to a negative value, it won't # influence the expiry process if (($self->{NUM_USES} == 0) || (time >= $self->{EXPIRY_TIME})) { # policy can be a coderef or a plain value return &{ $self->{POLICY} } if ref($self->{POLICY}) eq 'CODE'; return $self->{POLICY}; } $self->{NUM_USES}-- if $self->{NUM_USES} > 0; return $self->{VALUE}; } |
每次写绑定标量变量时,都会调用它的 STORE()
方法。标量也有 UNTIE()
和 DESTROY()
方法,但通常不会用到。
注:绑定标量变量以及在此问题上的 任何绑定变量都需要将其实际数据存储在某个地方。对于 Tie::Scalar::Timeout
,数据存储在 $self->{VALUE}
中,因为标量变量 $k
实际上只是一个散列。Perl 通过创建一种封装向我们隐藏了这层复杂性,这种封装十分类似于 OOP 中的封装。
上面的代码意味着每次请求 $k
变量的值时,该值都 可能改变。因此,如果希望拥有自己的 Schroedinger 盒,那么只需使用 Tie::Scalar::Timeout
模块和一个 0 到 100 之间的随机超时,并在 50 秒时读取 $k 变量值。假定有一个好的随机数生成器,您将根据超时获得 1 或未定义值。我们假定程序中指令执行所花费的时间是可以忽略的,但实际上它还是会引入一些偏差。的确,我们只用确定 rand(100)
是否大于 50 就行了,但这又有什么乐趣呢?
use Tie::Scalar::Timeout; # the timeout will be between 0 and 99 my $random_timeout = rand 100; tie my $k, 'Tie::Scalar::Timeout', VALUE => 1, EXPIRES => "+${random_timeout}s"; sleep(50); print 'The timeout ', ($k) ? 'did not happen' : 'happened', "/n"; |
如果超时发生了,那么逮住一只猫并对它射击的任务就留给了读者作为练习来完成。
|
数组比标量要复杂一些。它是标量的有序集合,因此需要额外的功能来处理它。数组有 TIEARRAY()
(构造函数,类似于绑定数组的 new()
)、 FETCH()
/ STORE()
(含意和绑定标量中的相同,但有一些额外的参数)、 FETCHSIZE()
/ STORESIZE()
(用于数组大小管理)以及 UNTIE()
和 DESTROY()
,最后两个(例如)可以用于关闭文件或刷新输出。
同绑定标量中等同的函数比起来,绑定数组的 FETCH()
和 STORE()
需要额外的参数。这个额外的参数就是数组中的下标。 FETCHSIZE()
和 STORESIZE()
是调用 scalar( @ARRAY
) 和 $#ARRAY = x
时分别要用到的。
如果要用到对应的 Perl delete()
和 exists()
函数,那么就需要实现 DELETE()
和 EXISTS()
函数。
还有 POP()
、 PUSH()
、 SHIFT()
、 UNSHIFT()
、 SPLICE()
和 EXTEND()
函数(其中的头五个函数对应于同名但名字是小写形式的 Perl 函数),但模块编写者通常都会继承 Tie::StdArray
并使用那些已经实现的方法。
例如,可以这样实现 POP()
:对最后一个元素使用 FETCH()
,然后使用 STORESIZE(FETCHSIZE()-1)
(将数组的大小减去 1,实际除去了最后一个元素)。当然,如果自己实现 POP()
,那么您要么完全知道您在做什么,要么完全不知道。
如果您想编写自己的绑定数组,那么请确保继承了 Tie::StdArray
(请参阅“perldoc Tie::Array”)。所有的函数都已为您做了定义,并且您只需覆盖您想修改的函数 — 没必要另起炉灶。顺便说一下,绑定的数组正好是最复杂的绑定变量类型,而且根据我的统计在 CPAN 上也实现得最少。散列则没这么复杂。(如果您对实现细节感兴趣,那么请看 Tie::CharArray
源代码。)
作为绑定数组的示例,我们将研究 CPAN Tie::CharArray 模块。该模块允许程序员将字符串当作字符数组对待,或者作为数字代码或者作为单字符字符串。下面是该文档的一个示例:
清单 5. 作为数组的字符串 use Tie::CharArray; my $foobar = 'a string'; tie my @foo, 'Tie::CharArray', $foobar; $foo[0] = 'A'; # $foobar = 'A string' push @foo, '!'; # $foobar = 'A string!' |
对于这,C/C++/Java 程序员应该是“刻骨铭心”了。
注:如果将上面例子中的第 3 行写成
tie my @foo, 'Tie::CharArray', 'a string';
那么它将无法工作,并给出消息“Modification of a read-only value attempted”。事实上,‘a string’是一个不能修改的常量字符串。 @foo
数组直接使用传递给它的字符串,如果给它赋值就直接修改 原始字符串。
Tie::CharArray
实际上是不考虑有关 Perl 中 substr()
或 pack()
/ unpack()
或者 split()
函数的细节的极佳办法。如果需要修改字符串中第 5 位到第 28 位的字符,那么您可以使用 substr()
或 pack()
/ unpack()
或者 split()
。您也可以只写:
use Tie::CharArray qw/chars/; $f = "jello is yellow"; my $chars = chars $f; foreach (5..28) { $chars->[$_] = "!"; }; |
您愿意使用哪一种方法(内置字符串操作或 Tie::CharArray
)属于个人喜好,但清单 6 的可读性却是无庸置疑的。
|
现在我们要研究好东西了。绑定的散列比绑定的数组更易于编写,而且更有用。
绑定的散列实现 TIEHASH()
构造函数、 FETCH()
/ STORE()
访问方法、 EXISTS()
/ DELETE()
方法(二者所起的作用和 Perl 中的 exists()
和 delete()
完全相象)、清除散列的 CLEAR()
以及用于遍历数组的 FIRSTKEY()
/ NEXTKEY()
。您完全可以继承 Tie::StdHash
包(位于 Tie::Hash perldoc 中),它定义了您需要的全部方法,这样您只需覆盖您想要的方法。
我们将在我的 Tie::Hash::TwoWay
模块中看到绑定散列的确切实现。该模块在内部维护两个散列,并在第一个散列获得数据时自动在第二个散列中创建反向映射。例如,如果您将键为“dog”的值 ["Fido"] 和键为“friend”的值 ["Fido"] 赋给 Tie::Hash::TwoWay
绑定散列(值必须在数组引用中),那么在同一个 Tie::Hash::TwoWay
绑定散列中将突然会出现一个带有值“dog”和“friend”的键“Fido”。请参阅该文档的示例:
use Tie::Hash::TwoWay; tie %hash, 'Tie::Hash::TwoWay'; my %list = ( Asimov => ['novelist', 'scientist'], King => ['novelist', 'horror'], ); foreach (keys %list) # these are the primary keys of the hash { $hash{$_} = $list{$_}; } # these will all print 'yes' print 'yes' if exists $hash{scientist}; print 'yes' if exists $hash{novelist}->{Asimov}; print 'yes' if exists $hash{novelist}->{King}; print 'yes' if exists $hash{King}->{novelist}; |
Tie::Hash::TwoWay
继承了 Tie::StdHash
模块并覆盖了 STORE()
、 FETCH()
、 EXISTS()
、 DELETE()
、 CLEAR()
、 FIRSTKEY()
和 NEXTKEY()
方法。除此以外,它还定义了一个 secondary_keys()
方法来获取反向映射键。主键存储在 $self->{1}
中而辅键存储在 $self->{0}
中;数字常量具有符号名称 PRIMARY 和 SECONDARY,我个人以为这使它们可读性更好。
以下是 Tie::Hash::TwoWay
的代码,除了没有绑定/继承/初始化导言以及文档以外,其它都和模块中的一样。清单是按函数划分的。因为我们继承了 Tie::StdHash
,所以我们无需为实例定义 TIEHASH()
方法。我们仅仅重新定义需要修改其行为的方法。
sub STORE { my ($self, $key, $value) = @_; my $val_array_ref; if (ref $value eq 'ARRAY') # array refs can be recognized { $val_array_ref = $value; } else # everything else gets converted to array refs { $val_array_ref = [ $value ]; } # add the values in the passed array to the primary and secondary hashes foreach my $value (@$val_array_ref) { $self->{SECONDARY}->{$value}->{$key} = 1; $self->{PRIMARY}->{$key}->{$value} = 1; } return 1; } |
STORE()
函数同时在主数组和辅数组(常规和反向映射)中创建一个项。数组被直接处理,其它任何东西都作为标量处理(并被插入数组引用中)。
# return the primary or secondary key, in that order (duplicate keys # are not detected here) sub FETCH { my ($self, $key) = @_; exists $self->{PRIMARY}->{$key} && return $self->{PRIMARY}->{$key}; exists $self->{SECONDARY}->{$key} && return $self->{SECONDARY}->{$key}; return undef; } |
FETCH()
函数从主散列或辅散列检索键,这里优先检索主散列。(语句 1)&&(语句 2)这种逻辑快捷表示是一种常见的 Perl 习惯语法。
# return the primary or secondary key existence, in that order # (duplicate keys are not detected here) sub EXISTS { my ($self, $key) = @_; return undef unless (exists $self->{PRIMARY} && exists $self->{SECONDARY}); return (exists $self->{PRIMARY}->{$key} || exists $self->{SECONDARY}->{$key}); } |
EXISTS()
函数依次检查正向和反向映射中是否存在某个键。
# delete the primary or secondary key, in that order (duplicate keys # are not detected here) sub DELETE { my ($self, $key) = @_; return undef unless (exists $self->{PRIMARY} && exists $self->{SECONDARY}); # make sure to delete reverse associations as well if (exists $self->{PRIMARY}->{$key}) { foreach (keys %{$self->{SECONDARY}}) { delete $self->{SECONDARY}->{$_}->{$key}; delete $self->{SECONDARY}->{$_} unless scalar keys %{$self->{SECONDARY}->{$_}}; } return delete $self->{PRIMARY}->{$key}; } if (exists $self->{SECONDARY}->{$key}) { foreach (keys %{$self->{PRIMARY}}) { delete $self->{PRIMARY}->{$_}->{$key}; delete $self->{PRIMARY}->{$_} unless scalar keys %{$self->{PRIMARY}->{$_}}; } return delete $self->{SECONDARY}->{$key}; } } |
删除函数略微有些复杂,因为我们还想删除正在删除的键的反向映射。因此,如果我们删除 a->b,那么我们也要除去 b->a(辅)映射,如果映射为空,还要除去映射的数组值。
清单 12. CLEAR() 函数 sub CLEAR { my ($self, $key) = @_; %$self = (); # clear the whole hash return 1; } |
清除内部散列十分简单,而且我们可以继续使用该对象,因为 STORE()
中具有自动复活能力。
sub FIRSTKEY { my ($self) = @_; return undef unless (exists $self->{PRIMARY} && exists $self->{SECONDARY}); return each %{$self->{PRIMARY}}; } sub NEXTKEY { my ($self, $lastkey) = @_; return undef unless (exists $self->{PRIMARY} && exists $self->{SECONDARY}); return each %{$self->{PRIMARY}}; } |
迭代器 FIRSTKEY()
和 NEXTKEY()
看似复杂,但实际上它们只是让 each()
这一 Perl 函数来完成所有的工作而已。
sub secondary_keys { my ($self) = @_; return undef unless (exists $self->{PRIMARY} && exists $self->{SECONDARY}); return keys %{$self->{SECONDARY}}; } |
由于 FIRSTKEY()
和 NEXTKEY()
的常规 keys()
迭代只是对主键进行,因此 secondary_keys()
函数是为第二个映射提供的。
|
遗憾的是,由于绑定的文件句柄过于复杂,所以无法在本文中介绍。但它确实能够让人神往,例如,通过写/读文件句柄,可以直接写/读数据库,或者在关闭文件时发送一封电子邮件。
将磁盘(文件)数据库绑定到散列是一个得到了深入讨论的主题。您可以阅读“perldoc DB_File”文档、查看 Programming Perl Third Edition一书中的相关章节,或者在线阅读众多教程中的某一篇(请参阅 参考资料一节以获取一些资料的链接)。
即便如此,从我们这里所做的来看,您会发现绑定的变量十分有用,而且我鼓励您去深入了解众多处理绑定的变量的 CPAN 模块。您几乎肯定能够找到一个满足您需要的模块。
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
- 要按照本文所讲的来做,您应该有 Perl 5.05 或更高版本。在“综合 Perl 归档网络(Comprehensive Perl Archive Network)” — 即人们常说的 CPAN 上找到 Perl 源代码及大量其它内容。
- 同样是在 CPAN,您将找到您想要的所有 Perl 模块。
- 访问 Perl.com以获取更多 Perl 信息和相关参考资料。
- 如果您有兴趣了解更多关于绑定的变量的内容,请检出 Larry Wall、Tom Christiansen 和 Jon Orwant(O'Reilly & Associates,2000 年)合著的 Programming Perl Third Edition 。它是目前最优秀的 Perl 指南,介绍最新的 5.005 和 5.6.0。第 14 章涉及了绑定变量,是一份 优秀的参考资料。
- 模块:
- 在 developerWorksLinux 专区中找到更多 为 Linux 开发人员提供的参考资料。
Teodor Zlatanov 于 1999 年从美国波士顿大学(Boston University)毕业,获得计算机工程硕士学位。他从 1992 年起就从事程序员的工作,使用了 Perl、Java、C 和 C++。他的兴趣是文本解析、三层客户机-服务器数据库体系结构、UNIX 系统管理、CORBA 和项目管理方面的开放源码工作。可以通过 tzz@iglou.com与 Teodor 联系。 |