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

代碼生成

新手程序員往往會(huì)寫多余的代碼绅项。一開始他們寫的代碼很長紊册,再后來會(huì)學(xué)會(huì)使用函數(shù)、使用參數(shù)快耿,再后來會(huì)使用面向?qū)ο竽叶浮⒏唠A函數(shù)和閉包--技能逐漸提升,代碼越來越簡練掀亥。

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

讓你所寫的程序來為你編程就叫元編程或代碼生成。相對(duì)于代碼重用吼拥,元編程能讓你的抽象重用倚聚。

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

eval

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

一個(gè)常見用途就是在你無法加載一個(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)義的問題粗卜。通過插入一些變量來增加復(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
}

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

帶參數(shù)的閉包

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

將訪問器和修改器安裝到符號(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è)名字空間洒扎,里面包含了全局可訪問的符號(hào)如包全局變量、函數(shù)和方法衰絮。

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

所以上面那段代碼解釋下就是:先接收訪問器和設(shè)置器;然后給類型團(tuán)賦值淌友。這樣以后在調(diào)用函數(shù)get_pie時(shí)就等同于調(diào)用之前接收的那個(gè)訪問器($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檢查的范圍過寬肴颊,如上例中就在函數(shù)內(nèi)和函數(shù)外都禁用了strcit檢查。正確的做法是僅為需要的操作禁用strcit檢查:

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

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

{
no warnings 'once';

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

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

簡化符號(hào)表的操作
你可以使用CPAN模塊Package::Stash來簡化符號(hào)表的操作祟身。

在編譯時(shí)操作

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

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

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還沒被賦值孩锡,這就會(huì)拋出一個(gè)異常:嘗試調(diào)用一個(gè)未定義的值。

Class::MOP

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

相對(duì)于自己動(dòng)手寫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)建過的類增加屬性和方法:

$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來偵測類的特征:

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末贩疙,一起剝皮案震驚了整個(gè)濱河市讹弯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖熟呛,帶你破解...
    沈念sama閱讀 222,865評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件没咙,死亡現(xiàn)場離奇詭異,居然都是意外死亡派殷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來癞尚,“玉大人,你說我怎么就攤上這事乱陡〗娇” “怎么了?”我有些...
    開封第一講書人閱讀 169,631評(píng)論 0 364
  • 文/不壞的土叔 我叫張陵憨颠,是天一觀的道長胳徽。 經(jīng)常有香客問我积锅,道長,這世上最難降的妖魔是什么养盗? 我笑而不...
    開封第一講書人閱讀 60,199評(píng)論 1 300
  • 正文 為了忘掉前任缚陷,我火速辦了婚禮,結(jié)果婚禮上往核,老公的妹妹穿的比我還像新娘箫爷。我一直安慰自己,他們只是感情好聂儒,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,196評(píng)論 6 398
  • 文/花漫 我一把揭開白布虎锚。 她就那樣靜靜地躺著,像睡著了一般薄货。 火紅的嫁衣襯著肌膚如雪翁都。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,793評(píng)論 1 314
  • 那天谅猾,我揣著相機(jī)與錄音柄慰,去河邊找鬼。 笑死税娜,一個(gè)胖子當(dāng)著我的面吹牛坐搔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播敬矩,決...
    沈念sama閱讀 41,221評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼概行,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了弧岳?” 一聲冷哼從身側(cè)響起凳忙,我...
    開封第一講書人閱讀 40,174評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎禽炬,沒想到半個(gè)月后涧卵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,699評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡腹尖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,770評(píng)論 3 343
  • 正文 我和宋清朗相戀三年柳恐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片热幔。...
    茶點(diǎn)故事閱讀 40,918評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡乐设,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绎巨,到底是詐尸還是另有隱情近尚,我是刑警寧澤,帶...
    沈念sama閱讀 36,573評(píng)論 5 351
  • 正文 年R本政府宣布场勤,位于F島的核電站戈锻,受9級(jí)特大地震影響介汹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜舶沛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,255評(píng)論 3 336
  • 文/蒙蒙 一嘹承、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧如庭,春花似錦叹卷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,749評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至往毡,卻和暖如春蒙揣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背开瞭。 一陣腳步聲響...
    開封第一講書人閱讀 33,862評(píng)論 1 274
  • 我被黑心中介騙來泰國打工懒震, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嗤详。 一個(gè)月前我還...
    沈念sama閱讀 49,364評(píng)論 3 379
  • 正文 我出身青樓个扰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親葱色。 傳聞我的和親對(duì)象是個(gè)殘疾皇子递宅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,926評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容