代碼生成
新手程序員往往會寫多余的代碼。一開始他們寫的代碼很長,再后來會學會使用函數(shù)、使用參數(shù),再后來會使用面向?qū)ο蟆⒏唠A函數(shù)和閉包--技能逐漸提升,代碼越來越簡練。
當你成為一個更好的程序員時,就會寫更少的代碼來解決問題。使用更好的抽象,寫更通用的代碼,還會重用代碼--甚至可以通過刪除代碼來添加功能,這時的你就達到了一定的境界。
讓你所寫的程序來為你編程就叫元編程或代碼生成。相對于代碼重用,元編程能讓你的抽象重用。
AUTOLOAD技術就演示了在缺失函數(shù)或方法時的元編程:Perl的調(diào)度系統(tǒng)允許你自己控制在查找函數(shù)(或方法)失敗時的行為。
eval
最簡單的代碼生成技術就是:構建一個包含Perl代碼的字符串,并且以eval操作符來編譯該字符串。不同于代碼異常捕獲的eval操作符,字符串的eval會在當前作用域內(nèi)編譯字符串的內(nèi)容。
一個常見用途就是在你無法加載一個可選的依賴時,提供一個倒退方案:
eval { require Monkey::Tracer } or eval 'sub Monkey::Tracer::log {}';
如果Monkey::Tracer不可用,其中l(wèi)og()函數(shù)就什么都不會做。你還得考慮關鍵字轉(zhuǎn)義的問題。通過插入一些變量來增加復雜性:
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每次被調(diào)用都會生成新的數(shù)據(jù)結構來表示代碼,還會花費性能來編譯代碼。eval機制有缺點,但貴在確實簡單、實用。
帶參數(shù)的閉包
通過使用eval,構建訪問器和修改器就變得簡單了。而閉包允許你接受參數(shù)并且在編譯時就生成代碼:
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;
}
這段代碼避免了不愉快的引用轉(zhuǎn)義問題,并且每個閉包只編譯一次,通過共享編譯過的閉包實例還會節(jié)省內(nèi)存。不同之處就是綁定的$attrname是詞法變量。在長時間運行的進程中或一個類中存在大量的訪問器時,這個技術非常有用。
將訪問器和修改器安裝到符號表是相當容易的:
my ($get, $set) = generate_accessors( 'pie' );
no strict 'refs';
*{ 'get_pie' } = $get;
*{ 'set_pie' } = $set;
代碼作用就是將函數(shù)引用安裝到了符號表,符號表就是一個名字空間,里面包含了全局可訪問的符號如包全局變量、函數(shù)和方法。
Perl內(nèi)部有個叫類型團(typeglob)的數(shù)據(jù)結構,里面包含了一組名字相同但類型不同的的指針,如*spud里面包含了$spud,@spud,%spud,&spud,spud(句柄)等。通過符號表spud項就能找到*spud里的各個類型。
所以上面那段代碼解釋下就是:先接收訪問器和設置器;然后給類型團賦值。這樣以后在調(diào)用函數(shù)get_pie時就等同于調(diào)用之前接收的那個訪問器($get)。(設置器set_pie是類似的)
賦值引用到符號表項就是安裝或替換這個符號表項。存儲這個函數(shù)引用到符號表,將匿名函數(shù)提升為方法。
賦值一個符號表項為字符串,而不是一個變量名字,這就是一個符合引用。你必須禁止strict的引用檢查,否則會報錯。很多程序可能會這么些:
no strict 'refs';
*{ $methname } = sub {
# subtle bug: strict refs disabled here too
};
但是這類代碼有著相同的BUG:禁用strcit檢查的范圍過寬,如上例中就在函數(shù)內(nèi)和函數(shù)外都禁用了strcit檢查。正確的做法是僅為需要的操作禁用strcit檢查:
{
my $sub = sub { ... };
no strict 'refs';
*{ $methname } = $sub;
}
如果方法名字是一個字符串而不是一個變量內(nèi)容,你可以直接賦值:
{
no warnings 'once';
(*get_pie, *set_pie) =
generate_accessors( 'pie' );
}
直接賦值給符號表(類型團)不會違反strict檢查,但是會產(chǎn)生告警:每個glob只使用了一次。你可以通過禁用該告警來解決這個問題。
簡化符號表的操作
你可以使用CPAN模塊Package::Stash來簡化符號表的操作。
在編譯時操作
不同于直接寫出來的代碼,通過eval操作生成的代碼是在運行時進行編譯的。當你期望一個普通函數(shù)在程序任何地方都可用時,運行時生成的函數(shù)可能達不到你的預期。(因為有可能函數(shù)還沒有生成好)
強制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一個模塊時,模塊中函數(shù)之外的代碼都會被執(zhí)行,這是因為Perl會強制將require和import放到BEGIN塊中,模塊內(nèi)函數(shù)之外的代碼都會在import()調(diào)用前執(zhí)行。如果僅僅是require一個模塊那是不會被放到BEGIN塊中的。
還要注意的是詞法聲明和詞法賦值之間的相互影響,聲明是在編譯時發(fā)生的,而賦值在代碼運行時才會發(fā)生。下面這段代碼有個小錯誤:
use UNIVERSAL::require;
my $wanted_package = 'Monkey::Jetpack';
BEGIN
{
$wanted_package->require;
$wanted_package->import;
}
BEGIN塊先執(zhí)行,而此時$wanted_package還沒被賦值,這就會拋出一個異常:嘗試調(diào)用一個未定義的值。
Class::MOP
在Perl中可以很方便的就能實現(xiàn)創(chuàng)建函數(shù)(將函數(shù)引用安裝到名字空間),但是卻幾乎沒辦法實現(xiàn)在動態(tài)地創(chuàng)建類。后來Moose和它的Class::MOP庫帶來了希望,它提供了一個元對象的協(xié)議---一個通過修改對象實例來控制面向?qū)ο笙到y(tǒng)的機制。
相對于自己動手寫eval或操作符號表這樣弱爆了的手段,現(xiàn)在你擁有了更為為大的武器,不僅可以操作實例,還能操作抽象(使用了面向?qū)ο蟮某绦虻某橄螅?/p>
創(chuàng)建一個類:
use Class::MOP;
my $class = Class::MOP::Class->create( 'Monkey::Wrench' );
創(chuàng)建的同時給予屬性和方法:
my $class = Class::MOP::Class->create(
'Monkey::Wrench' =>
(
attributes =>
[
Class::MOP::Attribute->new('$material'),
Class::MOP::Attribute->new('$color'),
]
methods =>
{
tighten => sub { ... },
loosen => sub { ... },
}
),
);
對于創(chuàng)建過的類增加屬性和方法:
$class->add_attribute(
experience => Class::MOP::Attribute->new('$xp')
);
$class->add_method( bash_zombie => sub { ... } );
MOP不僅能讓你在運行時創(chuàng)建新實體還能讓你感知現(xiàn)有的狀態(tài)。比如,你可以使用Class::MOP::Class來偵測類的特征:
my @attrs = $class->get_all_attributes;
my @meths = $class->get_all_methods;
類似的Class::MOP::Attribute和Class::MOP::Method也能實現(xiàn)創(chuàng)建、修改、偵測類的屬性和方法。