不同的人對(duì)于簡單有著不同的理解。高效的Perl程序員會(huì)知道Perl的各個(gè)特性是如何相互影響相互作用的,他們的代碼會(huì)很好的利用到這些特性。Perl化思維的產(chǎn)物就是簡潔、強(qiáng)大、流暢和實(shí)用的代碼,關(guān)鍵在于當(dāng)你理解Perl化思維后就會(huì)發(fā)現(xiàn)這一切都非常簡單。
習(xí)慣用法(成語)
每個(gè)語言都有其公認(rèn)的表達(dá)模式或習(xí)慣用法。比如事實(shí)上是地球公轉(zhuǎn),但我們卻都說是太陽升起、落下。我們崇拜駭客的聰明但是討厭他們那讓人迷惑的代碼。
Perl中的習(xí)慣用法就是語言特性和設(shè)計(jì)模式的利用。并不是必須要使用這些你才能完成工作,但是這些習(xí)慣用法的確能讓你的代碼更具Perl口音(參考英語中的倫敦腔)、且更加犀利。
$self
Moose系統(tǒng)會(huì)把方法的調(diào)用者看作一個(gè)普通的參數(shù)。無論是調(diào)用類方法還是實(shí)例方法,數(shù)組@_中的第一個(gè)元素總是調(diào)用者。按照慣例,大多數(shù)Perl代碼使用變量$class來保存類方法的調(diào)用者;使用變量$self來保存對(duì)象方法的調(diào)用者。很多模塊遵循了這個(gè)約定,比如Moops就會(huì)假設(shè)你是使用$self來保存對(duì)象調(diào)用者的。
有名字的參數(shù)
Perl喜歡列表。列表是Perl中的基本元素。列表具有的扁平化特性和連接特性可以讓你靈活而輕松地實(shí)現(xiàn)串聯(lián)表達(dá)式和操縱數(shù)據(jù)。
雖然Perl的傳參很簡單(任何東西都?jí)浩椒胚M(jìn)@_),有些時(shí)候我們認(rèn)為這種方式過于簡單了?,F(xiàn)在我們稍微轉(zhuǎn)換下思路:將@_放在列表語境下看成是有名字的參數(shù)對(duì)。胖箭頭操作符可以將一個(gè)普通的列表,打扮成更加明顯的成對(duì)參數(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最佳實(shí)踐》建議傳遞哈希引用。這樣就可以在主調(diào)端對(duì)哈希引用做驗(yàn)證。換句話說,如果你傳遞的參數(shù)數(shù)量不對(duì),就能在調(diào)用函數(shù)時(shí)得到錯(cuò)誤提示。
這個(gè)技術(shù)可以很好和import()方法或其他方法協(xié)同工作,在將參數(shù)賦值到哈希前進(jìn)行必要的處理:
sub import{
my ($class, %args) = @_;
my $calling_package = caller();
...
}
施瓦茨變換
施瓦茨變換就是一個(gè)優(yōu)雅的習(xí)慣用法范例:Perl從Lisp借過來的處理列表的方式。
假設(shè)你有一個(gè)哈希,存儲(chǔ)著名字和電話號(hào)碼:
my %extensions =(
'000' => 'Damian',
'002' => 'Wesley',
'012' => 'LaMarcus',
'042' => 'Robin',
'088' => 'Nic',
);
哈希鍵的引起規(guī)則
胖箭頭對(duì)鍵的自動(dòng)引起僅僅在看起來像裸字時(shí)起作用。對(duì)于以0開頭的,看起來更像一個(gè)八進(jìn)制數(shù)字,所以要手動(dòng)引起。幾乎所有人都會(huì)犯過這個(gè)錯(cuò)誤。
現(xiàn)在要對(duì)名字按字母順序進(jìn)行排序,你就必須以這個(gè)哈希的值排序,而不是鍵。當(dāng)然排序很容易的:
my @sorted_names = sort values %extensions;
但是你需要一個(gè)額外的步驟來保留其中關(guān)聯(lián)信息,這就是施瓦茨變換了。首先將哈希放入一個(gè)容易進(jìn)行排序處理的列表中,本例中就是兩個(gè)元素的匿名數(shù)字:
my @pairs = map { [ $_, $extensions{$_} ] }keys %extensions;
sort函數(shù)接受一系列的匿名數(shù)組,并比對(duì)他們的第2個(gè)元素(也就是人名):
my @sorted_pairs = sort { $a->[1] cmp $b->[1] } @pairs;
提供給sort的程序塊接受2個(gè)參數(shù):包變量$a和$b。@pairs第一個(gè)元素就是$a的內(nèi)容;第二個(gè)元素就是$b的內(nèi)容。如果$a的內(nèi)容應(yīng)該排在$b內(nèi)容的前面,那么程序塊返回-1;如果2個(gè)內(nèi)容值相同(也就是應(yīng)該排在同樣的位置),程序塊就返回0;最后,如果$a的內(nèi)容應(yīng)該排在$b內(nèi)容的后面,程序塊就返回1 ;其他返回值均表示發(fā)生錯(cuò)誤。
了解數(shù)據(jù)的特征
如果沒有相同的名稱,那么通過反轉(zhuǎn)哈希也能方便的實(shí)現(xiàn)目標(biāo)。本例中這個(gè)特定的數(shù)據(jù)集沒有相同的名稱,但是程序應(yīng)該考慮得全面。
cmp操作符用于比較字符串,飛碟操作符<=>用于比較數(shù)字。對(duì)于@sorted_pairs,可以轉(zhuǎn)換為更合適的形式:
my @formatted_exts = map { "$_->[1], ext. $_->[0]" } @sorted_pairs;
現(xiàn)在可以打印出來了:
say for @formatted_exts;
使用施瓦茨變換將所有的表達(dá)式串聯(lián)起來,還能消去臨時(shí)變量:
say for
map { " $_->[1], ext. $_->[0]" }
sort { $a->[1] cmp $b->[1] }
map { [ $_ => $extensions{$_} ] }
keys %extensions;
閱讀表達(dá)式的順序是從右至左,因?yàn)橛?jì)算也是這個(gè)順序。根據(jù)哈希extensions中的每一個(gè)鍵,創(chuàng)建一個(gè)2元素的匿名數(shù)組:鍵和值;針對(duì)匿名數(shù)組中的第2個(gè)元素排序(也就是值);然后將排序好的數(shù)組格式化輸出。
實(shí)際上可以看成是一系列管道m(xù)ap-sort-map,將一個(gè)數(shù)據(jù)結(jié)構(gòu)變換成一個(gè)更容易處理的形式。
這個(gè)例子的排序很簡單,但是如果數(shù)據(jù)量超大會(huì)怎么樣呢?這時(shí)施瓦茨變換就顯得尤其有用了,因?yàn)樗彺媪税嘿F的計(jì)算操作,實(shí)際上只會(huì)在最先的map中執(zhí)行一次。
一次性讀取文件的全部內(nèi)容
local是用于管理Perl全局魔法變量必不可少的工具。你必須理解作用域才能用好local。如果使用local,應(yīng)盡量控制在需要的最小作用域中。例如,一個(gè)表達(dá)式就能實(shí)現(xiàn)將文件內(nèi)容讀到一個(gè)標(biāo)量中:
my $file = do { local $/; <$fh> };
# 或者
my $file; { local $/; $file = <$fh> };
變量$/是輸入記錄的分隔符。臨時(shí)設(shè)置它的值為undef--待賦值。一旦分隔符的值是未定義的,Perl就會(huì)一下讀取文件句柄中的所有內(nèi)容。do程序塊的值就是塊中最后一個(gè)表達(dá)式的值:從文件句柄$fh讀取的內(nèi)容--文件的內(nèi)容。超出程序塊后$/恢復(fù)為以前的值,同時(shí)$file也獲得文件的全部內(nèi)容。
第二個(gè)示例代碼避免了第二次的文件內(nèi)容復(fù)制;沒那么好看但是內(nèi)存使用更少。
File::Slurp
這個(gè)例子很實(shí)用,但是對(duì)于那些不理解local和作用域的人來說則很抓狂。幸好CPAN上有個(gè)叫File::Slurp的模塊也能實(shí)現(xiàn)該功能。
處理main函數(shù)
Perl創(chuàng)建閉包不需要特別的語法。你可能不經(jīng)意間就關(guān)閉了一個(gè)詞法變量。很多程序會(huì)在其他函數(shù)未處理妥善前就設(shè)置一些整個(gè)文件有效(作用域?yàn)檎麄€(gè)文件)的詞法變量。相對(duì)于向函數(shù)傳值和從函數(shù)返回值,人們更傾向直接使用變量。不幸的是,這些程序可能會(huì)依于賴編譯過程--你認(rèn)為變量應(yīng)該已經(jīng)初始化為一個(gè)特定的值了,但實(shí)際上可能并沒有,直到某個(gè)時(shí)間之后才會(huì)初始化。要記住Perl創(chuàng)建閉包不需要特殊的語法--所以你可能在不經(jīng)意間就關(guān)閉了一個(gè)詞法變量。(創(chuàng)建了閉包?)
為了避免這種情況,可以將你程序的主要代碼用一個(gè)單獨(dú)的函數(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來防止運(yùn)行其他裸露的代碼。
受控執(zhí)行
程序和模塊實(shí)際的區(qū)別就是它們的用途。用戶直接調(diào)用程序,程序執(zhí)行時(shí)加載模塊。然而模塊和程序都是Perl代碼,要讓模塊運(yùn)行起來也很容易。所以應(yīng)該讓程序的行為像模塊。(這樣可以對(duì)程序的某一部分進(jìn)行測試,而不用正式的造一個(gè)模塊)。要做到這些只需要你了解Perl是如何執(zhí)行一段代碼的就夠了。
以前介紹過caller函數(shù),它的參數(shù)即調(diào)用框架的層數(shù), caller(0)會(huì)報(bào)告當(dāng)前調(diào)用框架的信息。要讓一個(gè)模塊像程序一樣正確地運(yùn)行起來,那就將所有可執(zhí)行的代碼放到main()函數(shù)里,然后在開始位置增加一行:
main() unless caller(0);
代碼的意思是:如果沒有東西來調(diào)用這個(gè)模塊那就直接執(zhí)行main函數(shù)。
更好的調(diào)用偵測
在列表語境中,如果使用的是use或require調(diào)用的,那么caller返回值的第8個(gè)元素是真值,其他方式都是undef。這個(gè)更準(zhǔn)確的,但是很少人使用。
參數(shù)驗(yàn)證后置
CPAN上有幾個(gè)模塊能幫助你對(duì)函數(shù)的參數(shù)進(jìn)行驗(yàn)證,如Params::Validate和MooseX::Params::Validate。一些簡單的驗(yàn)證當(dāng)然不值得動(dòng)用這些牛刀。
假設(shè)你的函數(shù)僅接受2個(gè)參數(shù),你可以這樣來驗(yàn)證:
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á)式的方式用起來更爽。
還有個(gè)叫函數(shù)簽名機(jī)制也能實(shí)現(xiàn)本例中的參數(shù)驗(yàn)證。
正則賦值
很多Perl的習(xí)慣用法會(huì)用到表達(dá)式賦值:
say my $ext_num = my $extension = 42;
這個(gè)代碼很丑,但它演示了如何在一個(gè)表達(dá)式中使用另一個(gè)表達(dá)式的值。這不是什么新東西,我們之前已經(jīng)使用過了:在列表中使用一個(gè)函數(shù)的返回值,或者在一個(gè)函數(shù)的參數(shù)中使用另一個(gè)函數(shù)的返回值作為參數(shù)。當(dāng)時(shí)你可能還沒有意識(shí)到它們的含義。
假設(shè)你想要使用正則表達(dá)式從全名中提取名的部分,可以這樣:
my ($first_name) = $name =~ /($first_name_rx)/;
在列表語境中,一個(gè)成功匹配的正則表達(dá)式會(huì)返回捕獲的列表。
要?jiǎng)?chuàng)建用戶的系統(tǒng)賬號(hào)需要?jiǎng)h除所有非單詞字符,這樣寫:
(my $normalized_name = $name) =~ tr/A-Za-z//dc;
首先,會(huì)對(duì)$normalized_name進(jìn)行賦值,因?yàn)槔ㄌ?hào)的優(yōu)先級(jí)高;然后對(duì)變量$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";
一元強(qiáng)制
只要你選擇了正確的操作符,Perl的類型系統(tǒng)就不會(huì)搞錯(cuò)。使用字符串連接符時(shí),Perl就會(huì)將2個(gè)操作數(shù)都視為字符串;使用加號(hào)操作符時(shí),Perl就會(huì)將操作數(shù)都視為數(shù)字。
但有些時(shí)候,Perl需要你給它一點(diǎn)暗示,這時(shí)你可以通過使用一元強(qiáng)制符來表明你的意圖。
通過增加0來表明想要的是數(shù)字:
my $numeric_value = 0 + $value;
雙重否定表明為布爾類型:
my $boolean_value = !! $value;
連接空字符來表明這是字符串:
my $string_value = '' . $value;
盡管用到這種技術(shù)的場景微乎其微,但你應(yīng)該知道這種習(xí)慣用法。不這樣做可能也不會(huì)出錯(cuò),但是強(qiáng)烈建議你要明確地表明自己的意圖。
全局變量
Perl提供一些超級(jí)全局變量,作用范圍(作用域)比包或文件還要大。作用域大意味著沖突的幾率大,任何直接或間接的修改都可能影響到程序的其他部分。全局變量有很多,少有人能記住全部--也沒有那個(gè)必要,只有其中的一小部分會(huì)被經(jīng)常使用到。perldoc perlvar有這些變量的詳盡列表。
管理超級(jí)全局行為
隨著Perl的發(fā)展,已經(jīng)將很多全局行為改成詞法行為了,使用全局行為的場景大幅減少。當(dāng)你無法避開全局行為時(shí),可使用local來將行為限制在最小的作用域,就像之前介紹的讀取文件全部內(nèi)容的例子那樣:
my $file; { local $/; $file = <$fh> };
本地化的$/,只在塊中有效。這里還有個(gè)極低的可能發(fā)生的事情:那就是在程序塊中修改$/的值--讀取文件句柄的內(nèi)容作為Perl代碼執(zhí)行并改變$/的值。
并不是在所有的情況下都能如此簡單地使用全局變量,但是通常都可以。
有些時(shí)候你需要獲取超級(jí)全局變量的值,同時(shí)希望不受其他代碼干擾。使用eval捕獲異常時(shí)也可能會(huì)收到干擾,比如在超出作用域時(shí)調(diào)用DESTROY()方法就可能會(huì)重置$@。
local $@;
eval { ... };
if (my $exception = $@) { ... }
捕獲異常時(shí)立即復(fù)制$@的值以避免后續(xù)的修改。
英文名字
核心模塊English為這些標(biāo)點(diǎn)符號(hào)的變量提供了詳細(xì)的英文名字。這樣使用:
use English '-no_match_vars'; # unnecessary in 5.20 and 5.22
這將允許你在該編譯指示的作用域內(nèi)使用變量對(duì)應(yīng)的英文名字,具體名字請(qǐng)查看perldoc perlvar。
有用的超級(jí)全局變量
大多數(shù)程序只會(huì)使用到為數(shù)不多的幾個(gè)超級(jí)全局變量,這些是你最有可能遇到的:
$/ 輸入記錄分隔符,讀取內(nèi)容時(shí)用于標(biāo)識(shí)行尾。
$. 讀取內(nèi)容的行數(shù)。
$| 控制著是否自動(dòng)立即刷新緩沖。
@ARGV 存儲(chǔ)著命令行參數(shù)。
$! 保留著最近系統(tǒng)調(diào)用的結(jié)果,它是雙變量,在數(shù)字語境中相當(dāng)于C語言中errno的值,非零值表示錯(cuò)誤;在字符串語境中通常返回系統(tǒng)錯(cuò)誤的描述信息。使用時(shí)應(yīng)盡量避免受到該變量受到其他代碼的影響(上文介紹的本地化、立即復(fù)制等)。
$" 列表分隔符,在字符串語境中進(jìn)行數(shù)組或列表內(nèi)插時(shí),作為元素之間的連接符。
%+ 正則表達(dá)式匹配成功時(shí)存儲(chǔ)著命令捕獲的結(jié)果。
$@ 保存著最近的異常的拋出的值
$0 當(dāng)前執(zhí)行的程序名,在類unix系統(tǒng)中可以修改該值以改變?cè)谠谄渌绦蛑械娘@示值,如ps或top。
$$ 進(jìn)程IP號(hào)。
@INC 保存著所有的文件系統(tǒng)路徑,這些路徑用于Perl在use或require加載文件時(shí)查找文件。
%SIG 保存著信號(hào)和信號(hào)處理函數(shù)的映射。欲了解具體細(xì)節(jié)請(qǐng)查看perldoc perlipc。
超級(jí)全局行為的替代方案
程序中通常最容易出岔子的地方就是IO和異常,我們可以使用Try::Tiny來進(jìn)行異常處理;使用本地化和立即復(fù)制$!的值來避免Perl在系統(tǒng)調(diào)用時(shí)出現(xiàn)奇怪的行為;使用IO::File和詞法文件句柄來避免不想要的全局IO行為。