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

代碼生成

新手程序員往往會寫多余的代碼。一開始他們寫的代碼很長,再后來會學會使用函數(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)建、修改、偵測類的屬性和方法。

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

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

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