代码生成
新手程序员往往会写多余的代码。一开始他们写的代码很长,再后来会学会使用函数、使用参数,再后来会使用面向对象、高阶函数和闭包--技能逐渐提升,代码越来越简练。
当你成为一个更好的程序员时,就会写更少的代码来解决问题。使用更好的抽象,写更通用的代码,还会重用代码--甚至可以通过删除代码来添加功能,这时的你就达到了一定的境界。
让你所写的程序来为你编程就叫元编程或代码生成。相对于代码重用,元编程能让你的抽象重用。
AUTOLOAD技术就演示了在缺失函数或方法时的元编程:Perl的调度系统允许你自己控制在查找函数(或方法)失败时的行为。
eval
最简单的代码生成技术就是:构建一个包含Perl代码的字符串,并且以eval操作符来编译该字符串。不同于代码异常捕获的eval操作符,字符串的eval会在当前作用域内编译字符串的内容。
一个常见用途就是在你无法加载一个可选的依赖时,提供一个倒退方案:
eval { require Monkey::Tracer } or eval 'sub Monkey::Tracer::log {}';
如果Monkey::Tracer不可用,其中log()函数就什么都不会做。你还得考虑关键字转义的问题。通过插入一些变量来增加复杂性:
sub generate_accessors
{
my ($methname, $attrname) = @_;
eval <<"END_ACCESSOR";
sub get_$methname
{
my \$self = shift;
return \$self->{$attrname};
}
sub set_$methname
{
my (\$self, \$value) = \@_;
\$self->{$attrname} = \$value;
}
END_ACCESSOR
}
上面例子中,要是谁没注意,忘记了写反斜杠会怎么样呢?幸运的是语法高亮可能会帮助你注意到这个问题。eval每次被调用都会生成新的数据结构来表示代码,还会花费性能来编译代码。eval机制有缺点,但贵在确实简单、实用。
带参数的闭包
通过使用eval,构建访问器和修改器就变得简单了。而闭包允许你接受参数并且在编译时就生成代码:
sub generate_accessors
{
my $attrname = shift;
my $getter = sub
{
my $self = shift;
return $self->{$attrname};
};
my $setter = sub
{
my ($self, $value) = @_;
$self->{$attrname} = $value;
};
return $getter, $setter;
}
这段代码避免了不愉快的引用转义问题,并且每个闭包只编译一次,通过共享编译过的闭包实例还会节省内存。不同之处就是绑定的$attrname是词法变量。在长时间运行的进程中或一个类中存在大量的访问器时,这个技术非常有用。
将访问器和修改器安装到符号表是相当容易的:
my ($get, $set) = generate_accessors( 'pie' );
no strict 'refs';
*{ 'get_pie' } = $get;
*{ 'set_pie' } = $set;
代码作用就是将函数引用安装到了符号表,符号表就是一个名字空间,里面包含了全局可访问的符号如包全局变量、函数和方法。
Perl内部有个叫类型团(typeglob)的数据结构,里面包含了一组名字相同但类型不同的的指针,如*spud里面包含了$spud,@spud,%spud,&spud,spud(句柄)等。通过符号表spud项就能找到*spud里的各个类型。
所以上面那段代码解释下就是:先接收访问器和设置器;然后给类型团赋值。这样以后在调用函数get_pie时就等同于调用之前接收的那个访问器($get)。(设置器set_pie是类似的)
赋值引用到符号表项就是安装或替换这个符号表项。存储这个函数引用到符号表,将匿名函数提升为方法。
赋值一个符号表项为字符串,而不是一个变量名字,这就是一个符合引用。你必须禁止strict的引用检查,否则会报错。很多程序可能会这么些:
no strict 'refs';
*{ $methname } = sub {
# subtle bug: strict refs disabled here too
};
但是这类代码有着相同的BUG:禁用strcit检查的范围过宽,如上例中就在函数内和函数外都禁用了strcit检查。正确的做法是仅为需要的操作禁用strcit检查:
{
my $sub = sub { ... };
no strict 'refs';
*{ $methname } = $sub;
}
如果方法名字是一个字符串而不是一个变量内容,你可以直接赋值:
{
no warnings 'once';
(*get_pie, *set_pie) =
generate_accessors( 'pie' );
}
直接赋值给符号表(类型团)不会违反strict检查,但是会产生告警:每个glob只使用了一次。你可以通过禁用该告警来解决这个问题。
简化符号表的操作
你可以使用CPAN模块Package::Stash来简化符号表的操作。
在编译时操作
不同于直接写出来的代码,通过eval操作生成的代码是在运行时进行编译的。当你期望一个普通函数在程序任何地方都可用时,运行时生成的函数可能达不到你的预期。(因为有可能函数还没有生成好)
强制Perl在编译时就去运行生成代码,可以使用关键字BEGIN来包含代码块。来对比下写法上的不同:
sub get_age { ... }
sub set_age { ... }
sub get_name { ... }
sub set_name { ... }
sub get_weight { ... }
sub set_weight { ... }
和
sub make_accessors { ... }
BEGIN
{
for my $accessor (qw( age name weight ))
{
my ($get, $set) =make_accessors( $accessor );
no strict 'refs';
*{ 'get_' . $accessor } = $get;
*{ 'set_' . $accessor } = $set;
}
}
当你use一个模块时,模块中函数之外的代码都会被执行,这是因为Perl会强制将require和import放到BEGIN块中,模块内函数之外的代码都会在import()调用前执行。如果仅仅是require一个模块那是不会被放到BEGIN块中的。
还要注意的是词法声明和词法赋值之间的相互影响,声明是在编译时发生的,而赋值在代码运行时才会发生。下面这段代码有个小错误:
use UNIVERSAL::require;
my $wanted_package = 'Monkey::Jetpack';
BEGIN
{
$wanted_package->require;
$wanted_package->import;
}
BEGIN块先执行,而此时$wanted_package还没被赋值,这就会抛出一个异常:尝试调用一个未定义的值。
Class::MOP
在Perl中可以很方便的就能实现创建函数(将函数引用安装到名字空间),但是却几乎没办法实现在动态地创建类。后来Moose和它的Class::MOP库带来了希望,它提供了一个元对象的协议---一个通过修改对象实例来控制面向对象系统的机制。
相对于自己动手写eval或操作符号表这样弱爆了的手段,现在你拥有了更为为大的武器,不仅可以操作实例,还能操作抽象(使用了面向对象的程序的抽象)。
创建一个类:
use Class::MOP;
my $class = Class::MOP::Class->create( 'Monkey::Wrench' );
创建的同时给予属性和方法:
my $class = Class::MOP::Class->create(
'Monkey::Wrench' =>
(
attributes =>
[
Class::MOP::Attribute->new('$material'),
Class::MOP::Attribute->new('$color'),
]
methods =>
{
tighten => sub { ... },
loosen => sub { ... },
}
),
);
对于创建过的类增加属性和方法:
$class->add_attribute(
experience => Class::MOP::Attribute->new('$xp')
);
$class->add_method( bash_zombie => sub { ... } );
MOP不仅能让你在运行时创建新实体还能让你感知现有的状态。比如,你可以使用Class::MOP::Class来侦测类的特征:
my @attrs = $class->get_all_attributes;
my @meths = $class->get_all_methods;
类似的Class::MOP::Attribute和Class::MOP::Method也能实现创建、修改、侦测类的属性和方法。