# **第 15 章 散列類**
在本章將詳細介紹散列(Hash)類。
-
**復習散列**
簡略地介紹散列的相關用法。
-
**散列的創建方法**
介紹如何創建散列。
-
**獲取、設定鍵值**
介紹批量獲取鍵值的方法。
-
**條件判斷**
介紹判斷鍵值是否在散列中存在的方法。
-
**查看大小**
介紹查看散列大小的方法。
-
**初始化**
比較散列的初始化與新創建有何異同。
-
**使用例子**
以計算單詞數量的程序為例,介紹散列的用法。
### **15.1 復習散列**
在復習散列前,我們再次回顧一下數組的用法。
通過索引可以獲取數組元素或對其賦值。
~~~
person = Array.new
person[0] = "田中一郎"
person[1] = "佐藤次郎"
person[2] = "木村三郎"
p person[1] #=> "佐藤次郎"
~~~
散列與數組一樣,都是表示對象集合的對象。數組通過索引訪問對象內的元素,而散列則是利用鍵。索引只能是數值,而鍵則可以是任意對象。通過使用鍵,散列就可以實現對元素的訪問與賦值。
~~~
person = Hash.new
person["tanaka"] = "田中一郎"
person["satou"] = "佐藤次郎"
person["kimura"] = "木村三郎"
p person["satou"] #=> "佐藤次郎"
~~~
在本例中,`tanaka`、`satou` 等字符串就是鍵,對應的值為 `"田中一郎 "`、`"佐藤次郎 "`。散列中 `[]` 的用法也與數組非常相似。
### **15.2 散列的創建**
與數組一樣,創建散列的方法也有很多。其中下面兩種是最常用到的。
### **15.2.1 使用 {}**
使用字面量直接創建散列。
**{ 鍵 => 值}**
像下面那樣指定鍵值對,鍵值對之間用逗號(`,`)隔開。
~~~
h1 = {"a"=>"b", "c"=>"d"}
p h1["a"] #=> "b"
~~~
另外,用符號作為鍵時,
**{ 鍵: 值}**
也可以采用上述定義方法。
~~~
h2 = {a: "b", c: "d"}
p h2 #=> {:a=>"b", :c=>"d"}
~~~
### **15.2.2 使用 Hash.new**
Hash.new 是用來創建新的散列的方法。若指定參數,則該參數值為散列的默認值,也就是指定不存在的鍵時所返回的值。沒指定參數時,散列的默認值為 nil。
~~~
h1 = Hash.new
h2 = Hash.new("")
p h1["not_key"] #=> nil
p h2["not_key"] #=> ""
~~~
散列的鍵可以使用各種對象,不過一般建議使用下面的對象作為散列的鍵。
-
**字符串(`String`)**
-
**數值(`Numeric`)**
-
**符號(`Symbol`)**
-
**日期(`Date`)**
更多詳細內容請參考專欄《關于散列的鍵》。
### **15.3 值的獲取與設定**
與數組一樣,散列也是用 `[]` 來實現與鍵相對應的元素值的獲取與設定的。
~~~
h = Hash.new
h["R"] = "Ruby"
p h["R"] #=> "Ruby"
~~~
另外,我們還可以用 `store` 方法設定值,用 `fetch` 方法獲取值。下面的例子的執行結果與上面的例子是一樣的。
~~~
h = Hash.new
h.store("R", "Ruby")
p h.fetch("R") #=> "Ruby"
~~~
使用 `fetch` 方法時,有一點與 `[]` 不一樣,就是如果散列中不存在指定的鍵,程序就會發生異常。
~~~
h = Hash.new
p h.fetch("N") #=> 錯誤(IndexError)
~~~
如果對 `fetch` 方法指定第 2 個參數,那么該參數值就會作為鍵不存在時散列的默認值。
~~~
h = Hash.new
h.store("R", "Ruby")
p h.fetch("R", "(undef)") #=> "Ruby"
p h.fetch("N", "(undef)") #=> "(undef)"
~~~
此外,`fetch` 方法還可以使用塊,此時塊的執行結果為散列的默認值。
~~~
h = Hash.new
p h.fetch("N"){ String.new } #=> ""
~~~
### **15.3.1 一次性獲取所有的鍵、值**
我們可以一次性獲取散列的鍵、值。由于散列是鍵值對形式的數據類型,因此獲取鍵、值的方法是分開的。此外,我們還可以選擇是逐個獲取,還是以數組的形式一次性獲取散列的所有鍵、值,不過這兩種情況下使用的方法是不同的(表 15.1)。
**表 15.1 獲取散列的鍵與值的方法**
<table border="1" data-line-num="127 128 129 130 131 132" width="90%"><thead><tr><th> <p class="表頭單元格">?</p> </th> <th> <p class="表頭單元格">數組形式</p> </th> <th> <p class="表頭單元格">迭代器形式</p> </th> </tr></thead><tbody><tr><td> <p class="表格單元格">獲取鍵</p> </td> <td> <p class="表格單元格"><code>keys</code></p> </td> <td> <p class="表格單元格"><code>each_key{| 鍵 | ......}</code></p> </td> </tr><tr><td> <p class="表格單元格">獲取值</p> </td> <td> <p class="表格單元格"><code>values</code></p> </td> <td> <p class="表格單元格"><code>each_value{| 值 | ......}</code></p> </td> </tr><tr><td> <p class="表格單元格">獲取鍵值對[ 鍵, 值]</p> </td> <td> <p class="表格單元格"><code>to_a</code></p> </td> <td> <p class="表格單元格"><code>each{| 鍵 , 值 | ......}</code><br/><code>each{| 數組 | ......}</code></p> </td> </tr></tbody></table>
`keys` 與 `values` 方法各返回封裝為數組后的散列的鍵與值。`to_a` 方法則會先按下面的形式把鍵值對封裝為數組,
**[ 鍵, 值]**
然后再將所有這些鍵值對數組封裝為一個大數組返回。
~~~
h = {"a"=>"b", "c"=>"d"}
p h.keys #=> ["a", "c"]
p h.values #=> ["b", "d"]
p h.to_a #=> [["a", "b"], ["c", "d"]]
~~~
除了返回數組外,我們還可以使用迭代器獲取散列值。
使用 `each_key` 方法與 `each_value` 方法可以逐個獲取并處理鍵、值。使用 `each` 方法還可以得到 [ 鍵 , 值 ] 這樣的鍵值對數組。
關于迭代器的例子請參考 15.8 節。
無論是使用 `each` 方法按順序訪問散列元素,還是使用 `to_a` 方法來獲取全部的散列元素,這兩種情況下都是可以按照散列鍵的設定順序來獲取元素的。
### **15.3.2 散列的默認值**
下面我們來討論一下散列的默認值(即指定散列中不存在的鍵時的返回值)。在獲取散列值時,即使指定了不存在的鍵,程序也會返回某個值,而且不會因此而出錯。我們有 3 種方法來指 定這種情況下的返回值。
-
**1. 創建散列時指定默認值**
`Hash.new` 的參數值即為散列的默認值(什么都不指定時默認值為 `nil`)。
~~~
h = Hash.new(1)
h["a"] = 10
p h["a"] #=> 10
p h["x"] #=> 1
p h["y"] #=> 1
~~~
這個方法與初始化數組一樣,所有的鍵都共享這個默認值。
-
**2. 通過塊指定默認值**
當希望不同的鍵采用不同的默認值時,或者不希望所有的鍵共享一個默認值時,我們可以使用 `Hash.new` 方法的塊指定散列的默認值。
~~~
h = Hash.new do |hash, key|
hash[key] = key.upcase
end
h["a"] = "b"
p h["a"] #=> "b"
p h["x"] #=> "X"
p h["y"] #=> "Y"
~~~
塊變量 `hash` 與 `key`,分別表示將要創建的散列以及散列當前的鍵。用這樣的方法創建散列后,就只能在需要散列默認值的時候才會執行塊。此外,如果不對散列進行賦值,通過指定相同的鍵也可以執行塊。
-
**3. 用 fetch 方法指定默認值**
最后就是剛才已經介紹過的 `fetch` 方法。當 `Hash.new` 方法指定了默認值或塊時,`fetch` 方法的第 2 個參數指定的默認值的優先級是最高的。
~~~
h = Hash.new do |hash, key|
hash[key] = key.upcase
end
p h.fetch("x", "(undef)") #=> "(undef)"
~~~
### **15.4 查看指定對象是否為散列的鍵或值**
-
***h*.`key?`(*key*)
*h*.`has_key?`(*key*)
*h*.`include?`(*key*)
*h*.`member?`(*key*)**
上面 4 個方法都是查看指定對象是否為散列的鍵的方法,它們的用法和效果都是一樣的。大家可以統一只用某一個,也可以根據不同的情況選擇使用。
散列的鍵中包含指定對象時返回 `true`,否則則返回 `false`。
~~~
h = {"a" => "b", "c" => "d"}
p h.key?("a") #=> true
p h.has_key?("a") #=> true
p h.include?("z") #=> false
p h.member?("z") #=> false
~~~
-
***h*.`value?`(*value*)
*h*.`has_value?`(*value*)**
查看散列的值中是否存在指定對象的方法。這兩個方法只是把 `key?`、`has_key?` 方法中代表鍵的 *key* 部分換成了值 *value*,用法是完全一樣的。
散列的值中有指定對象時返回 `true`,否則則返回 `false`。
~~~
h = {"a"=>"b", "c"=>"d"}
p h.value?("b") #=> true
p h.has_value?("z") #=> false
~~~
### **15.5 查看散列的大小**
-
***h*.`size`
*h*.`length`**
我們可以用 `length` 方法或者 `size` 方法來查看散列的大小,也就是散列鍵的數量。
~~~
h = {"a"=>"b", "c"=>"d"}
p h.length #=> 2
p h.size #=> 2
~~~
-
***h*.`empty?`**
我們可以用 `empty?` 方法來查看散列的大小是否為 0,也就是散列中是否不存在任何鍵。
~~~
h = {"a"=>"b", "c"=>"d"}
p h.empty? #=> false
h2 = Hash.new
p h2.empty? #=> true
~~~
### **15.6 刪除鍵值**
像數組一樣,我們也可以成對地刪除散列中的鍵值。
-
***h*.`delete`(*key*)**
通過鍵刪除用 `delete` 方法。
~~~
h = {"R"=>"Ruby"}
p h["R"] #=> "Ruby"
h.delete("R")
p h["R"] #=> nil
~~~
`delete` 方法也能使用塊。指定塊后,如果不存在鍵,則返回塊的執行結果。
~~~
h = {"R"=>"Ruby"}
p h.delete("P"){|key| "no #{key}."} #=> "no P."
~~~
-
***h*.`delete_if`{|*key, val*| … }
h.`reject!`{|*key, val*| … }**
希望只刪除符合某種條件的鍵值的時候,我們可以使用 `delete_if` 方法。
~~~
h = {"R"=>"Ruby", "P"=>"Perl"}
p h.delete_if{|key, value| key == "P"} #=> {"R"=>"Ruby"}
~~~
另外,雖然 `reject!` 方法的用法與 `delete_if` 方法相同,但當不符合刪除條件時,兩者的返回值卻各異。
`delete_if` 方法會返回的是原來的散列,而 `reject!` 方法則返回的是 `nil`。
~~~
h = {"R"=>"Ruby", "P"=>"Perl"}
p h.delete_if{|key, value| key == "L"}
#=> {"R"=>"Ruby", "P"=>"Perl"}
p h.reject!{|key, value| key == "L"} #=> nil
~~~
### **15.7 初始化散列**
-
***h*.`clear`**
用 `clear` 方法清空使用過的散列。
~~~
h = {"a"=>"b", "c"=>"d"}
h.clear
p h.size #=> 0
~~~
這有點類似于使用下面的方法創建新的散列:
~~~
h = Hash.new
~~~
實際上,如果程序中只有一個地方引用 `h` 的話,兩者的效果是一樣的。不過如果還有其他地方引用 `h` 的話,那效果就不一樣了。我們來對比一下下面兩個例子(圖 15.1)。
~~~
【例 1】
h = {"k1"=>"v1"}
g = h
h.clear
p g #=> {}
~~~
~~~
【例 2】
h = {"k1"=>"v1"}
g = h
h = Hash.new
p g #=> {"k1"=>"v1"}
~~~

**圖 15.1 例 1 與例 2 的不同點**
在例 1 中,`h.clear` 清空了 `h` 引用的散列,因此 `g` 引用的散列也被清空,`g` 與 `h` 還是引用同一個散列對象。
而在例 2 中,程序給 `h` 賦值了新的對象,但 `g` 還是引用原來的散列,也就是說,`g` 與 `h` 分別引用不同的散列對象。
需要注意的是,這里方法操作的不是變量,而是變量引用的對象。
### **處理有兩個鍵的散列**
散列的值也可以是散列,也就是所謂的“散列的散列”,這與數組中的“數組的數組”的用法是一樣的。
~~~
table = {"A"=>{"a"=>"x", "b"=>"y"},
"B"=>{"a"=>"v", "b"=>"w"} }
p table["A"]["a"] #=> "x"
p table["B"]["a"] #=> "v"
~~~
在本例中,名為 `table` 的散列的值也是散列。因此,這里使用了 `["A"]["a"]` 這種兩個鍵并列的形式來獲取值。
### **15.8 應用示例:計算單詞數量**
下面我們用散列寫個簡單的小程序。在代碼清單 15.1 中,程序會統計指定文件中的單詞數量,并按出現次數由多到少的順序將其顯示出來。
**代碼清單 15.1 word_count.rb**
~~~
1: # 計算單詞數量
2: count = Hash.new(0)
3:
4: ## 統計單詞
5: File.open(ARGV[0]) do |f|
6: f.each_line do |line|
7: words = line.split
8: words.each do |word|
9: count[word] += 1
10: end
11: end
12: end
13:
14: ## 輸出結果
15: count.sort{|a, b|
16: a[1] <=> b[1]
17: }.each do |key, value|
18: print "#{key}: #{value}\n"
19: end
~~~
首先,在程序第 2 行創建記錄單詞出現次數的散列 `count`。`count` 的鍵表示單詞,值表示該單詞出現的次數。如果鍵不存在,那么值應該為 0,因此將 `count` 的默認值設為 0。
在程序第 6 行到第 11 行的循環處理中,讀取指定的文件,并以單詞為單位分割文件,然后再統計各單詞的數量。
在程序第 6 行,使用 `each_line` 方法讀取每行數據,并賦值給變量 `line`。接下來,在程序第 7 行,使用 `split` 方法分割變量 `line`,將其轉換為以單詞為單位的數組,然后賦值給變量 `words`。
在程序第 8 行的循環處理中,對 `words` 使用 `each` 方法,逐個取出數組中的單詞,然后將各單詞作為鍵,從 `count` 中獲取對應的出現次數,并做 +1 處理。
在程序第 15 行的循環處理中,輸出統計完畢的出現次數。然后,在程序第 15 行到第 17 行,使用 `sort` 方法的塊將單詞按出現次數進行排序。
這里有兩個關鍵點。一是使用了 `<=>` 運算符進行排序,另外一點是比較對象使用了數組的第 2 個元素,如 `a[1]`、`b[1]`。
`<=>` 運算符會比較左右兩邊的對象,檢查判斷它們的關系是 `<`、`=`、還是 `>`。`<` 時結果為負數,`=` 時結果為 0,`>` 時結果為正數。另外,之所以使用 `a[1]` 這樣的數組,是因為用 `sort` 方法獲取 `count` 的對象時,各個值會被作為數組提取出來,如下所示:
**[ 單詞, 出現次數]**
這樣一來,`a[0]` 就表示單詞本身,`a[1]` 才表示出現次數。因此,通過比較 `a[1]` 與 `b[1]`,就能實現按出現次數排序。
在程序第 17 行,`each` 方法會將排序后的散列元素逐個取出,然后再在程序第 18 行輸出該單詞與出現次數。
以上就是整個程序的執行流程,下面就讓我們來實際執行一下這個程序,統計 Ruby 的 `README` 文件中各單詞出現的次數。
> **執行示例**
~~~
> ruby word_count.rb README
=: 1
What's: 1
end:: 1
rdoc: 1
┊
you: 10
of: 11
Ruby: 1
and: 13
to: 22
the: 23
*: 25
~~~
根據這個結果我們可以看出,除符號之外,出現最多的單詞是“`the`”,總共出現了 23 次。
> **專欄**
> **關于散列的鍵**
> 下面我們來討論一下用數值或者自己定義的類等對象作為散列的鍵時需要注意的地方。在下面的例子中,我們首先嘗試創建一個以數值為鍵的散列。
~~~
h = Hash.new
n1 = 1
n2 = 1.0
p n1==n2 #=> true
h[n1] = "exists."
p h[n1] #=> "exists."
p h[n2] #=> nil
~~~
> 用 `n1` 可以獲取以 `n1` 為鍵保存的值,但是用與 `n1` 有相同的值的 `n2` 卻無法獲取。這是由于使用 `n2` 時,無法在散列中找到與之對應的值,因此就返回了默認值 `nil`。
> 在散列內部,程序會將散列獲取值時指定的鍵,與將值保存到散列時指定的鍵做比較,判斷兩者是否一致。這時,判斷鍵是否一致與鍵本身有著莫大的關系。具體來說,對于兩個鍵 `key1`、`key2`,當 `key1.hash` 與 `key2.hash` 得到的整數值相同,且 `key1.eql?(key2)` 為 `true` 的時候,就會認為這兩個鍵是一致的。
> 像本例那樣,雖然使用 `==` 比較時得到的結果是一致的,但是,當兩個鍵分別屬于 `Fixnum` 類和 `Float` 類的對象時,由于不同類的對象不能判斷為相同的鍵,因此就會產生與期待不同的結果。
### **練習題**
1. 定義散列 `wday`,內容為星期的英文表達與中文表達的對應關系。
~~~
p wday[:sunday] #=> "星期天"
p wday[:monday] #=> "星期一"
p wday[:saturday] #=> "星期六"
~~~
2. 使用散列的方法,統計 1 的散列 `wday` 中鍵值對的數量。
3. 使用 `each` 方法和 1 的散列 `wday`,輸出下面的字符串:
~~~
“sunday”是星期天。
“monday”是星期一。
┊
~~~
4. 散列沒有像數組的 `%w` 這樣的語法。因此,請定義方法 `str2hash`,將被空格、制表符、換行符隔開的字符串轉換為散列。
~~~
p str2hash("blue 藍色 white 白色\nred 紅色")
#=> {"blue"=>"藍色", "white"=>"白色", "red"=>"紅色"}
~~~
> 參考答案:請到圖靈社區本書的“隨書下載”處下載([http://www.ituring.com.cn/book/1237](http://www.ituring.com.cn/book/1237))。
- 推薦序
- 譯者序
- 前言
- 本書的讀者對象
- 第 1 部分 Ruby 初體驗
- 第 1 章 Ruby 初探
- 第 2 章 便利的對象
- 第 3 章 創建命令
- 第 2 部分 Ruby 的基礎
- 第 4 章 對象、變量和常量
- 第 5 章 條件判斷
- 第 6 章 循環
- 第 7 章 方法
- 第 8 章 類和模塊
- 第 9 章 運算符
- 第 10 章 錯誤處理與異常
- 第 11 章 塊
- 第 3 部分 Ruby 的類
- 第 12 章 數值類
- 第 13 章 數組類
- 第 14 章 字符串類
- 第 15 章 散列類
- 第 16 章 正則表達式類
- 第 17 章 IO 類
- 第 18 章 File 類與 Dir 類
- 第 19 章 Encoding 類
- 第 20 章 Time 類與 Date 類
- 第 21 章 Proc 類
- 第 4 部分 動手制作工具
- 第 22 章 文本處理
- 第 23 章 檢索郵政編碼
- 附錄
- 附錄 A Ruby 運行環境的構建
- 附錄 B Ruby 參考集
- 后記
- 謝辭