[轉帖]perl的特殊變量

原作者:Dave Cross
翻譯者:sql (http://www.s8s8.net)
正文
讓你的perl代碼看起來更像perl代碼,而不是像C或者BASIC代碼,最好的辦法就是去了解perl的内置變量。perl可以通過這些内置變量可以控制程序運行時的諸多方面。
本文中,我們一起領略一下衆多内置變量在文件的輸入輸出控制上的出色表現。

行計數
我決定寫這篇文章的一個原因就是,當我發現很多人都不知道“$.”内置變量的存在,這的确讓我很吃驚。
我依然能看到很多人是這樣寫代碼的:
代碼


my $line_no = 0;

while (<FILE>) {
++$line_no;
unless (/some regex/) {
 warn "Error in line $line_no\n";
 next;
}

# process the record in some way
}


由于某些原因,很多人似乎完全忽略了“$.”的存在。而這個變量的作用就是跟蹤當前記錄号。因此上面的代碼也可以這樣來寫:
代碼

while (<FILE>)

{
unless (/some regex/) {
 warn "Error in line $.\n";
 next;
}

# process the record in some way
}



譯者注:通俗的說,這個内置變量就跟數據庫中的記錄指針非常相似,它的值就是你當前所讀文件中的當前行号。

雖然使用此内置變量并不能讓你少打多少字,但重要的是我們可以省去一些不必要的變量聲明。
另一種利用此内置變量的方法就是與連續操作符(..)一起使用。當用在列表上下文中時,(..)是列表構建操作符。它将從給出的開始和結束元素之間創建所有的元素。例如:
代碼

my @numbers = (1 .. 1000);

@numbers将包含從1到1000之間所有的整數。


但是當你在一個表達式上下文中使用此操作符時(比如,作為一個聲明的條件),它的作用就完全不一樣了。第一個操作數(“..“左側的表達式)将被求值,如果得出的值為假,此次操作将什麼也不做并返回假值。如果得出的值為真,操作返回真值并繼續依次返回下面的值直到第二個操作數(“..”操作符右面的表達式)返回真值。
我們舉個例子解釋一下。假設你有一個文件,你隻想處理這個文件的某幾個部分。這幾個部分以"!! START !!"為開始,"!! END !!"為結束。
使用連續操作符你可以這樣寫這段代碼:
代碼

while (<FILE>) {
if (/!! START !!/ .. /!! END !!/) {
 # process line
}
}



每一次循環,連續操作符就會檢查當前行。如果當前行與“/!! START !!/”不匹配,則操作符返回假值并繼續循環。當循環到第一個與/!! START !!/”相匹配的行時,連續操作符就會返回真值并執行if語句塊中的代碼。在while語句後面的循環中,連續操作符将再次檢查“/!! END !!/”的匹配行,但是它直到找到匹配行後才會返回真值。這也就是說在"!! START !!" 和"!! END !!" 标記之間的所有行都将被處理。當找到/!! END !!/的匹配行後,連續操作符返回假并再次開始匹配第一個規則表達式。

這些與“$.”有什麼關系呢?如果連續操作符的操作數有一個是常量的話,他們将被轉化為整型數并于“$.”匹配。
因此輸出一個文件的前10行内容我們可以這樣寫代碼:
代碼

while (<FILE>) {
print if 1 .. 10;
}


關于“$.”最後要說明的一點是,一個程序中隻有一個“$.”變量。如果你在從多個文件句柄中讀數據,那麼“$.”變量保存了最近讀過的文件句柄中的當前記錄号。如果你想要更複雜的解決此問題的方法那麼你可以使用類似IO::FILE對象。這些對象都有一個input_line_number方法。

記錄分隔符

“$/” 和 “$\”分别是輸入輸出記錄分隔符。當你在讀或者寫數據時,他們主要控制用什麼來定義一個“記錄”。

讓我更詳細地給大家解釋一下吧。當你第一次學習perl,第一次知道文件輸入操作符的時候,也許你會被告知“<FILE>”就是從一個文件讀
入一行數據,而讀入的每一行都包括一個新行字符(“\n”)。其實你所知道的這些并不完全是真的,那隻是一個很特殊的情況。實際上文件輸入操作符(“<>”)讀數據後會包含一個在“$/”中指定的文件輸入分隔符。讓我們來看一個例子:

假設你有一個文本文件,内容是些有趣的引文或者一些歌詞或者一些别的什麼東西。比如類似下面的内容:
代碼

This is the definition of my life
%%
We are far too young and clever
%%
Stab a sorry heart
With your favorite finger


在這裡有三段被一行“%%”分隔的引文。那麼我們該如何從這個文件中一次讀取一段引文呢。(譯者注:這一段引文可是一行也可以是幾行,比如例子中的第一段和第二段引文都是一行,而第三段引文是2行)
其中一個解決方法就是,一次從文件中讀取一行,然後檢查讀入的行是否是“%%”。因此我們需要聲明一個變量用來保存每次讀入的數據,當遇到“%%”後重新組合先前讀入的數據為一段完整的引文。哦,你還需要記得處理最後一段引文因為它最後沒有“%%”。
這樣的方法太過于複雜,一個簡單的方法就是更改“$/”變量的内容。該變量的默認值是一個新行字符(“\n”),這也就是為什麼“<>”
操作符在讀取文件内容時是一次讀一行。但是我們可以修改這一變量内容為我們喜歡的任意值。比如:

代碼

$/ = "%%\n";

while (<QUOTE>) {
chomp;
print;
}


現在我們每次調用“<>”,perl會從文件句柄中一次讀取數據直到發現 “%%\n”為止。(不是一次讀一行了)。
因此,當你用chomp函數來去掉讀取數據的行分隔符時,就會删除“$/”變量中指定的分隔符了。在上例中經過chomp函數處理後的數據都會将
%%\n”删除。

更改perl的特殊變量

在我們繼續之前,我需要提醒你的是,當你修改了這些特殊變量的值後,你會得到一個警告。問題就是這些變量中的多數是被強制在主包中
的。也就是說當你更改這些變量的值時,程序中用到這個值的地方(包括你包含的那些模塊)都會給出警告。
比如如果你在寫一個模塊,且你在模塊中更改了“$/”變量的值,那麼當别人把你的模塊應用到自己的程序中時就必須相應的修改其他模塊
以适應程序的執行。所以修改特殊變量的值潛在地增加了查找bugs的難度。

因此我們應該盡可能的避免它。第一個避免的方法是在你用完了修改後的特殊變量的值後應該将該特殊變量重值回原始值。比如:
代碼

$/ = "%%\n";

while (<QUOTE>) {
chomp;
print;
}

$/ = "\n";


而這個方法引發的另一個問題就是你不能确定在你重置特殊變量的值之前它的值就是系統默認值。
(譯者注:比如如果你在“$/ = "%%\n";”之前就修改過“$/”變量的值(不是默認值“\n”),那麼你最後重置回默認值肯定會引發錯誤的)
因此我們的代碼應該像如下才對,如下:
代碼

$old_input_rec_sep = $/;
$/ = "%%\n";

while (<QUOTE>) {
chomp;
print;
}

$/ = $old_input_rec_sep;



上面的代碼就避免了我們上述所說的bug,但是我們有另一個看起來更簡練的方法。這個方法就是使用local來定義“$/”變量。如下:
代碼

{
local $/ = "%%\n";

while (<QUOTE>) {
 chomp;
 print;
}
}


我們将代碼以一對大括号括起來。一般的,代碼塊往往與循環,條件或者是子程序有關聯,但是在perl中是可以單獨用大括号來說明一個代碼塊的。
而在這個代碼塊内用local定義的變量隻在當前代碼塊中起作用。
綜上所述,不更改perl的内置變量是一個很好的習慣,除非它被本地化在一個代碼塊中。


“$/”的其他值

下面給出一些你可以賦予“$/”變量的特殊值,這些值可以開啟一些有趣的行為。第一個就是設置該變量為未定義。這将開啟slurp模式,
開啟該模式後我們可以一次性從一個文件中讀取全部的文件内容。如下:
代碼

my $file = do { local $/; <FILE> };


一個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,那麼我們可以這樣寫:

代碼

sub println {
local $\ = "\n";
print @_;
}

這樣,在你每次用print輸出數據時都會在數據後面增加一個"\n"(即換行符)。

其它 Print 變量

接下來的兩個需要讨論的變量是非常容易混淆,盡管它們做的是完全不同的兩件事。為了舉例說明,看下面代碼:
代碼

my @arr = (1, 2, 3);

print @arr;
print "@arr";


現在,如果不仔細地看你是否知道上面兩個print調用的區别嗎?
答案是,第一個print調用會緊挨着輸出數組的三個元素,其間沒有任何分割符(輸出為:123)。然而第二個print語句輸出的元素确實以空格為分隔的(輸出為:1 2 3)。為什麼會有此區别呢?

理解這個問題的關鍵就是,在每種情況下實際傳給print調用的是什麼。在第一種情況下,傳遞給print的是一個數組。perl将展開傳遞過來的數組為一個列表,列表中的三個元素被視為單獨的參數。而第二種情況下,在傳遞給print之前,數組被雙引号所包含。
确切地說第二種情況也可以理解成如下的過程:
代碼

my $string = "@arr";
print $string;

因此,在第二種情況看來,傳遞給print函數的隻是一個參數。事實上的結果就是對一個數組進行了雙引号的包含,并不影響print函數是如何對待該字符串的。



因此擺在我們面前的就是兩種情況。當print接收一組參數的時候,它将緊湊地将這些參數輸出而在輸出的參數之間沒有空格。當一個數組被
雙引号包含起來傳遞給print之前,數組的每個元素将以空格為分隔符展開為一個字符串。這兩種情況是完全不相幹的。不過從我們上面舉的例子我們很容易看出人們是如何混淆這兩種情況的。
當然,如果我們願意,perl允許我們改變這種行為。“ $,”變量保存了分隔傳遞給print函數的參數所用到的字符串。正如上面介紹的,默認分割print參數的字符是空字符,當然這都是可以更改的:

代碼

my @arr = (1, 2, 3);
{
local $, = ',';

print @arr;
}

這段代碼将輸出1,2,3


相應地,當一個數組被雙引号包含傳遞給print函數時,展開這個數組後用來分割元素的字符則保存在“$"”變量中。代碼如下:

代碼

my @arr = (1, 2, 3);
{
local $" = '+';

print "@arr";
}

這段代碼将輸出 1+2+3



當然,在一個print語句的使用中“$"”變量并不是必須的。你可以用在任何被雙引号包含的數組的地方。而且它也不僅僅是對數組才有效。

也可以用在哈希表上。
代碼

my %hash = (one => 1, two => 2, three => 3);

{
local $" = ' < ';

print "@hash{qw(one two three)}";
}

這将輸出: 1 < 2 < 3


總結

在這篇文章中,我們大體了解了修改perl的内置變量的值可以給我們帶來什麼樣的效果。如果你還想了解地更深入一下,去閱讀官方手冊吧。