第十章 超越Perl語法

不同的人對于簡單有著不同的理解母剥。高效的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行為坤塞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冯勉,一起剝皮案震驚了整個濱河市澈蚌,隨后出現(xiàn)的幾起案子摹芙,更是在濱河造成了極大的恐慌,老刑警劉巖宛瞄,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浮禾,死亡現(xiàn)場離奇詭異,居然都是意外死亡份汗,警方通過查閱死者的電腦和手機盈电,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杯活,“玉大人匆帚,你說我怎么就攤上這事∨跃” “怎么了吸重?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長歪今。 經(jīng)常有香客問我嚎幸,道長,這世上最難降的妖魔是什么寄猩? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任嫉晶,我火速辦了婚禮,結(jié)果婚禮上田篇,老公的妹妹穿的比我還像新娘替废。我一直安慰自己,他們只是感情好泊柬,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布椎镣。 她就那樣靜靜地躺著,像睡著了一般彬呻。 火紅的嫁衣襯著肌膚如雪衣陶。 梳的紋絲不亂的頭發(fā)上柄瑰,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機與錄音剪况,去河邊找鬼教沾。 笑死,一個胖子當(dāng)著我的面吹牛译断,可吹牛的內(nèi)容都是我干的授翻。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼孙咪,長吁一口氣:“原來是場噩夢啊……” “哼堪唐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起翎蹈,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤淮菠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后荤堪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體合陵,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年澄阳,在試婚紗的時候發(fā)現(xiàn)自己被綠了拥知。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡碎赢,死狀恐怖低剔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肮塞,我是刑警寧澤襟齿,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站峦嗤,受9級特大地震影響蕊唐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜烁设,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一替梨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧装黑,春花似錦副瀑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至疚颊,卻和暖如春狈孔,著一層夾襖步出監(jiān)牢的瞬間信认,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工均抽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嫁赏,地道東北人。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓油挥,卻偏偏與公主長得像潦蝇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子深寥,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349

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