符号表处理
取自 PerlChina.org - wiki
- 原 名:Symbol Table Manipulation
- 中 文:符号表处理
- 作 者:Phil Crow
- 原 文:http://www.perl.com/pub/a/2005/03/17/symtables.html
- 发 表:2005/03/17
- 翻 译:zjl_perl
- 审 校:zerray
- 出 处:中国 Perl 协会 FPC - PerlChina.org
目录 |
我最喜欢的模块之一就是 Class::DBI::mysql,差不多快到达超级懒惰的完美境界了。它使MySQL中的表看上去和类一样,而表中的行就像对象一样。这让我在大多数应用SQL的工作中得到了解脱。这篇文章就会详细揭示 Class::DBI::mysql 模块有多么神奇。我不会用那些复杂的案例来讲,相反,我会用一个很小的例子来说明: Class::Colon 。
[编辑] 介绍
现在开始介绍 CPAN 上我最喜欢的模块之一 Class::DBI::mysql,有了它,我几乎忘记了我是在用数据库工作。看下面这个例子:
#!/usr/bin/perl
use strict;
use warnings;
package Users;
use base 'Class::DBI::mysql';
Users->set_db('Main', 'dbi:mysql:rt3', 'rt_user', 'rt_pass');
Users->set_up_table('Users');
package main;
my @column_names = qw( Name RealName );
print "@column_names\n";
print "-" x 30 . "\n";
my $user_iter = Users->retrieve_all();
while (my $row = $user_iter->next) {
print $row->Name, " ", $row->RealName, "\n";
}
除了 MySQL 的连接信息之外,没有任何关于 SQL 和数据库的痕迹。
我现在的目的并不是真的把这个美妙的模块介绍给你。而是,我要给你解释如何像这样来构建。为了达到这个目的,我会结合一个 CPAN 上的小模块 Class::Colon 。这个模块会把一个用冒号分割的文件变成类,把文件中的行变成对象。下面是一个来自支票簿应用程序的例子。这个程序由用户提供的日期计算一个账户的结余,如果用户没有提供日期则认为是时间的末尾。
#!/usr/bin/perl
use strict; use warnings;
use Getopt::Std;
use Date;
use Class::Colon Trans => [ qw(
status type date=Date amount desc category memo
) ];
our $opt_d;
getopt('d');
my $date = Date->new($opt_d) if $opt_d;
my $account = shift or die "usage: $0 [-d date] account_file\n";
my $trans_list = Trans->READ_FILE($account);
my $balance = 0;
foreach my $trans (@$trans_list) {
if (not defined $date or $date >= $trans->date) {
$balance += $trans->amount;
}
}
print "balance = $balance\n";
在 use Class::Colon 模块的时候,我告诉它要建立的类的名字(Trans),后面跟一个列表,这个列表是由按照文件中出现顺序排列的字段组成。 date 字段本身就是一个对象,所以在它后面我用了 =Date 。这会告诉 Class::Colon ,一个叫 Date 的类来处理 date 字段。如果 Date 类的构造函数没有指定 new 。那么我就得这么写, date=Date=constructor_name 。我的 Date 类是很原始的,它只提供比较大小操作。而且它只认一种时间格式。我不会再因为把它展示出来而感到困窘了。
当提取完账户文件的名字之后,程序通过 Trans 来调用 Class::Colon 定义的 READ_FILE 。这将会返回一个 Trans 对象列表。这些对象中的字段就是 use Class::Colon 部分给出的样子。通过他们指定的子程序可以很容易的操作他们。
程序的其他部分是通过交易列表来检查日期。如果用户没有指定日期,或者当前交易是发生在用户指定日期之前的,那么程序会把这个数量加到总数中。最后报告余额。
上面这个例子只用了它的查询操作,你可以容易的改变数值。所有的存取器都恢复并存储。通过调用 WRITE_FILE 来将更新记录写回磁盘。
其它的方法用于那些用冒号分割的记录。一些让你可以用句柄处理,而不是文件名。其它的帮助你分析和生成字符串,这样你可以掌控你自己的输入和输出。详情请参看 Class::Colon 的 perldoc 文档。(冒号并不是唯一的分隔符)
[编辑] 现在我们开始了
和其他类一样, Class::DBI::mysql 和 Class::Colon 两个类都是在执行阶段建立类。它们怎么作的呢?它们直接操作符号表。要了解这是什么意思,我打算从简单的开始。假设我有一个下面这样的变量:
my $extremely_long_variable_indicator_string;
我不并想经常敲入这样的变量。我可以象下面这样用两步做一个别名:
our $elvis;
首先,我声明一个有更好名字的标示符。我必须让它是全局的。如果打开了 strict ,我应该用 our 来这么作。(尽管有很多老办法也可以实现)。词汇作用域上的变量(用 my 声明的)是不生存在符号表中的,所以下面的代码对它们无效。
*elvis = \$extremely_long_variable_indicator_string;
现在我可以用 $elvis 来指向那个长长的变量。关键是那个*前缀。它引用符号表中 elvis (没有前缀的名字)的条目。这一行在符号表中以 $elvis 存储了一个 $extremely_long_variable_indicator_string 的引用,但是它不会影响到象 @elvis 和 %elvis 这样的条目的。现在,这两个标量都指向相同的数据,所以 $elvis 是真正的长名字变量的别名,而不是一个拷贝。
除非你和非常拙劣的同事工作,或者陷入了自毁行为,你大概不会为了一个短小的变量名而生成一个别名。然而,这一技术可以在你遇到的其它状况下工作。特别的,它是 Class::Colon 提供的简化 API 的基础。
要理解 Class::Colon 做什么,记住,子程序是 perl 中最简单的类型。你可以象处理变量那样处理子程序。例如,我可以象下面这样存储一个子程序的引用(子程序引用的前缀是&)。
my $abbr; $abbr = chunk135416830chunk#38;some_long_sub_name;
用它来调用子程序:
my @answer = $abbr->();
这里,我声明了一个标量变量 $abbr ,它包含一个子程序的引用。这里与直接操作符号表有点不同,但你也可以那样作:
*alias = chunk135416710chunk#38;some_long_sub_name; my @retval = alias();
不同于在变量之中存储一个子程序的引用,这段代码是在符号表本身中存储一个子程序。意思是后面的代码可以访问子程序,就好像事先声明了一个新名字的子程序一样。调整符号表不比存储一个引用更容易读写,但是,在象 Class::Colon 这样的模块中,更改符号表是简化调用者 API 的要点。
[编辑] 纯魔法类
上面的示例讲述了在你需要的时候如何生成一个符号表条目。这些可以节省输入并且(或者)让东西变得更加可读。标准模块 English 就是使用了这个方法为每个特殊变量(象$_ )一个易懂的英文名字。尽管,你想要的更多,你想在执行阶段凭空地建立类。
建立类的关键是你要认识到,类就是一个包,而包其实就是一个符号表(或多或少是)。还有符号表自动生动化( autovivify )的事实,是你所需要的全部,用来应付有巨量帮助的家伙,象 Class::DBI::mysql 。
[编辑] use 部分到底做了什么
这一段我们解释如何通过 use 语句传递数据。如果你已经理解了 import 子程序,你可以跳到下一节。
当你在 Perl 中使用一个模块时,你可以为那个模块在载入时提供一些信息。当 Class::DBI::mysql 模块等待你在建立类之前调用子程序的时候, Class::Colon 类在用 import 方法载入的时候作了这些工作。
在别人使用了你的模块的时候, perl 就会调用它的 import 方法(如果它有一个的话)。 import 方法接收了调用者使用的类的名字,还有调用者提供的所有参数。
在上面的支票簿例子中,调用者使用 Class::Colon 时提供参数:
use Class::Colon Trans => [ qw(
status type date=Date amount desc category memo
) ];
Class::Colon 包的 import 方法就把接收到的下面的内容当作一个结果:
字符串 Class::Colon 。
一个列表有两个元素。第一,字符串 Trans 。第二,一个由字段组成列表的数组的引用。 import 方法象下面这样来存储。
[编辑] 插入到一个不存在的符号表
Class::Colon 的最主要的魔法就发生在 import 方法中,这就是它的样子:
sub import {
my $class = shift;
my %fakes = @_;
foreach my $fake (keys %fakes) {
no strict;
*{"$fake\::NEW"} = sub { return bless {}, shift; };
foreach my $proxy_method qw(
read_file read_handle objectify delim
write_file write_handle stringify
) {
my $proxy_name = $fake . "::" . uc $proxy_method;
my $real_name = $class . "::" . $proxy_method;
*{"$proxy_name"} = chunk135416590chunk#38;{"$real_name"};
}
my @attributes;
foreach my $col (@{$fakes{$fake}}) {
my ($name, $type, $constructor) = split /=/, $col;
*{"$fake\::$name"} = _make_accessor($name, $type, $constructor);
push @attributes, $name;
}
$simulated_classes{$fake} = {ATTRS => \@attributes, DELIM => ':'};
}
}
在把参数转成有实际意义的变量之后,主循环遍历每个被请求的类( fakes 列表)。在循环中禁止了 strict ,因为必须使用这么多的符号引用会比较烦。
在每个类的制作过程中有4个步骤:
- 制作构造器
- 制作类方法
- 制作存取方法
- 依次存储属性名称
每个要制作的类和其构造器都要尽可能的简单。它返回一个被 bless 进被请求类的哈希引用。比较酷的是你可以在一个预先还不存在的符号表中插入代码。这个构造器将会是 NEW 。(一般来说, Class::Colon 使用大写字母来命名它的方法,以避免和用户字段发生冲突)。
这段代码需要小心一点引用。 *{”$fake\::NEW”} 告诉 Perl 用 NEW 在新包的符号表中建立一个条目。反斜线用来防止变量内插。当 $fake 需要内插,则内插 $fake::NEW 只会导致 undef ,因为这是他在这里的第一次定义。
当 Perl 存储了构造器就已经完成了最难做的部分。它已经把包从无到有。现在只剩下命名一些别名了。
对于每一个提供的方法,程序在已经制造的类的符号表中都建立了条目。这些条目都指向 Class::Colon 包中的方法,这些方法作为永久共享代表服务于所有已经制造的类中。
调用者使用 use 时提供的很多属性,类似的,它为每个属性都建立一个存取通道。这些程序需要一些自定义的操作来寻找合适的属性名称并同对象构造器协调工作。因此,这里有一个小程序叫做“ _make_accessor ”,它为每个存取通道返回合适的闭合。
最后,他为模拟类的主列表中的新类制作条目。这将允许在用已定义的名字调用类的方法时,按照名字简单查询。注意,在 import 子程序中并没有限制调用者只能调用一次。深一层的 use 段可以激活额外的类。抑或调用者可以用单一的 use 段来请求多个新类,通过包含多重哈希键值。
在标准例子中, _make_accessor 象下面这样工作:
sub _make_accessor {
my $attribute = shift;
return sub {
my $self = shift;
my $new_val = shift;
$self->{$attribute} = $new_val if defined $new_val;
return $self->{$attribute};
};
}
实际的程序有一点复杂,所以它可以处理那些作为对象的属性的构造任务。注意 $attribute 的值,在闭合创建之后存在于这个范围中,将和 sub 保持在一起,在 sub 被调用的时候才会使用。目前的代码是典型的标准 Perl 双重使用存取器。如果调用者传递过来一个值,它就会分配给属性一个新值。它总是返回属性的值。
[编辑] Class::Colin 类提供什么
为了保持完整,这里展示 Class::Colon 如何把一个字符串变成一系列对象。注意,要严肃使用那些方法,通过它们事先输入的符号表名称。
sub objectify {
my $class = shift;
my $string = shift;
my $config = $simulated_classes{$class};
my $col_list = $config->{ATTRS};
my $new_object = $class->NEW();
my @cols = split /$config->{DELIM}/, $string;
foreach my $i (0 .. @cols - 1) {
my $method = $col_list->[$i];
$new_object->$method($cols[$i]);
}
return $new_object;
}
所有已制作的类都共享这个方法(和其他 Class::Colon 类中的方法)。
回忆一下刚才那个 NEW 方法,它返回一个空的 blessed 哈希引用。在对象化中,通过调用它们的存取器来循环填充那些属性。这保证了任一对象属性的适当构造。当调用者调用 READ_FILE 和它的兄弟程序时,他们间接的进行对象化。他们也可以通过它的 OBJECTIFY 别名来直接使用它。
[编辑] 总结
通过在符号表中建立条目,你可以为那些很难命名的数据创建别名。你甚至可以简单地通过引用它们来创建一个新的符号表。这允许你随意建立类。诸如 Class::DBI::mysql 和 Class::Colon 的类就是这样提供表示表格数据的类的。
这些技巧也可以使用于其他方面。例如, Memoize 用缓存模式包裹一个原始函数,并用这个包裹的版本替代调用者自己的符号表中的原始函数。因为在输入参数一样的情况下它们反回相同的结果,所以这可以节省时间。 Exporter 可以做更复杂的工作,用一个已经使用的包中的符号来污染调用者的符号表。本质上讲这些方式和上面展示的有些类似。只要小心地操作模块中的符号表,你通常可以极大地简化 API ,让客户端代码更易读,易写和易维护。
