2.7 環境
到現在爲止,我們已經看到了一些會產生標量值的項。在我們進一步討論項之前,我們要先討論帶環境(context)的術語。
2.7.1 標量和列表環境
你在 Perl 腳本里激活的每個操作(注:這裏我們用“操作”統稱操作符或項。當你開始討論那些分析起來類似項而看起來象操作符的函數時,這兩個概念間的界限就模糊了。)都是在特定的環境裏進行的,並且該操作的運轉可能依賴於那個環境的要求。存在兩種主要的環境:標量和列表。比如,給一個標量變量,一個數組或散列的標量元素賦值,在右手邊就會以標量環境計算:
$x = funkshun(); # scalar context $x[1] = funkshun(); # scalar context $x{"ray"} = funkshun(); # scalar context
但是,如果給一個數組或者散列,或者它們的片段賦值,在右手邊就會以列表環境進行計算,即便是該片段只選出了的一個元素:
@x = funkshun(); # list context @x[1] = funkshun(); # list context @x{"ray"} = funkshun(); # list context %x = funkshun(); # list context
即使你用 my 或 our 修改項的定義,這些規則也不會改變:
my $x = funkshun(); # scalar context my @x = funkshun(); # list context my %x = funkshun(); # list context my ($x) = funkshun(); # list context
在你正確理解標量和列表環境的區別之前你都會覺得很痛苦,因爲有些操作符(比如我們上面虛構的 funkshun()函數)知道它們處於什麼環境中,所以就能在列表環境中返回列表,在標量環境中返回標量。(如果這裏提到的東西對於某操作成立,那麼在那個操作的文檔裏面應該提到這一點。)用計算機行話來說,這些操作重載了它們的返回類型。不過這是一種非常簡單的重載,只是以單數和複數之間的區別爲基礎,別的就沒有了。
如果某些操作符對環境敏感,那麼很顯然必須有什麼東西給它們提供環境。我們已經顯示了賦值可以給它的右操作數提供環境,不過這個例子不難理解,因爲所有操作符都給它的每個操作數提供環境。你真正感興趣的應該是一個操作符會給它的操作數提供哪個環境。這樣,你可以很容易地找出哪個提供了列表環境,因爲在它們的語法描述部分都有 LIST。其他的都提供標量環境。通常,這是很直觀的。(注:不過請注意,列表環境可以通過子過程調用傳播,因此,觀察某個語句會在標量還是列表環境裏面計算並不總是很直觀。程序可以在子過程裏面用 wantarray 函數找出它的環境。)如果必要,你可以用僞函數 scalar 給一個 LIST 中間的參數強制一個標量環境。Perl 沒有提供強制列表環境成標量環境的方法,因爲在任何一個你需要列表環境的地方,都會已經通過一些控制函數提供了 LIST。
標量環境可以進一步分類成字串環境,數字環境和無所謂環境。和我們剛剛說的標量與列表環境的區別不同,操作從來不關心它們處於那種標量環境。它們只是想返回的標量值,然後讓 Perl 在字串環境中把數字轉換成字串,以及在數字環境中把字串轉換成數字。有些標量環境不關心返回的是字串還是數字還是引用,因此就不會發生轉換。這個現象會發生在你給另外一個變量賦值的時候。新的變量只能接受和舊值一樣的子類型。
2.7.2 布爾環境
另外一個特殊的無所謂標量環境是布爾環境。布爾環境就是那些要對一個表達式進行計算,看看它是真還是假的地方。當我們在本書中說到“真”或“假”的時候,我們指的是 Perl 用的技術定義:如果一個標量不是空字串 "" 或者數字 0 (或者它的等效字串,"0" )那麼就是真。一個引用總是真,因爲它代表一個地址,而地址從不可能是 0。一個未定義值(常稱做 undef)總是假,因爲它看起來象 "" 或者 0——取決於你把它當作字串還是數字。(列表值沒有布爾值,因爲列表值從來不會產生標量環境!)
因爲布爾環境是一個無所謂環境,它從不會導致任何標量轉換的發生,當然,標量環境本身施加在任何參與的操作數上。並且對於許多相關的操作數,它們在標量環境裏產生的標量代表一個合理的布爾值。也就是說,許多在列表環境裏會產生一個列表的操作符可以在布爾環境裏用於真/假測試。比如,在一個由 unlink 操作符提供的列表環境裏,一個數組名產生一列值:
unlink @files; # 刪除所有文件,忽略錯誤。
但是,如果你在一個條件裏(也就是說,在布爾環境裏)使用數組,數組就會知道它正處於一個標量環境並且返回數組裏的元素個數,只要數組裏面還有元素,通常就是真。因此,如果你想獲取每個沒有正確刪除的文件的警告,你可能就會這樣寫一個循環:
while (@files) { my $file = shift @files; unlink $file or warn "Can't delete $file: $!/n"; }
這裏的 @files 是在由 while 語句提供的布爾環境裏計算的,因此 Perl 就計算數組本身,看看它是“真數組”還是 “假數組”。只要裏面還有文件名,它就是真數組,不過一旦最後一個文件被移出,它就變成假數組。請注意我們早先說過的依然有效。雖然數組包含(和可以產生)一列數值,我們在標量環境裏並不計算列表值。我們只是告訴數組這裏是標量,然後問它覺得自己是什麼。
不要試圖在這裏用 defined @files。那樣沒用,因爲 defined 函數是詢問一個標量是否爲 undef,而一個數組不是標量。簡單的布爾測試就夠用了。
2.7.3 空(void)環境
另外一個特殊的標量環境是空環境(void context)。這個環境不僅不在乎返回值的類型,它甚至連返回值都不想要。從函數如何運行的角度看,這和普通標量環境沒有區別。但是如果你打開了警告,如果你在一個不需要值的地方,比如說在一個不返回值的語句裏,使用了一個沒有副作用的表達式,Perl 的編譯器就會警告你,比如,如果你用一個字串當作語句:
"Camel Lot";
你會收到這樣的警告:
Useless use of a constant in void context in myprog line 123;
2.7.4 代換環境
我們早先說過雙引號文本做反斜槓代換和變量代換,不過代換文本(通常稱做“雙引號文本”)不僅僅適用於雙引號字串。其他的一些雙引號類構造是:通用的反勾號操作符 qx,模式匹配操作符 m//,替換操作符 s///,和引起正則表達式操作符,qr//。替換操作符在處理模式匹配之前在它的左邊做代換動作,然後每次匹配左邊時做右邊的代換工作。
代換環境只發生在引起裏,或者象引起那樣的地方,也許我們把它當作與標量及列表環境一樣的概念來講並不恰當。(不過也許是對的。)
2.8 列表值和數組
既然我們談到環境,那我們可以談談列表文本和它們在環境裏的性質。你已經看到過一些列表文本。列表文本是用逗號分隔的獨立數值表示的(當有優先級要求的時候用圓括弧包圍)。因爲使用圓括弧幾乎從不會造成損失,所以列表值的語法圖通常象下面這樣說明:
(LIST)
我們早先說過在語法描述裏的 LIST 表示某個東西給它的參數提供了列表環境,不過只有列表文本自身會部分違反這條規則,就是說只有在列表和操作符全部處於列表環境裏纔會提供真正的列表環境。列表文本在列表環境裏的內容只是順序聲明的參數值。作爲一種表達式裏的特殊的項,一個列表文本只是把一系列臨時值壓到 Perl 的堆棧裏,當操作符需要的時候再從堆棧裏彈出來。
不過,在標量環境裏,列表文本並不真正表現得象一個列表(LIST),因爲它並沒有給它的值提供列表環境。相反,它只是在標量環境裏計算它的每個參數,並且返回最後一個元素的值。這是因爲它實際上就是僞裝的 C 逗號操作符,逗號操作符是一個兩目操作符,它會丟棄左邊的值並且返回右邊的值。用我們前面討論過的術語來說,逗號操作符的左邊實際上提供了一個空環境。因爲逗號操作符是左關聯的,如果你有一系列逗號分隔的數值,那你總是得到最後一個數值,因爲最後一個逗號會丟棄任何前面逗號生成的東西。因此,要比較這兩種環境,列表賦值:
@stuff = ( "one", "two", "three");
給數組@stuff,賦予了整個列表的值。但是標量賦值:
$stuff = ( "one", "two", "three");
只是把值 "three" 賦予了變量 $stuff。和我們早先提到的 @files 數組一樣,逗號操作符知道它是處於標量還是列表環境,並且根據相應環境選擇其動作。
值得說明的一點是列表值和數組是不一樣的。一個真正的數組變量還知道它的環境,處於列表環境時,它會象一個列表文本那樣返回其內部列表。但是當處於標量環境時,它只返回數組長度。下面的東西給 $stuff 賦值 3:
@stuff = ("one", "two", "three"); $stuff = @stuff;
如果你希望它獲取值 "three",那你可能是認爲 Perl 使用逗號操作符的規則,把 @stuff 放在堆棧裏的臨時值都丟掉,只留下一個交給 $stuff。不過實際上不是這樣。@stuff 數組從來不把它的所有值都放在堆棧裏。實際上,它從來不在堆棧上放任何值。它只是在堆棧裏放一個數值——數組長度,因爲它知道自己處於標量環境。沒有任何項或者操作符會在標量環境裏把列表放入堆棧。相反,它會在堆棧裏放一個標量,一個它喜歡的值,而這個值不太可能是列表的最後一個值(就是那個在列表環境裏返回的值),因爲最後一個值看起來不象時在標量環境裏最有用的值。你的明白?(如果還不明白,你最好重新閱讀本自然段,因爲它很重要。)
現在回到真正的 LIST(列表環境)。直到現在我們都假設列表文本只是一個文本列表。不過,正如字串文本可能代換其他子字串一樣,一個列表文本也可以代換其他子列表。任何返回值的表達式都可以在一個列表中使用。所使用的值可以是標量值或列表值,但它們都成爲新列表值的一部分,因爲 LIST 會做子列表的自動代換。也就是說,在計算一個 LIST 時,列表中的每個元素都在一個列表環境中計算,並且生成的列表值都被代換進 LIST,就好象每個獨立的元素都是 LIST 的成員一樣。因此數組在一個 LIST 中失去它們的標識(注:有些人會覺得這是個問題,但實際上不是。如果你不想失去數組的標識,那麼你總是可以用一個引用代換數組。參閱第八章。)。列表:
(@stuff,@nonsense,funkshun())
包含元素 @stuff,跟着是元素 @nonsense,最後是在列表環境裏調用子過程 &funkshun 時它的返回值。請注意她們的任意一個或者全部都可能被代換爲一個空列表,這時就象在該點沒有代換過數組或者函數調用一樣。空列表本身由文本 () 代表。對於空數組,它會被代換爲一個空列表因而可以忽略,把空列表代換爲另一個列表沒有什麼作用。所以,((),(),())等於()。
這條規則的一個推論就是你可以在任意列表值結尾放一個可選的逗號。這樣,以後你回過頭來在最後一個元素後面增加更多元素會簡單些:
@releases = ( "alpha", "beta", "gamma",);
或者你可以完全不用逗號:另一個聲明文本列表的方法是用我們早先提到過的 qw(引起字)語法。這樣的構造等效於在空白的地方用單引號分隔。例如:
@froots = qw( apple banana carambola coconut guava kumquat mandarin nectarine peach pear persimmon plum);
(請注意那些圓括弧的作用和引起字符一樣,不是普通的圓括弧。我們也可以很容易地使用尖括弧或者花括弧或者斜槓。但是圓括弧看起來比較漂亮。)
一個列表值也可以象一個普通數組那樣使用腳標。你必須把列表放到一個圓括弧(真的)裏面以避免混淆。我們經常用到從一個列表裏抓取一個值,但這時候實際上是抓了列表的一個片段,所以語法是:
(LIST)[LIST]
例子: # Stat 返回列表值 $modification_time = (stat($file))[9]; # 語法錯誤 $modification_time = stat($file)[9]; # 忘記括弧了。 # 找一個十六進制位 $hexdigit = ('a','b','c','d','e','f')[$digit-10]; # 一個“反轉的逗號操作符”。 return (pop(@foo),pop(@foo))[0]; # 把多個值當作一個片段 ($day, $month, $year) = (localtime)[3,4,5];
2.8.1 列表賦值
只有給列表賦值的每一個元素都合法時,才能給整個列表賦值:
($a, $b, $c) = (1, 2, 3); ($map{red}, ${map{green}, $map{blue}) = (0xff0000, 0x00ff00, 0x0000ff);
你可以給一個列表裏的 undef 賦值。這一招可以很有效地把一個函數的某些返回值拋棄:
($dev, $ino, undef, undef, $uid, $gid) = stat($file);
最後一個列表元素可以是一個數組或散列:
($a, $b, @rest) = split; my ($a, $b, %rest) = @arg_list;實際上你可以在賦值的列表裏的任何地方放一個數組或散列,只是第一個數組或散列會吸收所有剩餘的數值,而且任何在它們後面的東西都會被設置爲未定義值。這樣可能在 local 或 my 裏面比較有用,因爲這些地方你可能希望數組初始化爲空。
你甚至可以給空列表賦值:
() = funkshun();
這樣會導致在列表環境裏調用你的函數,但是把返回值丟棄。如果你在沒有賦值(語句)的情況下調用了此函數,那它就會在一個空環境裏被調用,而空環境是標量環境,因此可能令此函數的行爲完全不同。
在標量環境裏的列表賦值返回賦值表達式右邊生成的元素的個數:
$x = (($a,$b)=(7,7,7)); # 把 $x 設爲 3,不是 2 $x = ( ($a, $b) = funk()); # 把 $x 設爲 funk() 的返回數 $x = ( () = funk() ); # 同樣把$x 設爲 funk() 的返回數這樣你在一個布爾環境裏做列表賦值就很有用了,因爲大多數列表函數在結束的時候返回一個空(null)列表,空列表在賦值時生成一個 0,也就是假。下面就是你可能在一個 while 語句裏使用的情景:
while (($login, $password) = getpwent) { if (crypt($login, $password) eq $password) { print "$login has an insecure password!/n"; } }
2.7.6 數組長度
你可以通過在標量環境裏計算數組 @days 而獲取數組 @days 裏面的元素的個數,比如:
@days + 0; # 隱含地把 @days 處於標量環境 scalar(@days) # 明確強制 @days 處於標量環境
請注意此招只對數組有效。並不是一般性地對列表值都有效。正如我們早先提到的,一個逗號分隔的列表在標量環境裏返回最後一個值,就象 C 的逗號操作符一樣。但是因爲你幾乎從來都不需要知道 Perl 裏列表的長度,所以這不是個問題。
和 @days 的標量計算有緊密聯繫的是 $#days。這樣會返回數組裏最後一個元素的腳標,或者說長度減一,因爲(通常)存在第零個元素。給 $#days 賦值則修改數組長度。用這個方法縮短數組的長度會刪除插入的數值。你在一個數組擴大之前預先伸展可以獲得一定的效能提升。(你還可以通過給超出數組長度之外的元素賦值的方法來擴展一個數組。)你還可以通過給數組賦空列表 () 把它裁斷爲什麼都沒有。下面的兩個語句是等效的:
@whatever = (); $#whatever = -1;
而且下面的表達式總是真:
scalar(@whatever) == $#whatever + 1;
截斷一個數組並不回收其內存。你必須 undef(@whatever) 來把它的內存釋放回你的進程的內存池裏。你可能無法把它釋放回你的系統的內存池,因爲幾乎沒有那種操作系統支持這樣做。
2.9 散列
如前所述,散列只是一種有趣的數組類型,在散列裏你是用字串而不是數字來取出數值。散列定義鍵字和值之間的關聯,因此散列通常被那些打字不偷懶的人稱做關聯數組。
在 Perl 裏實際上是沒有叫什麼散列文本的東西的,但是如果你給一個散列賦一個普通列表的值,列表裏的每一對值將被當作一對鍵字/數值關聯:
%map = ('red', 0xff0000,'green', 0x00ff00,'blue',0x0000ff);
上面形式和下面的形式作用相同:
%map = (); # 先清除散列 $map{red} = 0xfff0000; $map{green} = 0x00ff00; $map{blue} = 0x0000ff;通常在鍵字/數值之間使用 => 操作符會有更好的可讀性。=> 操作符只是逗號的同義詞,不過卻有更好的視覺區分效果,並且還把任何空標識符引起在其左邊(就象上面的花括弧裏面的標識符),這樣,它在若干種操作中就顯得非常方便,包括初始化散列變量:
%map = ( red => 0xff0000, green => 0x00ff00, blue => 0x0000ff, );
或者初始化任何當作記錄使用的匿名散列引用:
$rec = { NAME => 'John Simth', RANK => 'Captain', SERNO => '951413', };或者用命名的參數激活複雜的函數:
$fiels = radio_group( NAME => 'animals' VALUES =>['camel','llama','ram','wolf'], DEFAULT =>'camel', LINEBREAD => 'true', LABELS =>/%animal_names, );
不過這裏我們又走的太遠了。先回到散列。
你可以在一個列表環境裏使用散列變量(%hash),這種情況下它把它的鍵字/數值對轉換成列表。但是,並不意味着以某種順序初始化的散列就應該同樣的順序恢復出來。散列在系統內部實現上是使用散列表來達到高速查找,這意味着記錄存儲的順序和內部用於計算記錄在散列表的裏的位置的散列函數有關,而與任何其它事情無關。因此,記錄恢復出來的時候看起來是隨機的順序。(當然,每一對鍵字/數值是以正確的順序取出來的。)關於如何獲得排序輸出的例子,可以參考第二十九章的 keys 函數。
當你在標量環境裏計算散列變量的數值的時候,它只有在散列包含任意鍵字/數值對時才返回真。如果散列裏存在鍵字/數值對,返回的值是一個用斜線分隔的已用空間和分配的總空間的值組成的字串。這個特點可以用於檢查 Perl 的(編譯好的)散列算法在你的數據集裏面性能是否太差。比如,你把 10,000 個東西放到一個散列裏面,但是在標量環境裏面計算 %HASH 得出“1/8”,意味着總共八個桶裏只用了一個桶。大概是一個桶裏存放了 10,000 個條目。這可是不應該發生的事情。
要算出一個散列裏面的鍵字的數量,在標量環境裏使用 keys 函數:scalar(keys(%HASH)).
你可以通過在花括弧裏面聲明用逗號分隔的,超過一個鍵字的方法仿真多維數組。列出的鍵字連接到一起,由 $;($SUBSCRIPT_SEPARATOR)(缺省值是 chr(28))的內容分隔。結果字串用做散列的真實鍵字。下面兩行效果相同:
$people{ $state, $country } = $census_results; $people{ join $; =>$state, $county} = $census_results;這個特性最初是爲了支持 a2p(awk 到 perl 轉換器)而實現的。現在,你通常會只使用第九章,數據結構,裏寫的一個真的(或者,更真實一些)的多維數組。舊風格依然有用的一個地方是與 DBM 文件捆綁在一起的散列(參閱第三十二章,標準模塊,裏的 DB_File),因爲它不支持多維鍵字。
請不要把多維散列仿真和片段混淆起來。前者表示一個標量數值,後者則是一個列表數值:
$hash{ $x, $y, $z} # 單個數值 @hash{ $x, $y, $z} # 一個三個值的片段
2.10 型團(typeglob)和文件句柄
Perl 裏面有種特殊的類型叫類型團(typeglob)用以保留整個符號表記錄。(符號表記錄 *foo 包括 $foo, @foo, %foo,&foo 和其他幾個 foo 的簡單解釋值。)類型團(typeglob)的類型前綴上一個 *,因爲它代表所有類型。
類型團(typeglob)(或由此的引用)的一個用途是是用於傳遞或者存儲文件句柄。如果你想保存一個文件句柄,你可以這麼幹:
$fh = *STDOUT;
或者作爲一個真的引用,象這樣:
$fh = /*STDOUT;
這也是創建一個本地文件句柄的方法,比如:
sub newopen { my $path = shift; local *FH; # 不是my() 或 our () open(FH,$path ) or return undef; return *FH: # 不是/*FH! } $fh = newopen('/etc/passwd');參閱 open 函數獲取另外一個生成新文件句柄的方法。
類型團如今的主要用途是把一個符號表取另一個符號表名字做別名。別名就是外號,如果你說:
*foo = *bar;
那所有叫“foo”的東西都是每個對應的叫“bar”的同意詞。你也可以通過給類型團賦予引用實現只給某一個變量取別名:
*foo = /$bar;
這樣 $foo 就是 $bar 的一個別名,而沒有把 @foo 做成 @bar 的別名,或者把 %foo 做成 %bar 的別名。所有這些都隻影響全局(包)變量;詞法不能通過符號表記錄訪問。象這樣給全局變量別名看起來可能有點愚蠢,不過事實是整個模塊的輸入/輸出機制都是建築在這個特性上的,因爲沒有人要求你正在當別名用的符號必須在你的名字空間裏。因此:
local *Here::blue = /$There::green;
臨時爲 $There::green 做了一個叫 $Here::blue 的別名,但是不要給 @There:green 做一個叫 @Here::blue 的別名,或者給 %There::green 做一個 %Here::blue 的別名。幸運的是,所有這些複雜的類型團操作都隱藏在你不必關心的地方。參閱第八章的“句柄參考”和“符號表參考”,第十章的“符號表”,和第十一章,模塊,看看更多的關於類型團的討論和重點。
2.11 輸入操作符
這裏我們要討論幾個操作符,因爲他們被當作項分析。有時候我們稱它們爲僞文本,因爲它們在很多方面象引起的字串。(象 print 這樣的輸出操作符被當作列表操作符分析並將在第二十九章討論。)
2.11.1 命令輸入(反勾號)操作符
首先,我們有命令輸入操作符,也叫反勾號操作符,因爲它看起來象這樣:
$info = `finger $user`;
一個用反勾號(技術上叫重音號)引起的字串首先進行變量替換,就象一個雙引號引起的字串一樣。得到的結果然後被系統當作一個命令行,而且那個命令的輸出成爲僞文本的值。(這是一個類似 Unix shell 的模塊。)在標量環境裏,返回一個包含所有輸出的字串。在列表環境裏,返回一列值,每行輸出一個值。(你可以通過設置 $/ 來使用不同的行結束符。)
每次計算僞文本的時候,該命令都得以執行。該命令的數字狀態值保存在 $?(參閱第二十八章獲取 $? 的解釋,也被稱爲 $CHILD_ERROR )。和這條命令的 csh 版本不同的是,對返回數據不做任何轉換——換行符仍然是換行符。和所有 shell 不同的是,Perl 裏的單引號不會隱藏命令行上的變量,使之避免代換。要給 shell 傳遞一個 $,你必須用反斜槓把它隱藏起來。我們上面的 finger 例子裏的 $user 被 Perl 代換,而不是被 shell。(因爲該命令 shell 處理,參閱第二十三章,安全,看看與安全有關的內容。)
反勾號的一般形式是 qx//(意思是“引起的執行”),但這個操作符的作用完全和普通的反勾號一樣。你只要選擇你的引起字符就行了。有一點和引起的僞函數類似:如果你碰巧選擇了單引號做你的分隔符,那命令行就不會進行雙引號代換;
$perl_info = qx(ps $$); # 這裏 $$ 是 Perl 的處理對象 $perl_info = qx'ps $$'; # 這裏 $$ 是 shell 的處理對象
2.11.2 行輸入(尖角)操作符
最頻繁使用的是行輸入操作符,也叫尖角操作符或者 readline 函數(因爲那是我們內部的叫法)。計算一個放在尖括弧裏面的文件句柄(比如 STDIN)將導致從相關的文件句柄讀取下一行。(包括新行,所以根據 Perl 的真值標準,一個新輸入的行總是真,直到文件結束,這時返回一個未定義值,而未定義值習慣是爲假。)通常,你會把輸入值賦予一個變量,但是有一種情況會發生自動賦值的現象。當且僅當行輸入操作符是一個 while 循環的唯一一個條件的時候,其值自動賦予特殊變量$_。然後就對這個賦值進行測試,看看它是否定義了。(這些東西看起來可能有點奇怪,但是你會非常頻繁地使用到這個構造,所以值得花些時間學習。)因此,下面行是一樣的:
while (defined($_ = <STDIN>)) {print $_; } # 最長的方法 while ($_ = <STDIN) { pirnt; } # 明確使用 $_ while (<STDIN>) { PRINT ;} # 短形式 for (;<STDIN>;) { print;} # 不喜歡用while 循環 print $_ while defined( $_ = <STDIN>); # 長的語句修改 print while $_ = <STDIN>; # 明確$_ print while <STDIN>; # 短的語句修改
請記住這樣的特殊技巧要求一個 while 循環。如果你在其他的什麼地方使用這樣的輸入操作符,你必須明確地把結果賦給變量以保留其值:
while(<FH1>&& <fh2>) { ... } # 錯誤:兩個輸入都丟棄 if (<STDIN>) { print; } # 錯誤:打印$_原先的值 if ($_=<STDIN>) {PRINT; } # 有小問題:沒有測試是否定義 if (defined($_=<STDIN>)) { print;} # 最好
當你在一個 $_ 循環裏隱含的給 $_ 賦值的時候,你賦值的對象是同名全局變量,而不是 while 循環裏的那隻局部的。你可以用下面方法保護一個現存的 $_ 的值:
while(local $_=) { print; } # 使用局部 $_
當循環完成後,恢復到原來的值。不過,$_ 仍然是一個全局變量,所以,不管有意無意,從那個循環裏調用的函數仍然能夠訪問它。當然你也可以避免這些事情的發生,只要定義一個文本變量就行了:
while (my $line =) { print $line;} # 現在是私有的了
(這裏的兩個 while 循環仍然隱含地進行測試,看賦值結果是否已定義,因爲 my 和 local 並不改變分析器看到的賦值。)文件句柄 STDIN,STDOUT,和 STDERR 都是預定義和預先打開的。額外的文件句柄可以用 open 或 sysopen 函數創建。參閱第二十九章裏面那些函數的文檔獲取詳細信息。
在上面的 while 循環裏,我們是在一個標量環境裏計算行輸入操作符,所以該操作符分別返回每一行。不過,如果你在一個列表環境裏使用這個操作符,則返回一個包括所有其餘輸入行的列表,每個列表元素一行。用這個方法你會很容易就使用一個很大的數據空間,所以一定要小心使用這個特性:
$one_line =; # 獲取第一行 $all_lines =; # 獲取文件其餘部分。
沒有哪種 while 處理和輸入操作符的列表形式相關聯,因爲 while 循環的條件總是提供一個標量環境(就象在任何其他條件語句裏一樣)。
在一個尖角操作符裏面使用空(null)文件句柄是一種特殊用法;它仿真典型的 Unix 的命令行過濾程序(象 sed 和 awk)的特性。當你從一個 <> 讀取數據行的時候,它會魔術般的把所有你在命令行上提到的所有文件的所有數據行都交給你。如果你沒有(在命令行)上聲明文件,它就把標準輸入交給你,這樣你的程序就可以很容易地插入到一個管道或者一個進程中。
下面是其工作原理的說明:當第一次計算 <> 時,先檢查 @ARGV 數組,如果它是空(null),則 $ARGV[0] 設置爲 “-”,這樣當你打開它的時候就是標準輸入。然後 @ARGV 數組被當作一個文件名列表處理。更明確地說,下面循環:
while (<>) { ... # 處理每行的代碼 }
等效於下面的類 Perl 的僞代碼:
@ARGV = ('-') unless @ARGV; # 若爲空則假設爲STDIN while( @ARGV) { $ARGV = shift @ARGV; # 每次縮短@ARGV if( !open(ARGV, $ARGV)) { warn "Can't open $ARGV: $!/n"; next; } while (<ARGV>) { ... # 處理每行的代碼 } }第一段代碼除了沒有那麼嘮叨以外,實際上是一樣的。它實際上也移動 @ARGV,然後把當前文件名放到全局變量 $ARGV 裏面。它也在內部使用了特殊的文件句柄 ARGV——<> 只是更明確的寫法 <ARGV>(也是一個特殊文件句柄)的一個同義詞,(上面的僞代碼不能運行,因爲它把當作一個普通句柄使用。)
你可以在第一個 <> 語句之前修改 @ARGV,直到數組最後包含你真正需要的文件名列表爲止。因爲 Perl 在這裏使用普通的 open 函數,所以如果碰到一個“-”的文件名,就會把它當作標準輸入,而其他更深奧的 open 特性是是 Perl 自動提供給你的(比如打開一個名字是“gzip -dc <file.gz|”的文件)。行號($.)是連續的,就好象你打開的文件是一個大文件一樣。(不過你可以重置行號,參閱第二十九章看看當到了 eof 時怎樣爲每個文件重置行號。)
如果你想把 @ARGV 設置爲你自己的文件列表,直接用:
# 如果沒有給出 args 則缺省爲 README @ARGV = ("README") unless @ARGV;
如果你想給你的腳本傳遞開關,你可以用 Getopt::* 模塊或者在開頭放一個下面這樣的循環:
while( @ARGV and $ARGV[0] =~ /^-/) { $_ = shift; last if /^--$/; if (/^-D(.*)/) {$debug = $1 } if (/^-v/) { $verbose++} ... # 其他開關 } while(<>){ ... # 處理每行的代碼 }
符號 <> 將只會返回一次假。如果從這(返回假)以後你再次調用它,它就假設你正在處理另外一個 @ARGV 列表,如果你沒有設置 @ARGV,它會從 STDIN 裏輸入。
如果尖括弧裏面的字串是一個標量變量(比如,<$foo>),那麼該變量包含一個間接文件句柄,不是你準備從中獲取輸入的文件句柄的名字就是一個這樣的文件句柄的引用。比如:
$fh = /*STDIN; $line = <$fh>;
或:
open($fh, "<data.txt"); $line = <$fh>;
2.11.3 文件名聚集操作符
你可能會問:如果我們在尖角操作符裏放上一些更有趣的東西,行輸入操作符會變成什麼呢?答案是它會變異成不同的操作符。如果在尖角操作符裏面的字串不是文件句柄名或標量變量(甚至只是多了一個空格),它就會被解釋成一個要 “聚集”(注:文件團和前面提到的類型團毫無關係,除了它們都把 * 字符用於通配符模式以外。當用做通配符用途時,字符 * 有“聚集”(glob)的別名。對於類型團而言,它是聚集符號表裏相同名字的符號。對於文件團而言,它在一個目錄裏做通配符匹配,就象各種 shell 做的一樣。)的文件名模式。這裏的文件名模式與當前目錄裏的(或者作爲文件團模式的一部分直接聲明的目錄)文件名進行匹配,並且匹配的文件名被該操作符返回。對於行輸入而言,在標量環境裏每次返回一個名字,而在列表環境裏則是一起返回。後面一種用法更常見;你常看到這樣的東西:
@files = <*.xml>;
和其他僞文本一樣,首先進行一層的變量代換,不過你不能說 <$foo>,因爲我們前面已經解釋過,那是一種間接文件句柄。在老版本的 Perl 裏,程序員可以用插入花括弧的方法來強制它解釋成文件團:<${foo}>。現在,我們認爲把它當作內部函數 glob($foo) 調用更爲清晰,這麼做也可能是在第一時間進行干預的正確方法。所以,如果你不想重載尖角操作符(你可以這麼幹。)你可以這麼寫:
@files = glob("*.xml");
不管你用 glob 函數還是老式的尖括弧形式,文件團操作符還是會象行輸入操作符那樣做 while 特殊處理,把結果賦予 $_。(也是在第一時間重載尖角操作符的基本原理。)比如,如果你想修改你的所有 C 源代碼文件的權限,你可以說:
while (glob "*.c") { chmod 0644, $_; }
等效於:
while (<*.c>) { chmod 0644,$_; }
最初 glob 函數在老的 Perl 版本里是作爲一個 shell 命令實現的(甚至在舊版的 Unix 裏也一樣),這意味着運行它開銷相當大,而且,更糟的是它不是在所有地方運行得都一樣。現在它是一個內建的函數,因此更可靠並且快多了。參閱第三十二章裏的 File:Glob 模塊的描述獲取如何修改這個操作符的缺省特性的信息,比如如何讓它把操作數(參數)裏面的空白當作路徑名分隔符,是否擴展發音符或花括弧,是否大小寫敏感和是否對返回值排序等等。
當然,處理上面 chmod 命令的最短的和可能最易讀的方法是把文件團當作一個列表操作符處理:
chmod 0644, <*.c>;
文件團只有在開始(處理)一個新列表的時候才計算它(內嵌)的操作數。所有數值必須在該操作符開始處理之前讀取。這在列表環境裏不算什麼問題,因爲你自動獲取全部數值。不過,在標量環境裏時,每次調用操作符都返回下一個值,或者當你的數值用光後返回一個假值。同樣,假值只會返回一次。所以如果你預期從文件團裏獲取單個數值,好些的方法是:
($file) =; # 列表環境
上面的方法要比:
$fiole =; # 標量環境
好,因爲前者返回所有匹配的文件名並重置該操作符,而後者要麼返回文件名,要麼返回假。
如果你準備使用變量代換功能,那麼使用 glob 操作符絕對比使用老式表示法要好,因爲老方法會導致與間接文件句柄的混淆。這也是爲什麼說項和操作符之間的邊界線有些模糊的原因:
@files = <$dir/*.[ch]>; # 能用,不過應該避免這麼用。 @files = glob("dir/*.[ch]"); # 把glob當函數用。 @files = glob $some_pattern; # 把glob當操作符用。
我們在最後一個例子裏把圓括弧去掉是爲了表明 glob 可以作爲函數(一個項)使用或者是一個單目操作符用;也就是說,一個接受一個參數的前綴操作符。glob 操作符是一個命名的單目操作符的例子;是我們下一章將要談到的操作符。稍後,我們將談談模式匹配操作符,它也是分析起來類似項,而作用象操作符。