第九章 管理真實(shí)的程序(七) -代碼生成

代碼生成

新手程序員往往會(huì)寫(xiě)多余的代碼。一開(kāi)始他們寫(xiě)的代碼很長(zhǎng),再后來(lái)會(huì)學(xué)會(huì)使用函數(shù)、使用參數(shù),再后來(lái)會(huì)使用面向?qū)ο?、高階函數(shù)和閉包--技能逐漸提升,代碼越來(lái)越簡(jiǎn)練。

當(dāng)你成為一個(gè)更好的程序員時(shí),就會(huì)寫(xiě)更少的代碼來(lái)解決問(wèn)題。使用更好的抽象,寫(xiě)更通用的代碼,還會(huì)重用代碼--甚至可以通過(guò)刪除代碼來(lái)添加功能,這時(shí)的你就達(dá)到了一定的境界。

讓你所寫(xiě)的程序來(lái)為你編程就叫元編程或代碼生成。相對(duì)于代碼重用,元編程能讓你的抽象重用。

AUTOLOAD技術(shù)就演示了在缺失函數(shù)或方法時(shí)的元編程:Perl的調(diào)度系統(tǒng)允許你自己控制在查找函數(shù)(或方法)失敗時(shí)的行為。

eval

最簡(jiǎn)單的代碼生成技術(shù)就是:構(gòu)建一個(gè)包含Perl代碼的字符串,并且以eval操作符來(lái)編譯該字符串。不同于代碼異常捕獲的eval操作符,字符串的eval會(huì)在當(dāng)前作用域內(nèi)編譯字符串的內(nèi)容。

一個(gè)常見(jiàn)用途就是在你無(wú)法加載一個(gè)可選的依賴時(shí),提供一個(gè)倒退方案:

eval { require Monkey::Tracer } or eval 'sub Monkey::Tracer::log {}';

如果Monkey::Tracer不可用,其中l(wèi)og()函數(shù)就什么都不會(huì)做。你還得考慮關(guān)鍵字轉(zhuǎn)義的問(wèn)題。通過(guò)插入一些變量來(lái)增加復(fù)雜性:

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
}

上面例子中,要是誰(shuí)沒(méi)注意,忘記了寫(xiě)反斜杠會(huì)怎么樣呢?幸運(yùn)的是語(yǔ)法高亮可能會(huì)幫助你注意到這個(gè)問(wèn)題。eval每次被調(diào)用都會(huì)生成新的數(shù)據(jù)結(jié)構(gòu)來(lái)表示代碼,還會(huì)花費(fèi)性能來(lái)編譯代碼。eval機(jī)制有缺點(diǎn),但貴在確實(shí)簡(jiǎn)單、實(shí)用。

帶參數(shù)的閉包

通過(guò)使用eval,構(gòu)建訪問(wèn)器和修改器就變得簡(jiǎn)單了。而閉包允許你接受參數(shù)并且在編譯時(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)義問(wèn)題,并且每個(gè)閉包只編譯一次,通過(guò)共享編譯過(guò)的閉包實(shí)例還會(huì)節(jié)省內(nèi)存。不同之處就是綁定的$attrname是詞法變量。在長(zhǎng)時(shí)間運(yùn)行的進(jìn)程中或一個(gè)類中存在大量的訪問(wèn)器時(shí),這個(gè)技術(shù)非常有用。

將訪問(wèn)器和修改器安裝到符號(hào)表是相當(dāng)容易的:

my ($get, $set) = generate_accessors( 'pie' );

no strict 'refs';
*{ 'get_pie' } = $get;
*{ 'set_pie' } = $set;

代碼作用就是將函數(shù)引用安裝到了符號(hào)表,符號(hào)表就是一個(gè)名字空間,里面包含了全局可訪問(wèn)的符號(hào)如包全局變量、函數(shù)和方法。

Perl內(nèi)部有個(gè)叫類型團(tuán)(typeglob)的數(shù)據(jù)結(jié)構(gòu),里面包含了一組名字相同但類型不同的的指針,如*spud里面包含了$spud,@spud,%spud,&spud,spud(句柄)等。通過(guò)符號(hào)表spud項(xiàng)就能找到*spud里的各個(gè)類型。

所以上面那段代碼解釋下就是:先接收訪問(wèn)器和設(shè)置器;然后給類型團(tuán)賦值。這樣以后在調(diào)用函數(shù)get_pie時(shí)就等同于調(diào)用之前接收的那個(gè)訪問(wèn)器($get)。(設(shè)置器set_pie是類似的)

賦值引用到符號(hào)表項(xiàng)就是安裝或替換這個(gè)符號(hào)表項(xiàng)。存儲(chǔ)這個(gè)函數(shù)引用到符號(hào)表,將匿名函數(shù)提升為方法。

賦值一個(gè)符號(hào)表項(xiàng)為字符串,而不是一個(gè)變量名字,這就是一個(gè)符合引用。你必須禁止strict的引用檢查,否則會(huì)報(bào)錯(cuò)。很多程序可能會(huì)這么些:

no strict 'refs';
*{ $methname } = sub {
# subtle bug: strict refs disabled here too
};

但是這類代碼有著相同的BUG:禁用strcit檢查的范圍過(guò)寬,如上例中就在函數(shù)內(nèi)和函數(shù)外都禁用了strcit檢查。正確的做法是僅為需要的操作禁用strcit檢查:

{
my $sub = sub { ... };
no strict 'refs';
*{ $methname } = $sub;
}

如果方法名字是一個(gè)字符串而不是一個(gè)變量?jī)?nèi)容,你可以直接賦值:

{
no warnings 'once';

(*get_pie, *set_pie) =
generate_accessors( 'pie' );
}

直接賦值給符號(hào)表(類型團(tuán))不會(huì)違反strict檢查,但是會(huì)產(chǎn)生告警:每個(gè)glob只使用了一次。你可以通過(guò)禁用該告警來(lái)解決這個(gè)問(wèn)題。

簡(jiǎn)化符號(hào)表的操作
你可以使用CPAN模塊Package::Stash來(lái)簡(jiǎn)化符號(hào)表的操作。

在編譯時(shí)操作

不同于直接寫(xiě)出來(lái)的代碼,通過(guò)eval操作生成的代碼是在運(yùn)行時(shí)進(jìn)行編譯的。當(dāng)你期望一個(gè)普通函數(shù)在程序任何地方都可用時(shí),運(yùn)行時(shí)生成的函數(shù)可能達(dá)不到你的預(yù)期。(因?yàn)橛锌赡芎瘮?shù)還沒(méi)有生成好)

強(qiáng)制Perl在編譯時(shí)就去運(yùn)行生成代碼,可以使用關(guān)鍵字BEGIN來(lái)包含代碼塊。來(lái)對(duì)比下寫(xiě)法上的不同:

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;
}
}

當(dāng)你use一個(gè)模塊時(shí),模塊中函數(shù)之外的代碼都會(huì)被執(zhí)行,這是因?yàn)镻erl會(huì)強(qiáng)制將require和import放到BEGIN塊中,模塊內(nèi)函數(shù)之外的代碼都會(huì)在import()調(diào)用前執(zhí)行。如果僅僅是require一個(gè)模塊那是不會(huì)被放到BEGIN塊中的。

還要注意的是詞法聲明和詞法賦值之間的相互影響,聲明是在編譯時(shí)發(fā)生的,而賦值在代碼運(yùn)行時(shí)才會(huì)發(fā)生。下面這段代碼有個(gè)小錯(cuò)誤:

use UNIVERSAL::require;

my $wanted_package = 'Monkey::Jetpack';

BEGIN
{
$wanted_package->require;
$wanted_package->import;
}

BEGIN塊先執(zhí)行,而此時(shí)$wanted_package還沒(méi)被賦值,這就會(huì)拋出一個(gè)異常:嘗試調(diào)用一個(gè)未定義的值。

Class::MOP

在Perl中可以很方便的就能實(shí)現(xiàn)創(chuàng)建函數(shù)(將函數(shù)引用安裝到名字空間),但是卻幾乎沒(méi)辦法實(shí)現(xiàn)在動(dòng)態(tài)地創(chuàng)建類。后來(lái)Moose和它的Class::MOP庫(kù)帶來(lái)了希望,它提供了一個(gè)元對(duì)象的協(xié)議---一個(gè)通過(guò)修改對(duì)象實(shí)例來(lái)控制面向?qū)ο笙到y(tǒng)的機(jī)制。

相對(duì)于自己動(dòng)手寫(xiě)eval或操作符號(hào)表這樣弱爆了的手段,現(xiàn)在你擁有了更為為大的武器,不僅可以操作實(shí)例,還能操作抽象(使用了面向?qū)ο蟮某绦虻某橄螅?/p>

創(chuàng)建一個(gè)類:

use Class::MOP;

my $class = Class::MOP::Class->create( 'Monkey::Wrench' );

創(chuàng)建的同時(shí)給予屬性和方法:

my $class = Class::MOP::Class->create(
'Monkey::Wrench' =>
(
attributes =>
[
Class::MOP::Attribute->new('$material'),
Class::MOP::Attribute->new('$color'),
]
methods =>
{
tighten => sub { ... },
loosen => sub { ... },
}
),
);

對(duì)于創(chuàng)建過(guò)的類增加屬性和方法:

$class->add_attribute(
experience => Class::MOP::Attribute->new('$xp')
);

$class->add_method( bash_zombie => sub { ... } );

MOP不僅能讓你在運(yùn)行時(shí)創(chuàng)建新實(shí)體還能讓你感知現(xiàn)有的狀態(tài)。比如,你可以使用Class::MOP::Class來(lái)偵測(cè)類的特征:

my @attrs = $class->get_all_attributes;
my @meths = $class->get_all_methods;

類似的Class::MOP::Attribute和Class::MOP::Method也能實(shí)現(xiàn)創(chuàng)建、修改、偵測(cè)類的屬性和方法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容