散列

散列


  • https://www.kancloud.cn/imxieke/ruby-base/107303


散列

通过索引可以获取数组元素或对其赋值。

person = Array.new
person[0] = "田中一郎"
person[1] = "佐藤次郎"
person[2] = "木村三郎"
p person[1] #=> "佐藤次郎"

散列与数组一样,都是表示对象集合的对象。数组通过索引访问对象内的元素,而散列则是利用键。索引只能是数值,而键则可以是任意对象。通过使用键,散列就可以实现对元素的访问与赋值。

person = Hash.new
person["tanaka"] = "田中一郎"
person["satou"] = "佐藤次郎"
person["kimura"] = "木村三郎"
p person["satou"] #=> "佐藤次郎"

在本例中,tanakasatou 等字符串就是键,对应的值为 "田中一郎 ""佐藤次郎 "。散列中 [] 的用法也与数组非常相似。

散列的创建

与数组一样,创建散列的方法也有很多。其中下面两种是最常用到的。

使用 {}

使用字面量直接创建散列。

{ 键 => 值}

像下面那样指定键值对,键值对之间用逗号(,)隔开。

h1 = {"a"=>"b", "c"=>"d"}
p h1["a"]    #=> "b"

另外,用符号作为键时,

{ 键: 值}

也可以采用上述定义方法。

h2 = {a: "b", c: "d"}
p h2    #=> {:a=>"b", :c=>"d"}

使用 Hash.new

Hash.new 是用来创建新的散列的方法。若指定参数,则该参数值为散列的默认值,也就是指定不存在的键时所返回的值。没指定参数时,散列的默认值为 nil

h1 = Hash.new
h2 = Hash.new("")
p h1["not_key"]    #=> nil
p h2["not_key"]    #=> ""

散列的键可以使用各种对象,不过一般建议使用下面的对象作为散列的键。

  • 字符串(String)

  • 数值(Numeric)

  • 符号(Symbol)

  • 日期(Date)

值的获取与设定

与数组一样,散列也是用 [] 来实现与键相对应的元素值的获取与设定的。

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 }    #=> ""

一次性获取所有的键、值

我们可以一次性获取散列的键、值。由于散列是键值对形式的数据类型,因此获取键、值的方法是分开的。此外,我们还可以选择是逐个获取,还是以数组的形式一次性获取散列的所有键、值,不过这两种情况下使用的方法是不同的。

方法 | 数组形式 | 迭代器形式

  • | - | - 获取键 | keys | each_key{| 键 | ......} 获取值 | values | each_value{| 值 | ......} 获取键值对[ 键, 值] | to_a | each{| 键 , 值 | ......}each{| 数组 | ......}

keysvalues 方法各返回封装为数组后的散列的键与值。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 方法还可以得到 [ 键 , 值 ] 这样的键值对数组。

无论是使用 each 方法按顺序访问散列元素,还是使用 to_a 方法来获取全部的散列元素,这两种情况下都是可以按照散列键的设定顺序来获取元素的。

散列的默认值

下面我们来讨论一下散列的默认值(即指定散列中不存在的键时的返回值)。在获取散列值时,即使指定了不存在的键,程序也会返回某个值,而且不会因此而出错。我们有 3 种方法来指定这种情况下的返回值。

  • 创建散列时指定默认值

    Hash.new 的参数值即为散列的默认值(什么都不指定时默认值为 nil)。

    h = Hash.new(1)
    h["a"] = 10
    p h["a"]    #=> 10
    p h["x"]    #=> 1
    p h["y"]    #=> 1

    这个方法与初始化数组一样,所有的键都共享这个默认值。

  • 通过块指定默认值

    当希望不同的键采用不同的默认值时,或者不希望所有的键共享一个默认值时,我们可以使用 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"

    块变量 hashkey,分别表示将要创建的散列以及散列当前的键。用这样的方法创建散列后,就只能在需要散列默认值的时候才会执行块。此外,如果不对散列进行赋值,通过指定相同的键也可以执行块。

  • 用 fetch 方法指定默认值

    最后就是刚才已经介绍过的 fetch 方法。当 Hash.new 方法指定了默认值或块时,fetch 方法的第 2 个参数指定的默认值的优先级是最高的。

    h = Hash.new do |hash, key|
    hash[key] = key.upcase
    end
    p h.fetch("x", "(undef)")    #=> "(undef)"

查看指定对象是否为散列的键或值

  • 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

查看散列的大小

  • 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

删除键值

像数组一样,我们也可以成对地删除散列中的键值。

  • 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

初始化散列

  • h.clear

    clear 方法清空使用过的散列。

    h = {"a"=>"b", "c"=>"d"}
    h.clear
    p h.size    #=> 0

    这有点类似于使用下面的方法创建新的散列:

    h = Hash.new

    实际上,如果程序中只有一个地方引用 h 的话,两者的效果是一样的。不过如果还有其他地方引用 h 的话,那效果就不一样了。我们来对比一下下面两个例子.

    h = {"k1"=>"v1"}
    g = h
    h.clear
    p g  #=> {}
    h = {"k1"=>"v1"}
    g = h
    h = Hash.new
    p g  #=> {"k1"=>"v1"}

    在例 1 中,h.clear 清空了 h 引用的散列,因此 g 引用的散列也被清空,gh 还是引用同一个散列对象。

    而在例 2 中,程序给 h 赋值了新的对象,但 g 还是引用原来的散列,也就是说,gh 分别引用不同的散列对象。

    需要注意的是,这里方法操作的不是变量,而是变量引用的对象。

处理有两个键的散列

散列的值也可以是散列,也就是所谓的“散列的散列”,这与数组中的“数组的数组”的用法是一样的。

table = {"A"=>{"a"=>"x", "b"=>"y"},
         "B"=>{"a"=>"v", "b"=>"w"} }
p table["A"]["a"]  #=> "x"
p table["B"]["a"]  #=> "v"

在本例中,名为 table 的散列的值也是散列。因此,这里使用了 ["A"]["a"] 这种两个键并列的形式来获取值。

应用示例:计算单词数量

下面我们用散列写个简单的小程序。在代码中,程序会统计指定文件中的单词数量,并按出现次数由多到少的顺序将其显示出来。

# 计算单词数量
count = Hash.new(0)

## 统计单词
File.open(ARGV[0]) do |f|
  f.each_line do |line|
    words = line.split
    words.each do |word|
      count[word] += 1
    end
  end
end

## 输出结果
count.sort{|a, b|
  a[1] <=> b[1]
}.each do |key, value|
  print "#{key}: #{value}\n"
end

首先,在程序第 2 行创建记录单词出现次数的散列 countcount 的键表示单词,值表示该单词出现的次数。如果键不存在,那么值应该为 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

在散列内部,程序会将散列获取值时指定的键,与将值保存到散列时指定的键做比较,判断两者是否一致。这时,判断键是否一致与键本身有着莫大的关系。具体来说,对于两个键 key1key2,当 key1.hashkey2.hash 得到的整数值相同,且 key1.eql?(key2) 为 true 的时候,就会认为这两个键是一致的。

像本例那样,虽然使用 == 比较时得到的结果是一致的,但是,当两个键分别属于 Fixnum 类和 Float 类的对象时,由于不同类的对象不能判断为相同的键,因此就会产生与期待不同的结果。