讀古今文學網 > 精通正則表達式(第3版) > 正則表達式相關的Perl教義 >

正則表達式相關的Perl教義

Regex-Related Perlisms

學習正則表達式,還需要掌握許多一般的Perl概念。下面幾節的內容包括:

●應用場合(context)Perl 的重要概念之一就是,許多函數和運算符在不同應用場合有不同的意義。例如,Perl的 while循環希望接收一個純量值(scalar value)作為判斷條件,但對於print語句希望接收一組值(a list of value)。因為Perl容許表達式對其應用場合進行「響應」(respond),同樣的表達式在不同的應用場合可能得到截然不同的結果。

●動態作用域(dynamic scope)大多數編程語言都支持本地變量和全局變量,但是 Perl還提供了另一種複雜功能,稱為動態作用域。動態作用域會臨時「保護」全局變量,保存一份副本,稍後自動恢復。這個複雜的概念對我們來說很重要,因為它影響到$1和其他的匹配相關變量。

表達式應用場合

Expression Context

context對Perl來說是很重要的概念,尤其對match運算符來說更是如此。一個表達式可能出現在三種context中:序列(list)、純量值(scalar)或者空(void),它們表示表達式期望接收的參數類型。所以,list context說明表達式期望獲得一個序列。scalar context說明表達式期望獲得單個值。以上兩者極為常見,而且對使用正則表達式非常有價值。void context說明不期望獲得任何值。

看下面兩個賦值:

因為$s是scalar變量(它用來保存單個的值,而不是序列),期望簡單的純量值,所以第一個表達式的應用場合為scalar context。同樣,因為@a是一個數組,期望獲得一個list,第二個表達式的應用場合為list context。即使這兩個表達式完全等價,也可能返回完全不同的結果,產生不同的影響。具體情況依表達式而定。

舉例來說,localtime函數如果用在list context中,會返回一組值,表示當前年、月、日、時。但如果用在scalar context中,則返回文本類型的當前時間,比如『Mon Jan 20 22:05:15 2003』。

另一個例子是<MYDATA>之類的I/O運算符,在scalar context中,它返回文件的下一行,但是在list context中,返回所有(剩下的)行。

就像localtime和I/O運算符一樣,許多Perl的結構會根據應用場合的不同返回不同的值,正則運算符同樣如此。拿 match 運算符 m/…/來說,有時候它會簡單地返回true/false 值,有時候返回一組匹配結果。所有的細節都會在本章講解。

強轉正則表達式

不是所有的正則表達式天生都能區分場合的,所以,如果某個應用場合中正則表達式無法提供期望的返回類型,就要按照 Perl 的規定處理。為了把方樁插入圓孔,Perl 會「強轉(contort)」這個值。如果在list context中返回的是scalar值,Perl會生成只包含單個元素的list。這樣@a=42就等於@a=(42)。

另一方面,把list轉換為scalar卻沒有統一的規定。如果程序是這樣:

$var=($this,&is,0xA,'list');

逗號運算符返回最後的元素『list』給$var。如果給定的是一個數組,例如$var=@array,則返回數組的長度。

其他語言用不同的術語描述這種處理,例如修正(cast)、提示(promote)、強制轉換(coerce)或轉換(convert),但是我認為這些詞都已經具有了自己的意義(有點令人討厭),不適合描述Perl的做法,所以我使用「強轉(contort)」。

動態作用域及正則匹配效應

Dynamic Scope and Regex Match Effects

Perl的變量分為兩類(全局變量和私有變量),動態作用域是正確理解它們的重點,研究正則表達式時也需要關注此概念,因為它關係到匹配完成之後信息如何使用。下一節介紹了這些概念及其與正則表達式的關係。

全局和私有變量

總的來說,Perl 提供了兩種變量:全局的和私有的。私有變量使用 my(…)來聲明,全局變量不需要聲明,在使用時會自動出現。全局變量通常在程序的任何地方都是可見的,而私有變量,按照語言的規定只有在它們所屬的代碼塊之內才是可見的。也就是說,只有私有變量聲明所在的代碼塊之內的Perl代碼,能夠訪問私有變量。

全局變量的使用則很普通,只是有的特殊變量不太好理解,例如$1、$_、@ARGB 之類。普通用戶的變量是全局的,除非它們以my來聲明,否則即使它們「看上去」是私有的,也是全局變量。按照Perl的規定,Package Acme::Widget中的全局變量$Debug,雖然有完整的限定名$Acme::Widget::Debug,仍然是一個全局變量。如果出現了 use strict;,則所有(不包括特殊的)全局變量必須使用完整的限定名,或者通過our來聲明(our聲明一個名稱(name),而不是一個新變量,請參考Perl的文檔)。

使用動態作用域的值

動態作用域(dynamic scoping)是個值得一提的概念,很少有編程語言提供這種功能。下文會講解它與正則表達式的關係,簡單地說,動態作用域可以讓Perl保存全局變量的一個副本,在某個代碼塊中修改此副本,退出之後自動恢復原來的值。保存副本的操作就稱為生成動態作用域(creating a new dynamic scope),或者本地化(localizing)。

使用動態作用域的原因之一是為了臨時改變某些保存在全局變量中的某些全局狀態。舉例來說,package Acme::Widget 提供了一個調試標誌位(flag),我們可以修改全局變量$Acme::Widget::Debug來啟用或者停用調試功能。下面的代碼可以臨時改變此標誌位:

local函數的命名很成問題,但它生成了一個新的動態作用域。調用 local並沒有創造新的變量,local是行為,而不是聲明。在全局變量之前,local做了三步處理:

1.在內部保存變量值的副本;

2.把新值賦予到變量(無論是undef還是傳給local的值);

3.local代碼塊執行結束之後,把變量恢復到之前的值。

也就是說,「local」指的是對變量的修改的持續時間。對本地化的變量來說,持續時間就是代碼塊執行的時間。如果代碼塊中調用了子程序,本地化的值仍然保留(畢竟,變量仍然是一個全局變量)。它與非本地化的全局變量的唯一區別是,在代碼塊執行完成之後,之前的值會被恢復。

local對全局變量的自動保存和恢復比想像的要複雜。請參考表7-4右側,詳細瞭解背後的處理。

為方便起見,我們也可以給本地變量賦一個值local($SomeVar),這等於把undef賦值給$SomeVar。如果不使用括號,表示強制使用scalar context。

舉個實際的例子,我們需要調用一個寫得很糟糕的函數,而它會產生許多「Use of uninitialized value」的警告。優秀的Perl程序員都會使用Perl的-w選項來解決這個問題,但是庫的作者

表7-4:local的含義

顯然沒有。你對這些警告非常惱火,但是如果不能修改程序庫,有什麼其他簡便辦法來代替-w嗎?這時候可以使用對$^W的調用的local,即時關閉警報(^W可以表示為兩個字符,脫字符和『W』,也就是ctrl+W)。

無論全局變量$^W是什麼值,調用local保存都會為其保存一份內部副本。然後$^W被用戶置為 0。上面提到的糟糕程序在執行時,Perl 檢查$^W,發現其值為 0,就不會發出警報。在函數返回時,新值0仍然有效。

這樣看來,不用 local 的話似乎也沒有問題。不過,在子程序返回,代碼塊退出時,$^W會恢復到之前的值。這種改變是本地的、即時的,只在代碼塊內部生效。按照表7-4右側的做法,用戶可以手工生成和返回副本,達到同樣的效果,但是local更為方便。

考慮在其他情況下會發生什麼,比如用 my替代 local(注4)。my會新建一個變量,其初始值是undef。只有在聲明的代碼塊中才可見(也就是說,在my和它所在的代碼塊結束之間)。它不會改變、修改,或以其他方式引用和影響其他變量,包括可能存在的同樣名字的全局變量。新建的變量在程序的其他部分都不可見,包括在那個糟糕的程序內。這樣新的$^W的確被置為0,但永遠不會再使用或者引用,所以它完全是白費工夫(執行糟糕的程序時,Perl根據與其無關的全局變量$^W決定是否報警)。

更好的比喻:充分的透明度

可以這樣理解 local,它對變量的修改是用戶完全無法察覺的(好像是把新值投影到原變量之上)。用戶(還包括能看到的任何人,例如子程序和信號處理程序)會看到這些新的值。在代碼塊結束之前,local的修改會取代之前的值。退出之後,這種透明特性會自動消除,也就是取消local進行的所有修改。

相比「保存一個內部副本」,這個比喻更接近現實。使用local並不會生成一個副本,而是在訪問變量時,使用新設置的值(即屏蔽原來的值)。退出代碼塊之後會拋棄新設置的值。調用 local時,新值是手動設置的,但我們要講解這些細節的原因在於:正則表達式的伴隨效應變量(side-effect variables)會自動使用動態作用域。

正則表達式的伴隨效應和動態作用域

正則表達式與動態作用域有什麼關係呢?關係很大。作為伴隨效應,許多變量——例如$&(引用匹配的文本)和$1(引用第一組括號內表達式匹配的文本)——會在匹配成功時自動設置。在下一節會詳細討論這些問題。在其所處的代碼塊中,這些變量都會自動使用動態作用域。

這種設計的好處在於,每次調用子程序都要啟動新的代碼塊,也就是為這些變量提供了新的動態作用域範圍。因為在代碼塊之前的值會在代碼塊執行完之後恢復(也就是子程序返回時),子程序不能改變調用方能看到的值。

來看個例子:

因為$1 的值在進入代碼塊時進行了動態作用域處理,這段代碼不關心也不必關心,函數DoSomeOtherStuff是否改變了$1的值。此函數對$1的任何改動都只在函數定義的代碼塊內部,或者函數的子代碼塊中生效。所以,DoSomeOtherStuff不會影響 print接收的$1的值。

自動使用動態作用域很有用,雖然有時候不那麼明顯:

標準庫模塊 Config 定義了一個關聯數組(associative array)%Config,其成員$Config-{perladmin}保存本地Perlmaster的E-mail地址。如果$1沒有使用動態作用域,這段代碼就很難理解,因為%Confg 是一個綁定變量(tied variable)。也就是說,對它的任何引用都意味著幕後的子程序調用,用$Config{…}進行正則表達式匹配時,Config 中的子程序返回對應的值。這次匹配發生在上一行的匹配和對$1的使用中間,所以如果$1沒有使用動態作用域,它的值會被修改。所以,$Config{…}中對$1 的任何修改都被動態作用域安全地保護了起來。

動態作用域還是詞法作用域

如果使用恰當,動態作用域能提供許多便利,但是濫用動態作用域會帶來無休止的噩夢,因為閱讀程序的人很難理解,分散在散落的 local、子程序和本地變量引用之間的複雜交互。

我曾說,my(…)聲明會在詞法範圍(lexical scope)內創造一個私有變量。與私有變量的詞法範圍對應的是全局變量的範圍,但是詞法範圍與動態作用域沒有關係(僅有的聯繫是:不能對my變量調用local)。請記住,local只是行為(action),而my既是行為,又是聲明,這很重要。

匹配修改的特殊變量

Special Variables Modified by a Match

成功的匹配會設置一組只讀的全局變量,它們通常會自動使用動態作用域。如果匹配不成功,這些值永遠也不會改變。在需要的時候,它們會設置為空字符串(不包括任何字符的字符串)或者undefind(「未定義」,一個「沒有值」的值,與空字符串類似,但測試時兩者不相等)。表7-5給出了若干例子。

詳細地說,匹配完成之後會設置這些變量:

$&正則表達式所匹配文本的副本。從效率方面考慮(參見第356頁的討論),最好不要使用這個變量(還包括下面介紹的$ˋ 和$)。一旦匹配成功,$&就不會是未定義狀態,儘管它可能是空字符串。

表7-5:匹配後特殊變量的說明

在目標文本中匹配開始之前(左邊)文本的副本。如果使用/g修飾符,你可以期望$

的起點是開始嘗試位置的文本,但它每次都是從整個字符串的開始位置開始的。如果匹

配成功,肯定不會是未定義狀態。

保存目標文本中匹配成功文本之後(右邊)的文本的副本。如果匹配成功,肯定不會是未定義狀態。匹配成功之後,字符串就是目標字符串的副本(注5)。

$1、$2、$3、…

對應第 1、2、3 組捕獲型括號匹配的文本(請注意,這裡沒有$0,因為它是腳本的名字,與正則表達式無關)。如果它們對應的括號在表達式中不存在,或者沒有實際參與匹配,則設置為未定義狀態。

匹配之後就可以使用這些變量,在s/…/…/中的replacement也可以使用。它們還能在動態正則結構或者嵌入代碼中使用(☞327)。在正則表達式中使用這些變量是沒多少意義的(因為已經有了「\1」之類)。請參考第303頁的「在正則表達式中使用$1」。

「(\w+)」和「(\w)+」的區別可以用來說明$1的設置方式。兩個表達式都能匹配同樣的文注5:事實上,即使目標字符串是未定義的,但能匹配成功(雖然不太現實,但有可能),是一個空字符串,而不是未定義。只有在這種情況下,兩者才不一樣。

本,但是它們的區別在於括號內的子表達式匹配的內容。用這個表達式匹配字符串『tubby』,第一個表達式的$1的內容是『tubby』,而第二個表達式中的$1只包含『y』:在「(\w)+」中,加號在括號外面,所以每次迭代都會重新捕獲,$1保留最後的字符。

還需要注意的是「(x)?」和「(x?)」的差別。前一個表達式中括號及其捕獲內容不是必然出現的,所以$1可能是『x』,或者是未定義,但「(x?)」中括號在匹配的外面——匹配的內容不是必然出現的,但匹配必須發生。如果整個表達式匹配成功,這部分的匹配必然會發生,儘管「x?」匹配的是空字符串。所以在「(x?)」中,$1可能是『x』或者是空字符串。下表給出了一些例子:

從上表可以看出,如果需要添加括號來捕獲文本,如何添加取決於我們的意圖。在所舉的例子中,增加的括號對整體匹配沒有影響(整體匹配是不變的),其中唯一的區別就是$1設置的伴隨效應。

$+ 表示$1、$2等匹配過程中明確設定的,編號最大的變量的副本。在下面的情況中會有用:

如果沒有$+,我們可能需要依次檢查$1、$2和$3,才能找出明確設置的那個。

如果正則表達式中沒有捕獲型括號(或者在匹配中沒有用到),則這個值為未定義。

$^N 最後結束的,在匹配中明確設定的括號匹配的文本的副本(明確設定的$1等變量中,閉括號在最後)。如果正則表達式中沒有捕獲型括號(或者匹配中沒有用到),則其值為未定義。第344頁開頭有個恰當的例子。

@-和@+

表示各捕獲型括號所匹配文本的起始和結束位置在目標文本中偏移值的數組。使用起來可能有點迷惑,因為它們的名字比較怪異。兩個數組的第一個元素都對應整體匹配。也就是說,通過$-[0]訪問到的@-的第一個元素,是整個匹配在目標字符串中的偏移值,即:

$-[0]的值為 8,代表匹配從目標字符串的第 8 個位置開始(在 Perl 中,偏移值從 0開始)。

@+的第一個元素通過$+[0]訪問,表示對應匹配文本結束位置的偏移值。在上例中其值為 9,表示整體匹配結束於目標字符串的第 9 個字符之前。所以,如果$text沒有變化,substr($text,$-[0],$+[0]-$-[0])就等於$&,但沒有$&那樣的性能缺陷(☞356),下例給出了@-的簡單用法:

1 while $line=~s/\t/''x (8-$-[0]%8)/e;

它會把給定文本中的製表符(tab)替換為合適長度的空格序列(注6)。

兩個數組中接下來的元素分別對應各捕獲分組的開始位置和結束位置的偏移值。$-[1]和$+[1]對應$1,$-[2]和$+[2]對應$2,依次類推。

$^R 這個變量保存最近執行的嵌入代碼的結果,如果嵌入代碼結構作為「(?if then|else)」條件語句(☞140)中的if 部分,則不設定$^R。在正則表達式內部(即在嵌入代碼或者動態正則結構☞327 中),它會自動根據匹配的各部分進行本地化處理,所以因為回溯而「交還」的代碼對應的$^R的值會被放棄。換一種說法就是,它保存引擎到達當前狀態的工作路徑中「最近」的值。

如果正則表達式根據/g修飾符重複使用,那麼每次循環都會重新設置這些變量。也就是說可以在s/…/…/g中使用$1,因為每次匹配時它的值都不一樣。

在正則表達式中使用$1

Perl手冊專門提到,在正則表達式外部,不能用「\1」反向引用(而應該使用$1)。變量$1對應上次成功匹配中的某個固定字符串。「\1」則是正則表達式元字符,它對應正則引擎遇到「\1」時第一組捕獲型括號捕獲的文本。在NFA的回溯過程中,它的值可能會變化。

與之對應的問題是,正則運算元中是否能夠使用$1之類的變量。通常,在內嵌代碼或者動態正則結構(☞327)中可用,在其他情況下就沒什麼意義。出現在運算元中「表達式部分」的$1與其他變量一樣處理:在匹配或替換操作開始時插值。也就是說,對正則表達式而言,$1與當前的匹配沒什麼關係,它屬於上一次匹配。