不同的人對于簡單有著不同的理解母剥。高效的Perl程序員會知道Perl的各個特性是如何相互影響相互作用的,他們的代碼會很好的利用到這些特性。Perl化思維的產(chǎn)物就是簡潔、強大偶宫、流暢和實用的代碼,關(guān)鍵在于當(dāng)你理解Perl化思維后就會發(fā)現(xiàn)這一切都非常簡單环鲤。
習(xí)慣用法(成語)
每個語言都有其公認(rèn)的表達(dá)模式或習(xí)慣用法纯趋。比如事實上是地球公轉(zhuǎn),但我們卻都說是太陽升起冷离、落下吵冒。我們崇拜駭客的聰明但是討厭他們那讓人迷惑的代碼。
Perl中的習(xí)慣用法就是語言特性和設(shè)計模式的利用西剥。并不是必須要使用這些你才能完成工作痹栖,但是這些習(xí)慣用法的確能讓你的代碼更具Perl口音(參考英語中的倫敦腔)、且更加犀利瞭空。
$self
Moose系統(tǒng)會把方法的調(diào)用者看作一個普通的參數(shù)揪阿。無論是調(diào)用類方法還是實例方法,數(shù)組@_中的第一個元素總是調(diào)用者咆畏。按照慣例南捂,大多數(shù)Perl代碼使用變量$class來保存類方法的調(diào)用者;使用變量$self來保存對象方法的調(diào)用者旧找。很多模塊遵循了這個約定溺健,比如Moops就會假設(shè)你是使用$self來保存對象調(diào)用者的。
有名字的參數(shù)
Perl喜歡列表钮蛛。列表是Perl中的基本元素鞭缭。列表具有的扁平化特性和連接特性可以讓你靈活而輕松地實現(xiàn)串聯(lián)表達(dá)式和操縱數(shù)據(jù)剖膳。
雖然Perl的傳參很簡單(任何東西都壓平放進(jìn)@_),有些時候我們認(rèn)為這種方式過于簡單了×肜保現(xiàn)在我們稍微轉(zhuǎn)換下思路:將@_放在列表語境下看成是有名字的參數(shù)對潮秘。胖箭頭操作符可以將一個普通的列表,打扮成更加明顯的成對參數(shù):
make_ice_cream_sundae(
whipped_cream => 1,
sprinkles => 1,
banana => 0,
ice_cream => 'mint chocolate chip',
);
我們可以將參數(shù)放到哈希里面易结,這樣就能看成是單一參數(shù):
sub make_ice_cream_sundae{
my %args = @_;
my $dessert = get_ice_cream( $args{ice_cream} );
...
}
哈希還是哈希引用
《Perl最佳實踐》建議傳遞哈希引用枕荞。這樣就可以在主調(diào)端對哈希引用做驗證。換句話說搞动,如果你傳遞的參數(shù)數(shù)量不對躏精,就能在調(diào)用函數(shù)時得到錯誤提示。
這個技術(shù)可以很好和import()方法或其他方法協(xié)同工作鹦肿,在將參數(shù)賦值到哈希前進(jìn)行必要的處理:
sub import{
my ($class, %args) = @_;
my $calling_package = caller();
...
}
施瓦茨變換
施瓦茨變換就是一個優(yōu)雅的習(xí)慣用法范例:Perl從Lisp借過來的處理列表的方式矗烛。
假設(shè)你有一個哈希,存儲著名字和電話號碼:
my %extensions =(
'000' => 'Damian',
'002' => 'Wesley',
'012' => 'LaMarcus',
'042' => 'Robin',
'088' => 'Nic',
);
哈希鍵的引起規(guī)則
胖箭頭對鍵的自動引起僅僅在看起來像裸字時起作用箩溃。對于以0開頭的瞭吃,看起來更像一個八進(jìn)制數(shù)字,所以要手動引起涣旨。幾乎所有人都會犯過這個錯誤歪架。
現(xiàn)在要對名字按字母順序進(jìn)行排序,你就必須以這個哈希的值排序霹陡,而不是鍵和蚪。當(dāng)然排序很容易的:
my @sorted_names = sort values %extensions;
但是你需要一個額外的步驟來保留其中關(guān)聯(lián)信息,這就是施瓦茨變換了烹棉。首先將哈希放入一個容易進(jìn)行排序處理的列表中攒霹,本例中就是兩個元素的匿名數(shù)字:
my @pairs = map { [ $_, $extensions{$_} ] }keys %extensions;
sort函數(shù)接受一系列的匿名數(shù)組,并比對他們的第2個元素(也就是人名):
my @sorted_pairs = sort { $a->[1] cmp $b->[1] } @pairs;
提供給sort的程序塊接受2個參數(shù):包變量$a和$b浆洗。@pairs第一個元素就是$a的內(nèi)容催束;第二個元素就是$b的內(nèi)容。如果$a的內(nèi)容應(yīng)該排在$b內(nèi)容的前面伏社,那么程序塊返回-1抠刺;如果2個內(nèi)容值相同(也就是應(yīng)該排在同樣的位置),程序塊就返回0洛口;最后矫付,如果$a的內(nèi)容應(yīng)該排在$b內(nèi)容的后面凯沪,程序塊就返回1 第焰;其他返回值均表示發(fā)生錯誤。
了解數(shù)據(jù)的特征
如果沒有相同的名稱妨马,那么通過反轉(zhuǎn)哈希也能方便的實現(xiàn)目標(biāo)挺举。本例中這個特定的數(shù)據(jù)集沒有相同的名稱杀赢,但是程序應(yīng)該考慮得全面。
cmp操作符用于比較字符串湘纵,飛碟操作符<=>用于比較數(shù)字脂崔。對于@sorted_pairs,可以轉(zhuǎn)換為更合適的形式:
my @formatted_exts = map { "$_->[1], ext. $_->[0]" } @sorted_pairs;
現(xiàn)在可以打印出來了:
say for @formatted_exts;
使用施瓦茨變換將所有的表達(dá)式串聯(lián)起來梧喷,還能消去臨時變量:
say for
map { " $_->[1], ext. $_->[0]" }
sort { $a->[1] cmp $b->[1] }
map { [ $_ => $extensions{$_} ] }
keys %extensions;
閱讀表達(dá)式的順序是從右至左砌左,因為計算也是這個順序。根據(jù)哈希extensions中的每一個鍵铺敌,創(chuàng)建一個2元素的匿名數(shù)組:鍵和值汇歹;針對匿名數(shù)組中的第2個元素排序(也就是值);然后將排序好的數(shù)組格式化輸出偿凭。
實際上可以看成是一系列管道m(xù)ap-sort-map产弹,將一個數(shù)據(jù)結(jié)構(gòu)變換成一個更容易處理的形式。
這個例子的排序很簡單弯囊,但是如果數(shù)據(jù)量超大會怎么樣呢痰哨?這時施瓦茨變換就顯得尤其有用了,因為它緩存了昂貴的計算操作匾嘱,實際上只會在最先的map中執(zhí)行一次斤斧。
一次性讀取文件的全部內(nèi)容
local是用于管理Perl全局魔法變量必不可少的工具。你必須理解作用域才能用好local霎烙。如果使用local折欠,應(yīng)盡量控制在需要的最小作用域中。例如吼过,一個表達(dá)式就能實現(xiàn)將文件內(nèi)容讀到一個標(biāo)量中:
my $file = do { local $/; <$fh> };
# 或者
my $file; { local $/; $file = <$fh> };
變量$/是輸入記錄的分隔符锐秦。臨時設(shè)置它的值為undef--待賦值。一旦分隔符的值是未定義的盗忱,Perl就會一下讀取文件句柄中的所有內(nèi)容酱床。do程序塊的值就是塊中最后一個表達(dá)式的值:從文件句柄$fh讀取的內(nèi)容--文件的內(nèi)容。超出程序塊后$/恢復(fù)為以前的值趟佃,同時$file也獲得文件的全部內(nèi)容扇谣。
第二個示例代碼避免了第二次的文件內(nèi)容復(fù)制;沒那么好看但是內(nèi)存使用更少闲昭。
File::Slurp
這個例子很實用罐寨,但是對于那些不理解local和作用域的人來說則很抓狂。幸好CPAN上有個叫File::Slurp的模塊也能實現(xiàn)該功能序矩。
處理main函數(shù)
Perl創(chuàng)建閉包不需要特別的語法鸯绿。你可能不經(jīng)意間就關(guān)閉了一個詞法變量。很多程序會在其他函數(shù)未處理妥善前就設(shè)置一些整個文件有效(作用域為整個文件)的詞法變量。相對于向函數(shù)傳值和從函數(shù)返回值瓶蝴,人們更傾向直接使用變量毒返。不幸的是,這些程序可能會依于賴編譯過程--你認(rèn)為變量應(yīng)該已經(jīng)初始化為一個特定的值了舷手,但實際上可能并沒有拧簸,直到某個時間之后才會初始化。要記住Perl創(chuàng)建閉包不需要特殊的語法--所以你可能在不經(jīng)意間就關(guān)閉了一個詞法變量男窟。(創(chuàng)建了閉包盆赤?)
為了避免這種情況,可以將你程序的主要代碼用一個單獨的函數(shù)包裹起來歉眷,如main()函數(shù)弟劲,這樣就能將變量封裝在正確的作用域。然后在加載模塊和編譯指示之后增加一行:
#!/usr/bin/perl
use Modern::Perl;
exit main( @ARGV ); #這一行
sub main {
...
# successful exit
return 0;
}
最開始就調(diào)用main()來明確初始化和編譯順序姥芥。以main()的返回值來調(diào)用exit來防止運行其他裸露的代碼兔乞。
受控執(zhí)行
程序和模塊實際的區(qū)別就是它們的用途。用戶直接調(diào)用程序凉唐,程序執(zhí)行時加載模塊庸追。然而模塊和程序都是Perl代碼,要讓模塊運行起來也很容易台囱。所以應(yīng)該讓程序的行為像模塊淡溯。(這樣可以對程序的某一部分進(jìn)行測試,而不用正式的造一個模塊)簿训。要做到這些只需要你了解Perl是如何執(zhí)行一段代碼的就夠了咱娶。
以前介紹過caller函數(shù),它的參數(shù)即調(diào)用框架的層數(shù)强品, caller(0)會報告當(dāng)前調(diào)用框架的信息膘侮。要讓一個模塊像程序一樣正確地運行起來,那就將所有可執(zhí)行的代碼放到main()函數(shù)里的榛,然后在開始位置增加一行:
main() unless caller(0);
代碼的意思是:如果沒有東西來調(diào)用這個模塊那就直接執(zhí)行main函數(shù)琼了。
更好的調(diào)用偵測
在列表語境中,如果使用的是use或require調(diào)用的夫晌,那么caller返回值的第8個元素是真值雕薪,其他方式都是undef。這個更準(zhǔn)確的晓淀,但是很少人使用所袁。
參數(shù)驗證后置
CPAN上有幾個模塊能幫助你對函數(shù)的參數(shù)進(jìn)行驗證,如Params::Validate和MooseX::Params::Validate凶掰。一些簡單的驗證當(dāng)然不值得動用這些牛刀燥爷。
假設(shè)你的函數(shù)僅接受2個參數(shù)蜈亩,你可以這樣來驗證:
use Carp 'croak';
sub groom_monkeys{
if (@_ != 2){
croak 'Can only groom two monkeys!';
}
...
}
但是從語言學(xué)的角度來講,結(jié)果比檢查更重要局劲,所以應(yīng)該將位置提前:
croak 'Can only groom two monkeys!' unless @_ == 2;
#很顯然這種后綴表達(dá)式的方式用起來更爽勺拣。
還有個叫函數(shù)簽名機制也能實現(xiàn)本例中的參數(shù)驗證奶赠。
正則賦值
很多Perl的習(xí)慣用法會用到表達(dá)式賦值:
say my $ext_num = my $extension = 42;
這個代碼很丑鱼填,但它演示了如何在一個表達(dá)式中使用另一個表達(dá)式的值。這不是什么新東西毅戈,我們之前已經(jīng)使用過了:在列表中使用一個函數(shù)的返回值苹丸,或者在一個函數(shù)的參數(shù)中使用另一個函數(shù)的返回值作為參數(shù)。當(dāng)時你可能還沒有意識到它們的含義苇经。
假設(shè)你想要使用正則表達(dá)式從全名中提取名的部分赘理,可以這樣:
my ($first_name) = $name =~ /($first_name_rx)/;
在列表語境中,一個成功匹配的正則表達(dá)式會返回捕獲的列表扇单。
要創(chuàng)建用戶的系統(tǒng)賬號需要刪除所有非單詞字符商模,這樣寫:
(my $normalized_name = $name) =~ tr/A-Za-z//dc;
首先,會對$normalized_name進(jìn)行賦值蜘澜,因為括號的優(yōu)先級高施流;然后對變量$normalized_name進(jìn)行轉(zhuǎn)換操作。
無損替換
新代碼(Perl 5.14之后)可以使用無損替換操作符/r:my $normalized_name = $name =~ tr/A-Za-z//dcr;
鄙信。
這種技術(shù)也適用于其他類似的就地修改操作:
my $age = 14;
(my $next_age = $age)++;
say "I am $age, but next year I will be $next_age";
一元強制
只要你選擇了正確的操作符瞪醋,Perl的類型系統(tǒng)就不會搞錯。使用字符串連接符時装诡,Perl就會將2個操作數(shù)都視為字符串银受;使用加號操作符時,Perl就會將操作數(shù)都視為數(shù)字鸦采。
但有些時候宾巍,Perl需要你給它一點暗示,這時你可以通過使用一元強制符來表明你的意圖渔伯。
通過增加0來表明想要的是數(shù)字:
my $numeric_value = 0 + $value;
雙重否定表明為布爾類型:
my $boolean_value = !! $value;
連接空字符來表明這是字符串:
my $string_value = '' . $value;
盡管用到這種技術(shù)的場景微乎其微蜀漆,但你應(yīng)該知道這種習(xí)慣用法。不這樣做可能也不會出錯咱旱,但是強烈建議你要明確地表明自己的意圖确丢。
全局變量
Perl提供一些超級全局變量,作用范圍(作用域)比包或文件還要大吐限。作用域大意味著沖突的幾率大鲜侥,任何直接或間接的修改都可能影響到程序的其他部分。全局變量有很多诸典,少有人能記住全部--也沒有那個必要描函,只有其中的一小部分會被經(jīng)常使用到。perldoc perlvar有這些變量的詳盡列表。
管理超級全局行為
隨著Perl的發(fā)展舀寓,已經(jīng)將很多全局行為改成詞法行為了胆数,使用全局行為的場景大幅減少。當(dāng)你無法避開全局行為時互墓,可使用local來將行為限制在最小的作用域必尼,就像之前介紹的讀取文件全部內(nèi)容的例子那樣:
my $file; { local $/; $file = <$fh> };
本地化的$/,只在塊中有效篡撵。這里還有個極低的可能發(fā)生的事情:那就是在程序塊中修改$/的值--讀取文件句柄的內(nèi)容作為Perl代碼執(zhí)行并改變$/的值判莉。
并不是在所有的情況下都能如此簡單地使用全局變量,但是通常都可以育谬。
有些時候你需要獲取超級全局變量的值券盅,同時希望不受其他代碼干擾。使用eval捕獲異常時也可能會收到干擾膛檀,比如在超出作用域時調(diào)用DESTROY()方法就可能會重置$@锰镀。
local $@;
eval { ... };
if (my $exception = $@) { ... }
捕獲異常時立即復(fù)制$@的值以避免后續(xù)的修改。
英文名字
核心模塊English為這些標(biāo)點符號的變量提供了詳細(xì)的英文名字咖刃。這樣使用:
use English '-no_match_vars'; # unnecessary in 5.20 and 5.22
這將允許你在該編譯指示的作用域內(nèi)使用變量對應(yīng)的英文名字泳炉,具體名字請查看perldoc perlvar。
有用的超級全局變量
大多數(shù)程序只會使用到為數(shù)不多的幾個超級全局變量僵缺,這些是你最有可能遇到的:
$/ 輸入記錄分隔符胡桃,讀取內(nèi)容時用于標(biāo)識行尾。
$. 讀取內(nèi)容的行數(shù)磕潮。
$| 控制著是否自動立即刷新緩沖翠胰。
@ARGV 存儲著命令行參數(shù)。
$! 保留著最近系統(tǒng)調(diào)用的結(jié)果自脯,它是雙變量之景,在數(shù)字語境中相當(dāng)于C語言中errno的值,非零值表示錯誤膏潮;在字符串語境中通常返回系統(tǒng)錯誤的描述信息锻狗。使用時應(yīng)盡量避免受到該變量受到其他代碼的影響(上文介紹的本地化、立即復(fù)制等)焕参。
$" 列表分隔符轻纪,在字符串語境中進(jìn)行數(shù)組或列表內(nèi)插時,作為元素之間的連接符叠纷。
%+ 正則表達(dá)式匹配成功時存儲著命令捕獲的結(jié)果刻帚。
$@ 保存著最近的異常的拋出的值
$0 當(dāng)前執(zhí)行的程序名,在類unix系統(tǒng)中可以修改該值以改變在在其他程序中的顯示值涩嚣,如ps或top崇众。
$$ 進(jìn)程IP號掂僵。
@INC 保存著所有的文件系統(tǒng)路徑,這些路徑用于Perl在use或require加載文件時查找文件顷歌。
%SIG 保存著信號和信號處理函數(shù)的映射锰蓬。欲了解具體細(xì)節(jié)請查看perldoc perlipc。
超級全局行為的替代方案
程序中通常最容易出岔子的地方就是IO和異常眯漩,我們可以使用Try::Tiny來進(jìn)行異常處理芹扭;使用本地化和立即復(fù)制$!的值來避免Perl在系統(tǒng)調(diào)用時出現(xiàn)奇怪的行為;使用IO::File和詞法文件句柄來避免不想要的全局IO行為坤塞。