Ruby 哈希

哈希(Hash)是 Ruby 里第二常用的集合——键值对结构,适合表示配置、对象属性、计数器等。和数组配在一起,几乎能表达任何结构化数据。Ruby 哈希的特殊之处在于符号(Symbol)键的语法糖,让 name: "Alice" 写起来像 JSON 但又更快。

1. 创建哈希:两种语法

Ruby 哈希有两种键语法——哈希火箭 =>(老语法,任意类型键)和符号键 key:(Ruby 1.9+ 新语法,只能用于符号键):

# 老语法:hash rocket(哈希火箭 =>)
h1 = { "name" => "Alice", "age" => 30 }

# 新语法(Ruby 1.9+):符号键专用
h2 = { name: "Alice", age: 30 }
# 等价于 { :name => "Alice", :age => 30 }

# 两种可以混用,但建议统一
h3 = { name: "Bob", "city" => "NYC" }

# Hash.new
h4 = Hash.new          # {}(默认值 nil)
h5 = Hash.new(0)       # 访问不存在的键返回 0
h6 = Hash.new { |h, k| h[k] = [] }  # 默认值是新数组(每次独立)

# 范围转 hash(Ruby 2.6+)
Hash[key1, val1, key2, val2]   # 交替参数
Hash[[:a, 1], [:b, 2]]         # 数组的数组

实战约定:符号键用新语法(name: "Alice"),字符串键才用火箭("name" => "Alice")。两种风格不要在同一 hash 里混用,显得杂乱。

2. 增删改查

user = { name: "Alice", age: 30, role: "admin" }

# 取值
user[:name]          # "Alice"
user[:email]         # nil(键不存在返回 nil)

# fetch:键不存在时更明确
user.fetch(:name)            # "Alice"
user.fetch(:email, "无")      # "无"(提供默认)
user.fetch(:email) { |k| "#{k} 不存在" }  # 块生成默认

# 赋值
user[:age] = 31
user[:email] = "a@x.com"    # 新增

# 删除
user.delete(:role)           # "admin"(返回被删的值)

# 判断键是否存在
user.key?(:name)             # true
user.has_key?(:name)         # true(别名,旧风格)
user.include?(:name)         # true(又一个别名)

一个常见坑:h[:missing] 返回 nil 而不报错,容易掩盖 bug。需要严格判断时用 fetch——它对缺失键会抛 KeyError,或让你显式提供默认值。

3. 迭代与变换

user = { name: "Alice", age: 30, city: "NYC" }

# each:遍历键值对
user.each { |k, v| puts "#{k}: #{v}" }
# name: Alice
# age: 30
# city: NYC

# each_key / each_value
user.each_key { |k| puts k }
user.each_value { |v| puts v }

# map 成数组(注意:返回数组不是 hash)
user.map { |k, v| "#{k}=#{v}" }    # ["name=Alice", "age=30", ...]

# transform_values:对每个值变换,返回新 hash(Ruby 2.4+)
{a: 1, b: 2}.transform_values { |v| v * 10 }
# {a: 10, b: 20}

# transform_keys(Ruby 2.5+)
{a: 1, b: 2}.transform_keys { |k| k.to_s }
# {"a"=>1, "b"=>2}

注意 map 在 hash 上返回数组而不是 hash(因为 map 永远返回数组)。要把 hash 变换成新 hash,用 transform_values / transform_keys

4. 合并、过滤、转换

h = { a: 1, b: 2 }

# merge:合并(后者覆盖前者)
{ a: 1 }.merge({ b: 2 })       # {a: 1, b: 2}
{ a: 1 }.merge({ a: 99 })      # {a: 99}(后者覆盖)
{ a: 1 }.merge({ a: 99 }) { |k, old, new| old }  # 用块决定冲突时取谁

# invert:键值互换
{a: 1, b: 2}.invert           # {1=>:a, 2=>:b}

# select / reject:过滤成新 hash
{a: 1, b: 2, c: 3}.select { |k, v| v > 1 }  # {b: 2, c: 3}

# to_a:转数组的数组
{a: 1, b: 2}.to_a             # [[:a, 1], [:b, 2]]

# 反向:数组转 hash
[[:a, 1], [:b, 2]].to_h       # {a: 1, b: 2}

merge 是高频方法——配置合并、默认值叠加全靠它。带块的 merge 能精细控制冲突处理,实战里常用来实现"deep merge"。

5. dig:安全取嵌套值

处理嵌套 hash(JSON、API 响应)时,dig救命方法——任何一层缺失都返回 nil,不会抛 NoMethodError:

user = {
  name: "Alice",
  profile: {
    address: {
      city: "NYC",
      zip: "10001"
    }
  }
}

# 传统取嵌套值:层层 []
user[:profile][:address][:city]   # "NYC"
# 风险:任何一层是 nil 就会 NoMethodError

# dig:安全取嵌套值(Ruby 2.3+)
user.dig(:profile, :address, :city)    # "NYC"
user.dig(:profile, :phone, :number)    # nil(中间缺了就返回 nil,不报错)

# 同样适用于数组
data = { users: [{ name: "Bob" }, { name: "Cathy" }] }
data.dig(:users, 0, :name)             # "Bob"
data.dig(:users, 5, :name)             # nil(索引越界也安全)

Rails 里更进一步有 try / dig 组合,但纯 Ruby 场景 dig 已足够。所有 Array、Hash、Struct 都支持 dig

6. 默认值(高频坑)

哈希访问不存在的键时,默认返回 nil,但你可以自定义默认值。这里有三个层次的写法和一个经典坑:

# 默认值的三种方式

# 方式 1:Hash.new(默认值)
counts = Hash.new(0)
"hello world".chars.each { |c| counts[c] += 1 }
# {"h"=>1, "e"=>1, "l"=>3, ...}
# 不存在时返回 0,加 1 后变成 1,自动写入

# 方式 2:块(默认值是可变对象时必须用)
groups = Hash.new { |h, k| h[k] = [] }
%w[apple banana apricot].each { |w| groups[w[0]] << w }
# {"a"=>["apple", "apricot"], "b"=>["banana"]}

# 方式 3:fetch 提供 fallback
cache = {}
cache.fetch(:key) { compute_expensive_thing() }

# 坑:Hash.new(0) 的 0 是共享的,但 0 不可变所以没事
# 坑:Hash.new([]) 的 [] 是共享的!改一个会影响所有
h = Hash.new([])
h[:a] << 1      # 不要这样!所有键都会变成 [1]
# 正确:用块默认值

规则:不可变默认值(数字、字符串)用 Hash.new 传第二参数;可变默认值(数组、hash)必须用块形式调用 Hash.new(块里手动赋值),否则所有键会共享同一个对象——这是新手必踩的坑。

7. 关键字参数与哈希的微妙关系

前面方法章节讲的关键字参数,底层就是哈希。Ruby 2.7/3.0 对此做了改动——不再自动把 hash 当关键字参数传,但符号键 hash 仍可。简单记:**opts 显式表达意图,避免踩 3.0 的 breaking change。

小结

本章覆盖了哈希的核心用法。重点:符号键新语法、fetch 严格取值、dig 安全嵌套、transform_values 变换、默认值用块形式。下一篇进入 Ruby 的面向对象——类,把数据和行为封装起来。

← 上一篇 Ruby 数组

下一篇 Ruby 类与面向对象

✈️💬