天天看點

《高階Perl》——1.4 階層化資料

已經介紹的例子顯示了遞歸過程的風采,但是它們遺漏了重要的一點。在介紹漢諾塔問題時,我說過當你想解決的問題可以簡化成同樣問題的更簡形式時,遞歸是很有用的。但是這樣的問題是普遍的這一點可能不清楚。

多數遞歸的函數是為處理遞歸的資料結構的建立。一個遞歸的資料結構就像一個清單、一棵樹或者一個堆,它們根據相同資料結構的更小的執行個體定義而成。最常見的例子可能是檔案系統目錄結構。檔案可以是:

《高階Perl》——1.4 階層化資料

檔案可以是一個目錄,含有一列檔案,其中的一些可以是目錄,再含有另一些檔案,以此類推。處理這樣的結構最有效的辦法是遞歸過程。從概念上,每次調用這樣一個過程就處理一個單一檔案。這個檔案可以是普通檔案,也可以是目錄,程式通過遞歸調用自身處理這個目錄所含有的子檔案。如果子檔案也是目錄,程式将繼續遞歸調用。

下面的例子是一個函數,以目錄名作為參數計算它包含的所有檔案的總大小,以及它的子目錄、子目錄的子目錄等。

初次調用此函數時,它有一個參數$top,是要檢查的檔案或目錄的名字。函數首先用perl的-s操作符得到這個檔案或目錄本身的大小。此操作符産生以位元組為機關的檔案大小。如果檔案是一個目錄,此操作符得到目錄本身占用的磁盤空間,而不是指目錄所含檔案所占用的。請記住,目錄本身是檔案的一個清單,這個清單也占用一些空間。如果頂層檔案确實是個目錄,此函數會把它的内容的大小累計到總和$total中。

-f操作符檢查參數是否是普通檔案,如果是,那麼函數可以直接傳回總和。否則,它假設此頂層檔案确實是個目錄,并嘗試用opendir()打開它。如果無法打開目錄,函數發出一條警告消息并傳回到目前的總值,它包括目錄本身的大小,但不包括它所含内容的大小。

接下來的代碼塊,while循環,是函數的核心。它每次從目錄中讀取一個檔案名,對後者遞歸調用自身,把結果累計到總和中。

循環結束後,函數關閉目錄并傳回總和。

在循環中,函數略過了檔案名如.和..,這些分别是目錄本身和它的父目錄的别名,如果函數不這樣做,它将永不會結束,因為它将嘗試計算許許多多的檔案,如././././././fred和dir/../dir/../dir/../dir/fred。

這個函數有個大bug,事實上它根本不能工作。問題在目錄句柄,即dir,是全局的,是以函數是不可重入的。函數會失效,本質上和缺少my的factorial()失效一樣。第一次調用進行順利,但是如果total_size()遞歸調用自身,即第二次執行将打開同一個目錄句柄dir。最後,第二次調用将到達它的目錄的終點,關閉dir,然後傳回,從這以後,第一次調用将嘗試繼續,發現dir已經關閉了,那就退出while循環,還未從頂層目錄讀取所有的檔案名。第二次執行遞歸調用自身時,也将有同樣的問題。

結果就是寫成的函數隻周遊了目錄樹的第一個分支。如果目錄層次有一個這樣的結構,

《高階Perl》——1.4 階層化資料

那函數将沿着top-a-d路徑,看到檔案j和k,然後報告top+a+d+j+k的總大小,而沒有注意到b、c、e、f、g、h、i或l。

為了修正這個錯誤,需要使目錄句柄dir成為一個私有變量,像$top和$total一樣。這裡沒有用opendir dir, $top,而是使用opendir $dir, $top,其中$dir是私有變量。當opendir的第一個參數是一個未定義的變量,opendir會産生一個新的、匿名的目錄句柄并把它儲存到$dir。

與其這樣做:

不如這樣做:

達到同樣的效果。這有很大的差別:dir是個全局目錄句柄,能被程式的任何其他部分讀取或關閉;$dir中的目錄句柄是私有的,僅能被生成它的函數讀取或關閉,或者被一些明确地傳遞了$dir的值的函數讀取或關閉。

有了這個新的技巧,就可以重寫total_size()函數以使其正确運作:

實際上,此處的closedir不是必需的,因為此種方法産生的目錄句柄會在含有它的變量離開作用域時自動關閉。當total_size()傳回時,它的私有變量銷毀了,包括$dir,它包含了打開的目錄句柄對象的最後一個引用。perl接着銷毀目錄句柄對象,在此過程中,關閉目錄句柄。未來将省略明确的closedir。

這個函數仍然有些問題:它沒有正确處理符号連結,如果一個檔案在同一目錄中有兩個名字,它會統計兩次。此外,在unix系統裡,一個檔案在磁盤上實際所占的空間通常與-s報告的長度不同,因為磁盤空間是每次按1024位元組的塊配置設定的。但是函數足夠有用了,是以可以期望它很好地應用到一些别的任務中。如果要修正這些問題,将隻需在這一處修正它們,而不用在50個不同的應用中50個略微不同的目錄周遊函數中修正同樣的問題。