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 类与面向对象 →