« 4. 基本的控制結構 | Perl 學習手札目录 | 6. 副常式 »

5. 雜湊(Hash)

5. 雜湊(Hash)
雜湊對一般使用者大概都非常不熟悉,尤其是沒接觸過Perl的人來說,雜湊對他們來說都是全新名詞。但是在現實生活中,雜湊卻是不斷出現在一般人的生活之中。因此只要搞懂雜湊到底在講甚麼,你就會覺得這個東西用起來真是自然極了,而且沒有了雜湊還可能讓很多事情顯得不知所措,因為你要花大量的時間跟精力才能利用其他資料結構做出雜湊所達到的結果。
聽起來雜湊確實非常吸引人,那我們先來了解一下甚麼是雜湊。所謂的雜湊,其實用最簡單的話來說,也就是一對鍵值(key-value),沒錯,就是這麼簡單,一個鍵搭配著一個值的對應方式。當然,你可以搭配Perl所提供複雜的方式來建立多層的雜湊來符合程式的需要,不過那不是基本的雜湊,而且所謂複雜的結構,還是依循著最簡單的原理,也就是鍵跟值的相對應關係。
5.1 日常生活的雜湊
沒錯,如果我們只說雜湊是一對鍵值的組合,那要讓人真正理解顯然並不容易。所以如果我們可以使用一般人常用的詞彙來解釋雜湊這個東西,顯然應該會容易許多。既然如此,我們就來看看大家每天接觸的資料中,有甚麼是能夠以雜湊精確的表現出來的。
最簡單的例子應該算是身份證字號了吧,我們可以很容易的用身份證字號知道一個人的姓名,其中的身份證字號就是雜湊的鍵(key),而利用這個鍵所得到的值(value)就是姓名。而且鍵是這個雜湊中唯一的值,也就是一個雜湊中,不能有重複的鍵。這也應該很明顯,如果有兩個一模一樣的身份證字號,那麼我們要怎麼確認使用者希望找到的是那一個呢?所以這也是雜湊中的限制,我們必須要求雜湊中的鍵值必須是不重複的,很顯然,這樣的限制是非常合理的。另外,我們每個人的行動電話中也藏著使用雜湊的好素材。如果你曾經使用行動電話的電話簿功能,那麼你也許每天都會接觸這種非常雜湊式的結構,因為電話簿功能也是雜湊足以發揮功用的地方。電話簿就是一整個的雜湊,他裡面的鍵是以姓名為主,值則是這個人的電話。所以你必須為每一個人鍵入一個獨特的鍵,大多也就是名字,以及這個鍵所對應的值,當然就是電話了。因為我們只要找到鍵(姓名)就可以查到依附在這個鍵的值(電話)。如此一來,我們應該很容易可以理解雜湊的代表意義了。
5.2 雜湊的表達
雜湊在Perl中是以百分比符號(%)作為表示,變數的命名方式則維持一貫原則,也就是可以包含字母,數字及底線的字串,但是不能以數字作為開頭。所以你可以像這樣的方式定義一個雜湊變數:


my %hash;   # 基本的命名方式 my %ID_Hash;   # 包含底線的變數 my %id_hash;   # 大小寫還是被認為是不同的字串 my %_underline;   # 以底線開始的變數名稱 my %2hash;   # 程式會產生錯誤,因為這不是合法的變數

雜湊的存取,我們可以利用大括號來進行,因此我們把所想要取得的雜湊鍵放入大括號中,就可以藉此找到相對應的值。同樣的方式,我們也可以利用這樣的形式指定某一對鍵值,這樣的作法非常接近我們存取陣列的形式:


my %hash; $hash{key} = 'value';  # 最簡單的賦值形式 print $hash{key};

就如我們說的,我們是使用大括號({})來標示所要存取的雜湊鍵,這和使用陣列是不同的。不過更重要的是千萬別把你的程式寫得像這個樣子:


my $var = 1; my @var = (1, 2, 3, 4, 5, 6); my %var; $var{1} = 2; $var{3} = 4; $var{5} = 6; print $var[2];

這樣的形式對於Perl來說當然是合法的,不過我們顯然不希望你用這樣的形式來寫程式,否則即使Perl可以很容易的分辨出來,只怕寫程式或維護的人自己還先搞混了。
有時候,我們會忽略一些小地方,那就會讓自己找不到雜湊中的值,其中有一個非常重要的部份,也就是雜湊鍵的資料型態。Perl會把雜湊鍵全部轉為字串,這樣的轉換其實是有些道理的。我們來研究一下這樣的程式會發生甚麼狀況呢:


my %hash; $hash{2} = 'two';   # 指定雜湊的一對鍵值 $hash{'4/2'} = '這是字串 4/2';   # 注意引號的使用 print $hash{4/2};   # 先運算後轉為字串的鍵

你認為Perl會輸出甚麼樣的結果呢?答案是'two'。沒錯,很有趣吧,所以你可以在雜湊鍵的地方放上一個運算式,那麼Perl會先進行運算,然後把運算結果轉為字串,所以上面的例子,我們所要求Perl輸出的其實是$hash{2},否則你可以利用引號來指定字串,就像$hash{'4/2'}這樣的方式。我們再看看另一個例子:


my %hash; for (1...5) { $hash{$_*2} = $_**2; }

那我們可以得到的雜湊就是像是這個樣子:


$hash{2} = 1; $hash{4} = 4; $hash{6} = 9; $hash{8} = 16; $hash{10} = 25;

沒錯,正如我們所預料的,Perl會把運算出來的結果轉為字串後當成雜湊的鍵。還記得我們可以利用字串的內插方式來插入變數到字串嗎?你可以猜測以下的程式會產生出甚麼不同的結果:


my %hash; for (1...5) { $hash{"$_*2"} = $_**2; }

如果你可以想辦法看到雜湊的內容,你會發現你得到的雜湊鍵變成了 "1*2","2*2"......。沒錯,因為他們被視為一個字串了。所以如果你以為你可以利用$hash{2}或$hash{4}來得到雜湊內的值,恐怕會失望了。所以當你要開始使用雜湊時,可就要小心別搞混了。

5.3 雜湊賦值
我們剛剛學到了利用 $hash{2} = 4; 這樣的方式來指定一對鍵值給雜湊,沒錯,這是賦值給雜湊的最基本方式,不過就跟我們使用陣列一樣,我們經常需要一次指定大量的雜湊鍵值,想必Perl的開發者一定也會遇到相同的問題,而且應該有一些合理的解決方案。既然如此,我們應該有其他方式可以一次指定超過一組的鍵值。利用串列的方式賦值給雜湊就是其中之一,而且當你在定義某個雜湊時就預先知道他的一些鍵值時特別有用,看看下面的例子:


my %hash = qw/1 one 2 two 3 three/;

這樣的賦值方式看起來跟處理陣列時候的方式非常接近,我們利用qw//來指定一個串列,並且將這個串列賦值給雜湊。這時候,Perl會按照串列的順序,分別為【鍵】,【值】,並且賦予雜湊。所以在這個例子中,所得到的結果就跟我們這麼寫是一樣的:


$hash{1} = 'one'; $hash{2} = 'two'; $hash{3} = 'three';

或許你會想到某個狀況,也就是鍵值的個數不一的時候。這時候,Perl會把最後一個鍵所對應的值設為undef(註二),你可以利用這個程式來確認:


my %hash = (1, 2, 3, 4, 5); print 'false' unless defined($hash{5});

當然利用串列賦值的方式是方便了一些,可是就像我們剛剛遇到的問題,有時候會發現利用串列賦值的情況似乎比較容易發生錯誤。尤其當一個串列的元素足夠多的時候,你要怎麼確認某個串列中的元素應該是鍵,還是值呢?最簡單的方式大概就是進行人工比對,所以你或許可以考慮用另外的方式來賦值給雜湊,就像這樣的寫法:


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

在這裡,我們利用箭號(=>)來表示雜湊中鍵跟值的相對關係,而且在一對鍵值的後面加上逗號作為區隔。這樣的方式就顯得方便、也直覺了許多。不過當你在使用箭號進行指定時,你可能會發現一些不同。因為箭號左邊的雜湊鍵已經完全被視為一個字串,所以你如果使用這樣的方式:


my %hash = ( 4/2 => 3, ); print $hash{'4/2'}; print $hash{2};

別忘了,跟之前的狀況一樣,Perl還是會幫你先把箭號左邊的運算式算出結果,然後轉成字串,作為雜湊的鍵。所以當你在取值時使用了引號確保你要找雜湊鍵等於'4/2'的值時,你就沒辦法找到任何結果,因為目前雜湊中只有一個雜湊鍵為'2'的值。
要從雜湊中取出現有的值以目前的方式應該足夠方便,你只需要知道雜湊中的鍵,就可以取得他的內容值。不過這樣顯然還不夠,因為雜湊跟陣列還是有著相當的差異。在陣列中,你可以很清楚的知道陣列的索引值是從0到最後一個陣列的大小減1,可是在雜湊中卻並不是這麼一回事。如果你沒辦法知道雜湊的鍵,又怎麼取出他的值呢?那麼這個時候,你應該考慮先把整個雜湊讀過一次。

5.4 each
就像在陣列當中,你可以使用foreach這樣的迴圈來找到陣列中的每一個值,當然我們也經常需要在雜湊中進行類似的工作,我們希望可以在雜湊中能一次取出所有的鍵,值。所以你必須仰賴類似foreach的工具來幫助你,那就是each函數。例如你可以利用下面的寫法讀出剛剛我們所建立起來的雜湊:


while (my ($key, $value) = each (%hash)) {   # 取出雜湊中的每一對鍵值,並且分別放入$key, $value print "$key => $value\n"; }

很明顯的,每次each函數都會送回了一個包含兩個值的串列,其中這兩個值分別是一個雜湊鍵跟相對應的值。因此我們把取回的串列指定給$key和$value兩個變數,接著印出結果,就可以看到一對一對的鍵值了。而當傳回空陣列時,while判斷就會變成偽值,while迴圈也就結束了。利用這樣的函式對我們有很大的幫助,如果我們想要整理一個雜湊的內容,我們可以在完全不知道雜湊中有什麼內容的狀況下開始進行處理。使用each函數在處理雜湊時是讓事情顯得容易許多,可是有時候還是有點不方便的地方,舉例來說:如果我有一個包含著主機ip跟主機名稱的雜湊,雖然我不知道雜湊裡面到底有多少資料,可是我卻希望能找出所有的雜湊鍵值,然後取出以192開始的ip位址。這時候如果使用each來作,那就必須先把所有的鍵值取出,然後再一一進行比對,所以也許程式就像這樣:


my %hash = ( '168.1.2.1' => 'verdi', '192.1.2.2' => 'wagner', '168.1.2.3' => 'beethoven', );   # 定義主機跟ip 的對應 my @hostname; while (my ($key, $value) = each (%hash)) { if ($key =~ /^192/) {   # 要找出ip以192開頭的部份 push @hostname, $value;   # 找到之後放入新的陣列中 } } print @hostname;

很顯然,這樣的寫法確實可以讓程式正確的找出我們要的結果,不過我們總是還會繼續思考可以有更乾淨俐落的寫法,畢竟使用Perl的程式設計師都不太喜歡拉拉雜雜的程式。所以有甚麼方法可以讓過濾出需要的鍵值可以顯得方便些呢?

5.5 keys跟values
如果我們可以用簡單的方式一次取得雜湊的所有鍵(keys),那麼要進行過去的過程就非常容易,而我們所需要的就是過濾後留下來的鍵,跟他們的相對值。當然,有某些時候,你可能只想要拿到雜湊中的所有值,這時候你就不需要擔心他們是屬於什麼鍵的相關。為了因應這樣的需求,有兩個函數可以滿足我們,他們分別是keys跟values。很顯然的,這兩個函數所作的工作就是取出雜湊的鍵跟值。和使用 each相當不同的是:你可以只單讀取出所有的鍵,或所有的值,而不需要一次全部取出。
例如我們可以用這樣來把雜湊鍵放在同一個陣列中:


my @keys = keys(%hash);

如果你希望取出所有的值,那麼不妨這樣寫:


my @values = values(%hash);

當然,你可以用他來完成each的工作,就像這樣:


my @keys = keys(%hash); for (@keys) { print "$_ => $hash{$_}\n"; } 其實跟這麼寫是一樣的效果: while (my ($key, $value) = each(%hash)) { print "$key => $value\n"; }

不過你顯然會發現,有時候用keys/values比較簡單,有時候用each比較方便,當然,至於要使用何者是完全取決於你所想要得出的結果,或者你認為最省力,簡潔,或是效率比較好的寫法。

在雜湊中使用keys/values這兩個函數都傳回串列,因此我們可以把我們所得到的串列輕易的放入陣列,接下來再以陣列的方式進行運算。這樣的方便之處在於我們可以有很多可供利用的陣列函數,所以我們可以把剛剛的那個例子改寫成這樣:


my %hash = ( '168.1.2.1' => 'verdi', '192.1.2.2' => 'wagner', '168.1.2.3' => 'beethoven', ); my @keys = map { $hash{$_} } grep { (m/^192/) } keys(%hash); print @keys;

這樣的寫法比起之前的方式看起來是不是乾淨許多了呢?我們來看看最關鍵的一行,結果到底怎麼產生的:我們先用keys函數取出雜湊中的所有鍵,就如我們所說的,這個函數傳回一個串列。然後我們對所得到的串列進行過濾,利用grep取出串列中以192開頭的ip子串列,最後利用map一一比對得出雜湊中以對應這些ip的主機名稱。

5.6 雜湊的操作
毫無疑問,雜湊這樣的資料結構對於程式的寫作有著莫大的幫助,但是我們必須能熟悉對雜湊的操作才能夠讓我們更容易發揮雜湊的功能。其中最重要的大概就是exists跟delete兩個函數了,這兩個函式能讓我們有效的掌握雜湊的元素,同時它們也是perl內建相關於雜湊函數的最後兩個(註一)。
5.6.1 exists
我們就繼續用ip跟主機的雜湊當例子吧。假如我有一個ip,我不確定我是否有這部主機的資料,如果我們只用剛剛的方法,那我們就必須取得所有的ip,然後把手上的ip跟取得的ip串列一一比對,以便確定自己有沒有這個ip的主機資料。所以我們的程式也許長的像這樣:


my %hash = ( '168.1.2.1' => 'verdi', '192.1.2.2' => 'wagner', '168.1.2.3' => 'beethoven', ); my $ip = '192.1.2.2'; print "bingo" if ($hash{$ip});

在這裡,我們有一個雜湊,其中三個鍵分別是'168.1.2.1','192.1.2.2','168.1.2.3',而我們希望判定目前手上的一組ip'192.1.2.2'是不是我們主機所擁有的ip。於是我們利用這個ip作為雜湊鍵,並判斷如果取得的值為真,那麼我們就說這個ip屬於雜湊的其中一個鍵,這樣的想法似乎暫時解決了我們的需求。不過我們來看看下面的例子:


my %hash = ( 'cd' => 2, 'book' => 10, 'video' => 0, ); my $media = 'video'; print "bingo" if ($hash{$media});

我們假設這是某個社區圖書館目前外借的東西數量,其中的鍵就是代表則可以外借的圖書館資產,其中包含了CD,書跟錄影帶。而所對應到的值則是他們目前被借出的數量。我們看到,CD被借走了兩套,書被借走了十本,而錄影帶則是原封不動,一卷也沒被借走。是的,大家都不喜歡錄影帶了。
這時候,我們希望知道圖書館是否提供錄影帶外借,也就是要檢查video這個鍵是否存在。於是我們利用剛剛的方式,看看$hash{$media}是否傳回真值。很遺憾,因為錄影帶這個鍵目前的值是0,因此當我們利用錄影帶當成鍵來取的相對應的值時,Perl會傳回0給我們。而我們知道0其實是個偽值。於是我們以為'video'這個鍵並不存在於這個雜湊中,也就是說這個圖書館並沒有錄影帶出借,但是這樣的結果跟我們的認知有所不同,因為取得的值為0只是代表目前沒人借出。所以我們發現這個方法並不正確,至少我們已經知道他會產生錯誤的結果。所以我們必須嘗試其他方法,例如利用keys找到包含所有索引鍵的串列,然後進行一一的比對。就像這樣:


print "exist" if (grep { $_ eq 'video' } keys (%hash));

這樣就可以確定某個鍵是否存在於這個雜湊,可是程式還是有點長,而且我們也許必須經常去判斷某個值是否為雜湊的鍵。所幸Perl提供了簡潔的函式可以使用,所以利用exists這個函式讓我們有了極佳的判斷方式。有了exist之後,對於剛剛那一行程式,我們只需要這麼改寫:


print "exists" if (exists $hash{video});

這樣的寫法顯然輕鬆了許多。

5.6.2 delete
有些時候,我們也會遇到某些鍵值我們不再需要的狀況,這時候如果可以把這些沒有必要的鍵值移除似乎是非常必要的。所以Perl也提供了移除雜湊鍵值的函式,也就是delete。這個函式的使用其實非常容易,你只需要指定想要刪除的某一個雜湊鍵,就像這樣:


delete $hash{video};

當然,所謂的移除是指這個鍵將不再存在於這個雜湊,而不是指讓這個鍵對應的雜湊值消失。所以並不是把需要被delete的這對鍵值設為undef。也就是說,即使有一個鍵所對應的雜湊值為undef,那這個鍵依然被視為存在(exists)的,這在剛剛解釋exists這個函數的例子中就可以了解了。

5.7 怎麼讓雜湊上手
在Perl中要使用雜湊,有一些重點也許還是應該提醒大家的。首先,Perl對於雜湊的大小限制依然採取了「放任」的態度,也就是以最沒有限制的方式。只要電腦可以容量的大小,Perl都可以接受。因此程式設計師可以有很大的揮灑空間,只是也必須注意避免讓系統因為被Perl佔用太多資源而導致無法正常運作。
另外,使用者可以利用任何的純量值來表示雜湊中的鍵與值。可是在雜湊鍵的部份,Perl會把所有的鍵轉換為字串。所以如果你在不注意的情況下把運算式當成雜湊的鍵,Perl會幫你先進行運算,然後利用運算所得的結果作為雜湊鍵,這樣的情況可能會有出乎意料的結果。當然,如果你使用運算式來作為雜湊的鍵值,那就應該有些準備,因此應該會更小心的注意,而我們也在前面提到了不少例子。
另外,你還會希望知道自己甚麼時候該用雜湊,這就必須依賴你對於雜湊的感覺,最基本的原則還是以雜湊的特性來看,如果你有一個可以辨識的鍵,而且希望藉由這個鍵找到相關連的值,這時候你幾乎就可以放心的使用雜湊了,只不過這裡所謂的值當然不限定單指特定的值,而可能是任何一種純量值,也就是因為這個特性,可以讓我們搭建出複雜的雜湊結構,不過這個部份則是屬於進階的內容,我們就不在這裡解釋。就像我們所舉的例子,你可以利用ip作為每一部主機的辨識,那麼你可以藉由ip找到那部機器的相關資料。
還有一個常常被搞混的問題,也就是雜湊的順序。許多人想當然爾,以為雜湊的順序是依照新增的順序來決定的。其實事實並非如此,雜湊的排列方式並非按照使用者加入的順序,而是Perl會依照內部的演算法找出最佳化的排列。

習題:
1. 將下列資料建立一個雜湊:
John => 1982.1.5
Paul => 1978.11.3
Lee => 1976.3.2
Mary => 1980.6.23
2. 印出1980年以後出生的人跟他們的生日。
3. 新增兩筆資料到雜湊中:
Kayle => 1984.6.12
Ray => 1978.5.29
4. 檢查在不修改程式碼的情況下,能否達成第二題的題目需求

註一:可以利用perldoc perlfunc來查看perl所提供的函數。
註二:其實,如果你在程式裡打開了警告訊息的選項,這樣的指定會讓Perl產生警告訊息:"Odd number of elements in hash assignment"。