第15章 散列类
系统掌握Hash创建、默认值、fetch/presence、keys/values、size、删除/clear、merge冲突与词频应用,控制key稳定性和schema。
学习目标
- 能解释散列的创建与Hash.new在“第15章 散列类”中的责任边界
- 能围绕“怎样证明散列的键相等、默认值、更新与合并没有制造隐藏共享?”运行正常与故障轨迹并定位首个分岔
- 能用“第15章 散列类的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”证明“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”
来源、版次与运行边界
“第15章 散列类”以作者维护的第 5 版支持页核定 2016 年 3 月 12 日首刷、两位作者、松本行弘监修以及四部分 23 章目录;逐章程序清单、练习答案和勘误只作为公开支持材料,不被冒充为原书全文。
对“第15章 散列类”而言,中文解释、示例、交互、练习和答案均为独立教学重写;站内中文章名是与官方 23 章顺序对应的课程映射,不宣称是日文小节的逐字翻译,也不从公开程序清单复制整段实现。
“第15章 散列类”固定在 Ruby 2.3 语境;Ruby 2.3.0 官方文档与稳定版发布说明用于核对当时可用的语言和标准库行为。现代 Ruby 的差异只能另列迁移说明,不能静默改变本页示例的版本结论。
围绕“怎样证明散列的键相等、默认值、更新与合并没有制造隐藏共享?”,本页验收“第15章 散列类的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”。先预测正常轨迹,再只注入“使用 Hash.new([]) 后直接修改默认数组却没有为键写回独立对象”;若无法定位第一处状态分岔,就拒绝当前解释。
从“Hash只是按名字取值”开始深化
Hash通过key的hash与eql?定位value;key type、equality、mutation和missing default共同决定正确性。不能由一组随意Symbol/String键替代。
先预测:把mutable String作为key后原地修改,再用修改后的String lookup会怎样?Stored hash bucket可能基于旧hash,lookup失败;需要stable immutable key,或非常谨慎地rehash。Key应表达稳定identity。
复习散列
Hash保存key-value pairs,key唯一,赋同key替换value。现代Ruby保留insertion order;本书Ruby 2.3也有相应行为,但业务排序仍应显式。Assignment复制Hash reference,dup只复制outer Hash,keys/values对象继续共享。
Symbol与String是不同keys。JSON通常产生String keys,Ruby options常用Symbols;在parser boundary normalize一次并allowlist,深层代码不应到处同时尝试两种。防止schema漂移。
散列的创建:{}与Hash.new
Literal { :name => "Ruby" }可用任意key;{ name: "Ruby" }是Symbol-key shorthand。Hash.new(default)设置static default但不会为missing key写entry;mutable default会被所有missing reads共享。
record = {
id: 42,
name: "Ruby",
active: false,
}
p record[:name]
p record["name"] # nil: different key散列的默认值
Default value适合immutable 0等;每key mutable collection用block并写回:Hash.new { |hash, key| hash[key] = [] }。必须测试。
groups = Hash.new { |hash, key| hash[key] = [] }
groups[:language] << "Ruby"
groups[:language] << "Lua"
groups[:database] << "SQLite"
p groupsDefault block不应执行昂贵/有副作用的隐式I/O;普通read会触发。Cache loading用明确method并考虑concurrency,Hash default block不提供atomic check-create。
值的获取与设定
hash[key]按default policy宽松读取,fetch(key)对missing抛KeyError,或接收显式default/block。避免远处nil error。
config = { retries: 0, enabled: false }
retries = config.fetch(:retries)
enabled = config.fetch(:enabled)
timeout = config.fetch(:timeout, 5)
p retries: retries, enabled: enabled, timeout: timeout不要用config[:enabled] || true,它覆盖false;fetch同时保留false/0/nil(若key存在)。Assignment hash[key] = value返回value,store等价形式;设置nil不会像Lua那样删除key,presence仍true。
一次性获取所有的键、值
keys和valuesmaterialize Arrays,each_key/each_valuestream traversal;values_at(*keys)按顺序取多个values并对missing使用default。Large Hash若只遍历,不要先创建keys Array。
键或值的存在检查:查看指定对象是否为散列的键或值
key?/has_key?按key semantics判断presence,平均lookup快速;value?/has_value?通常线性扫描。要求根据问题选API。
查看散列的大小
size/length返回stored pairs数量,不含仅被读取但未写回的static default;empty?判断无entries。防止把size == 3当schema validation。
Validate record应逐required key fetch并拒绝unknown keys,再验证values。Counting cache size还不等于memory size;keys/values retained graph、default proc capture和overhead需profile。
删除键值与初始化散列
delete(key)移除并返回value,missing可yield key给block;若stored value为nil,仅返回值无法区分是否删除,先key?或使用sentinel。delete_if/keep_if按predicate原地过滤,reject返回new Hash。
clear移除所有pairs但保留同一Hash object及default/default_proc;所有aliases看到empty Hash。若希望其它references保留旧snapshot,应rebind owner到new Hash而非clear。
settings = Hash.new(0)
settings[:a] = 1
same = settings
settings.clear
p same.empty? # true
p same.default # 0在cache/config reload中尤其重要。
合并两个散列
merge返回new Hash,merge!修改receiver;默认冲突由other/new value胜出,block可接key, old, new决定。不能只写“后者覆盖”。
defaults = { timeout: 5, tags: ["base"] }
override = { timeout: 10, tags: ["local"] }
combined = defaults.merge(override) do |key, old_value, new_value|
key == :tags ? old_value + new_value : new_value
end
p combinedNested Hash merge不是自动deep merge;shallow merge会整体替换nested value。Configuration layer应使用schema-aware merger并保留每个field source,测试explicit nil/false、unknown key和Array merge policy。
应用示例:计算单词数量
词频核心是Hash.new(0)加一,但正确性取决于decode、normalization、tokenization、case与punctuation。使结果可复现。
counts = Hash.new(0)
ARGF.each_line do |line|
line.downcase.scan(/[[:alpha:]]+/) do |word|
counts[word] += 1
end
end
counts
.sort_by { |word, count| [-count, word] }
.first(20)
.each { |word, count| puts format("%7d %s", count, word) }Pattern对Unicode language coverage需验证;downcase/normalization受版本支持。ARGF合并ARGV files/stdin,生产工具要处理path error和encoding。Sort用count descending、word ascending tie-breaker,避免Hash insertion order让同频输出随input变化。
Large vocabulary消耗memory,可stream partition、external sort或approximate sketch;先记录max tokens/unique words。Untrusted超长token要限制length,日志不保留敏感原文。
正式节点与章专属证据
- ↡在“第15章 散列类”中,散列的创建必须连接输入、状态变化与可复核结果。 :第 1 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
- ↡在“第15章 散列类”中,Hash.new必须连接输入、状态变化与可复核结果。 :第 2 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
- ↡在“第15章 散列类”中,值的获取与设定必须连接输入、状态变化与可复核结果。 :第 3 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
- ↡在“第15章 散列类”中,散列的默认值必须连接输入、状态变化与可复核结果。 :第 4 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
- ↡在“第15章 散列类”中,键或值的存在检查必须连接输入、状态变化与可复核结果。 :第 5 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
- ↡在“第15章 散列类”中,散列的大小必须连接输入、状态变化与可复核结果。 :第 6 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
- ↡在“第15章 散列类”中,删除键值与初始化散列必须连接输入、状态变化与可复核结果。 :第 7 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
- ↡在“第15章 散列类”中,合并两个散列必须连接输入、状态变化与可复核结果。 :第 8 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
- ↡在“第15章 散列类”中,计算单词数量必须连接输入、状态变化与可复核结果。 :第 9 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
对象与状态模型
从输入、接收者到可观察证据
怎样证明散列的键相等、默认值、更新与合并没有制造隐藏共享?
输入与接收者
固定散列的创建所需的原始值、Ruby 版本和调用入口。
状态变化
在执行前记录接收者身份,并声明Hash.new的允许状态。
观察证据
保存第15章 散列类的初值、参数、编码或资源位置。
正式节点:散列的创建、Hash.new、值的获取与设定、散列的默认值、键或值的存在检查、散列的大小、删除键值与初始化散列、合并两个散列、计算单词数量
控制与消息轨迹
在相同初值下定位首个分岔
- 01固定散列的创建的输入和接收者
- 02执行Hash.new并记录状态
- 03观察值的获取与设定的返回或副作用
- 04用计算单词数量核对不变量并复位
运行不变量:每个键的规范化、哈希相等、默认对象和写回时机都可观察。
边界故障探针
一次只破坏一个前提
本章回顾:Key稳定、Missing明确、Merge有来源
- Hash key依赖hash/eql?,选择stable immutable keys;外部String/Symbol在边界统一schema。
- Static default共享且不写回,per-key mutable default用block显式写入。
- []适合optional,fetch适合required;key presence、nil/false value和value search不同。
- Delete/clear/merge的mutation、alias、default和conflict policy要明确,nested merge不是自动的。
- 词频Hash只是pipeline一环,encoding/tokenization/normalization/tie-break决定可复现结果。
练习与答案
练习
- 问题 1:建立正常轨迹。 回答“怎样证明散列的键相等、默认值、更新与合并没有制造隐藏共享?”,并写出四步执行记录。
- 问题 2:注入单一故障。 只制造“使用 Hash.new([]) 后直接修改默认数组却没有为键写回独立对象”,应从哪里开始定位?
- 问题 3:覆盖正式节点。 用一个证据包串联散列的创建、Hash.new、值的获取与设定、散列的默认值、键或值的存在检查、散列的大小、删除键值与初始化散列、合并两个散列、计算单词数量,说明为什么结论可由另一位读者独立复核。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 散列的创建
“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- Hash.new
“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 值的获取与设定
“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 散列的默认值
“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 键或值的存在检查
“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 散列的大小
“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 删除键值与初始化散列
“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 合并两个散列
“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 计算单词数量
“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。