原作者:Dave Cross
翻譯者:sql
正文
讓你的perl代碼看起來更像perl代碼,而不是像C或者BASIC代碼,最好的辦法就是去了解perl的內置變量。perl可以通過這些內置變量可以控制程序運行時的諸多方面。
本文中,我們一起領略一下衆多內置變量在文件的輸入輸出控制上的出色表現。
行計數
我決定寫這篇文章的一個原因就是,當我發現很多人都不知道“$.”內置變量的存在,這的確讓我很喫驚。
我依然能看到很多人是這樣寫代碼的:
while (<FILE>) {
++$line_no;
unless (/some regex/) {
warn "Error in line $line_no ";
next;
}
# process the record in some way
}
由於某些原因,很多人似乎完全忽略了“$.”的存在。而這個變量的作用就是跟蹤當前記錄號。因此上面的代碼也可以這樣來寫:
unless (/some regex/) {
warn "Error in line $. ";
next;
}
# process the record in some way
}
譯者注:通俗的說,這個內置變量就跟數據庫中的記錄指針非常相似,它的值就是你當前所讀文件中的當前行號。
雖然使用此內置變量並不能讓你少打多少字,但重要的是我們可以省去一些不必要的變量聲明。
另一種利用此內置變量的方法就是與連續操作符(..)一起使用。當用在列表上下文中時,(..)是列表構建操作符。它將從給出的開始和結束元素之間創建所有的元素。例如:
@numbers將包含從1到1000之間所有的整數。
但是當你在一個表達式上下文中使用此操作符時(比如,作爲一個聲明的條件),它的作用就完全不一樣了。第一個操作數(“..”左側的表達式)將被求值,如果得出的值爲假,此次操作將什麼也不做並返回假值。如果得出的值爲真,操作返回真值並繼續依次返回下面的值直到第二個操作數(“..”操作符右面的表達式)返回真值。
我們舉個例子解釋一下。假設你有一個文件,你只想處理這個文件的某幾個部分。這幾個部分以"!! START !!"爲開始,"!! END !!"爲結束。
使用連續操作符你可以這樣寫這段代碼:
if (/!! START !!/ .. /!! END !!/) {
# process line
}
}
每一次循環,連續操作符就會檢查當前行。如果當前行與"/!! START !!/"不匹配,則操作符返回假值並繼續循環。當循環到第一個與"/!! START !!/"相匹配的行時,連續操作符就會返回真值並執行if語句塊中的代碼。在while語句後面的循環中,連續操作符將再次檢查"/!! END !!/"的匹配行,但是它直到找到匹配行後纔會返回真值。這也就是說在"!! START !!" 和"!! END !!" 標記之間的所有行都將被處理。當找到"/!! END !!/"的匹配行後,連續操作符返回假並再次開始匹配第一個規則表達式。
這些與“$.”有什麼關係呢?如果連續操作符的操作數有一個是常量的話,他們將被轉化爲整型數並於“$.”匹配。
因此輸出一個文件的前10行內容我們可以這樣寫代碼:
print if 1 .. 10;
}
關於“$.”最後要說明的一點是,一個程序中只有一個“$.”變量。如果你在從多個文件句柄中讀數據,那麼“$.”變量保存了最近讀過的文件句柄中的當前記錄號。如果你想要更復雜的解決此問題的方法那麼你可以使用類似IO::FILE對象。這些對象都有一個input_line_number方法。
記錄分隔符
“$/” 和 “$/”分別是輸入輸出記錄分隔符。當你在讀或者寫數據時,他們主要控制用什麼來定義一個“記錄”。
讓我更詳細地給大家解釋一下吧。當你第一次學習perl,第一次知道文件輸入操作符的時候,也許你會被告知“<FILE>”就是從一個文件讀入一行數據,而讀入的每一行都包括一個新行字符(“/n”)。其實你所知道的這些並不完全是真的,那只是一個很特殊的情況。實際上文件輸入操作符(“<>”)讀數據後會包含一個在“$/”中指定的文件輸入分隔符。讓我們來看一個例子:
假設你有一個文本文件,內容是些有趣的引文或者一些歌詞或者一些別的什麼東西。比如類似下面的內容:
%%
We are far too young and clever
%%
Stab a sorry heart
With your favorite finger
在這裏有三段被一行“%%”分隔的引文。那麼我們該如何從這個文件中一次讀取一段引文呢。(譯者注:這一段引文可是一行也可以是幾行,比如例子中的第一段和第二段引文都是一行,而第三段引文是2行)
其中一個解決方法就是,一次從文件中讀取一行,然後檢查讀入的行是否是“%%”。因此我們需要聲明一個變量用來保存每次讀入的數據,當遇到“%%”後重新組合先前讀入的數據爲一段完整的引文。哦,你還需要記得處理最後一段引文因爲它最後沒有“%%”。
這樣的方法太過於複雜,一個簡單的方法就是更改“$/”變量的內容。該變量的默認值是一個新行字符(“/n”),這也就是爲什麼“<>”操作符在讀取文件內容時是一次讀一行。但是我們可以修改這一變量內容爲我們喜歡的任意值。比如:
while (<QUOTE>) {
chomp;
print;
}
現在我們每次調用“<>”,perl會從文件句柄中一次讀取數據直到發現 “%%/n”爲止。(不是一次讀一行了)。
因此,當你用chomp函數來去掉讀取數據的行分隔符時,就會刪除“$/”變量中指定的分隔符了。在上例中經過chomp函數處理後的數據都會將%%/n”刪除。
更改perl的特殊變量
在我們繼續之前,我需要提醒你的是,當你修改了這些特殊變量的值後,你會得到一個警告。問題就是這些變量中的多數是被強制在主包中的。也就是說當你更改這些變量的值時,程序中用到這個值的地方(包括你包含的那些模塊)都會給出警告。比如如果你在寫一個模塊,且你在模塊中更改了“$/”變量的值,那麼當別人把你的模塊應用到自己的程序中時就必須相應的修改其他模塊以適應程序的執行。所以修改特殊變量的值潛在地增加了查找bugs的難度。
因此我們應該儘可能的避免它。第一個避免的方法是在你用完了修改後的特殊變量的值後應該將該特殊變量重值回原始值。比如:
while (<QUOTE>) {
chomp;
print;
}
$/ = " ";
而這個方法引發的另一個問題就是你不能確定在你重置特殊變量的值之前它的值就是系統默認值。
(譯者注:比如如果你在“$/ = "%%/n";”之前就修改過“$/”變量的值(不是默認值“/n”),那麼你最後重置回默認值肯定會引發錯誤的)
因此我們的代碼應該像如下才對,如下:
$/ = "%% ";
while (<QUOTE>) {
chomp;
print;
}
$/ = $old_input_rec_sep;
上面的代碼就避免了我們上述所說的bug,但是我們有另一個看起來更簡練的方法。這個方法就是使用local來定義“$/”變量。如下:
local $/ = "%% ";
while (<QUOTE>) {
chomp;
print;
}
}
我們將代碼以一對大括號括起來。一般的,代碼塊往往與循環,條件或者是子程序有關聯,但是在perl中是可以單獨用大括號來說明一個代碼塊的。而在這個代碼塊內用local定義的變量只在當前代碼塊中起作用。
綜上所述,不更改perl的內置變量是一個很好的習慣,除非它被本地化在一個代碼塊中。
“$/”的其他值
下面給出一些你可以賦予“$/”變量的特殊值,這些值可以開啓一些有趣的行爲。第一個就是設置該變量爲未定義。這將開啓slurp模式,開啓該模式後我們可以一次性從一個文件中讀取全部的文件內容。如下:
一個do語句塊的返回值是語句塊中最後一個表達式的值,如上面的do語句塊的返回值就是“<>”操作符的返回值。而且由於“$/”變量被設置爲 undef(未定義),所以返回的就是整個文件的內容。需要注意的是,我們不需要明確地指定“$/”變量爲undef,因爲所有的perl變量在定義的時候就被自動初始化爲undef。
設置“$/”變量爲undef和空值是有很大區別的:設置成空值意味着開啓paragraph模式(即段落模式),在這種模式下,每個記錄就是一段以一個或更多空行爲結束的文本段落。也許你會認爲這種效果和把“$/”變量被設置爲“/n/n”的效果是一樣的,但是他們還是有微妙的區別的。如果一定進行比較,那麼應該把“$/”變量設置成爲“/n/n+”才能和paragraph模式相同。(注意,這裏只是比方說。實際上是不能將“$/”變量設置爲規則表達式的)“$/”變量的最後一個特殊值就是可以將其設置爲一個整數標量變量的引用或者是一個整數常量的引用。
在這種情況下,從文件句柄中每次讀出的數據最多是“$/”變量指定的大小。(在這裏我說“最多”是因爲在文件的最後有可能剩餘的數據大小小於“$/”變量指定的大小)。因此,如果你想每次讀出2kb的數據那麼你可以這樣做:
local $/ = 2048;
while (<FILE>) {
# $_ contains the next 2048 bytes from FILE
}
}
“$/” 和 “$.”
注意到當改變“$/” 變量的值時候也相應的改變了perl對於記錄的定義因此也改變了“$.”變量的行爲。“$.”變量實際上保存的不再是當前“行”號了,而是當前的記錄號。因此在前述的那個引文的例子中,“$.”變量將按照你所要讀出數據的文件中的每一段引文遞增。
關於“$/”
在前面的開始我提到了“$/” 和“$/”變量作爲輸入和輸出的記錄分隔符。但是我們一直沒有介紹“$/”變量。
說實話,“$/”並不像“$/”那麼有用。它包含了每次調用print輸出時在最後要增加的字符串。它的默認值是空字符串,因此當你用print進行輸出時,並沒有任何東西跟在輸出的數據後面。當然如果你非常希望能有個類似pascal的輸出函數println,那麼我們可以這樣寫:
local $ = " ";
print @_;
}
這樣,在你每次用print輸出數據時都會在數據後面增加一個"/n"(即換行符)。
其它 Print 變量
接下來的兩個需要討論的變量是非常容易混淆,儘管它們做的是完全不同的兩件事。爲了舉例說明,看下面代碼:
print @arr;
print "@arr";
現在,如果不仔細地看你是否知道上面兩個print調用的區別嗎?
答案是,第一個print調用會緊挨着輸出數組的三個元素,其間沒有任何分割符(輸出爲:123)。然而第二個print語句輸出的元素確實以空格爲分隔的(輸出爲:1 2 3)。爲什麼會有此區別呢?
理解這個問題的關鍵就是,在每種情況下實際傳給print調用的是什麼。在第一種情況下,傳遞給print的是一個數組。perl將展開傳遞過來的數組爲一個列表,列表中的三個元素被視爲單獨的參數。而第二種情況下,在傳遞給print之前,數組被雙引號所包含。
確切地說第二種情況也可以理解成如下的過程:
print $string;
因此,在第二種情況看來,傳遞給print函數的只是一個參數。事實上的結果就是對一個數組進行了雙引號的包含,並不影響print函數是如何對待該字符串的。
因此擺在我們面前的就是兩種情況。當print接收一組參數的時候,它將緊湊地將這些參數輸出而在輸出的參數之間沒有空格。當一個數組被雙引號包含起來傳遞給print之前,數組的每個元素將以空格爲分隔符展開爲一個字符串。這兩種情況是完全不相干的。不過從我們上面舉的例子我們很容易看出人們是如何混淆這兩種情況的。
當然,如果我們願意,perl允許我們改變這種行爲。“ $,”變量保存了分隔傳遞給print函數的參數所用到的字符串。正如上面介紹的,默認分割print參數的字符是空字符,當然這都是可以更改的:
{
local $, = ',';
print @arr;
}
這段代碼將輸出1,2,3
相應地,當一個數組被雙引號包含傳遞給print函數時,展開這個數組後用來分割元素的字符則保存在“$"”變量中。代碼如下:
{
local $" = '+';
print "@arr";
}
這段代碼將輸出 1+2+3
當然,在一個print語句的使用中“$"”變量並不是必須的。你可以用在任何被雙引號包含的數組的地方。而且它也不僅僅是對數組纔有效。
也可以用在哈希表上。
{
local $" = ' < ';
print "@hash{qw(one two three)}";
}
這將輸出: 1 < 2 < 3
總結
在這篇文章中,我們大體瞭解了修改perl的內置變量的值可以給我們帶來什麼樣的效果。如果你還想了解地更深入一下,去閱讀官方手冊吧。