第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的hasheql?定位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 groups

Default 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。

一次性获取所有的键、值

keysvaluesmaterialize 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 combined

Nested 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,日志不保留敏感原文。

正式节点与章专属证据

  • :第 1 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
  • :第 2 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
  • :第 3 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
  • :第 4 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
  • :第 5 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
  • :第 6 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
  • :第 7 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
  • :第 8 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。
  • :第 9 个正式节点要能回到“每个键的规范化、哈希相等、默认对象和写回时机都可观察。”,并说明故障发生前后的第一处差异。

对象与状态模型

从输入、接收者到可观察证据

怎样证明散列的键相等、默认值、更新与合并没有制造隐藏共享?

输入与接收者

固定散列的创建所需的原始值、Ruby 版本和调用入口。

状态变化

在执行前记录接收者身份,并声明Hash.new的允许状态。

观察证据

保存第15章 散列类的初值、参数、编码或资源位置。

正式节点:散列的创建、Hash.new、值的获取与设定、散列的默认值、键或值的存在检查、散列的大小、删除键值与初始化散列、合并两个散列、计算单词数量

控制与消息轨迹

在相同初值下定位首个分岔

  1. 01固定散列的创建的输入和接收者
  2. 02执行Hash.new并记录状态
  3. 03观察值的获取与设定的返回或副作用
  4. 04用计算单词数量核对不变量并复位

运行不变量:每个键的规范化、哈希相等、默认对象和写回时机都可观察。

边界故障探针

一次只破坏一个前提

基线满足:每个键的规范化、哈希相等、默认对象和写回时机都可观察。

本章回顾:Key稳定、Missing明确、Merge有来源

  1. Hash key依赖hash/eql?,选择stable immutable keys;外部String/Symbol在边界统一schema。
  2. Static default共享且不写回,per-key mutable default用block显式写入。
  3. []适合optional,fetch适合required;key presence、nil/false value和value search不同。
  4. Delete/clear/merge的mutation、alias、default和conflict policy要明确,nested merge不是自动的。
  5. 词频Hash只是pipeline一环,encoding/tokenization/normalization/tie-break决定可复现结果。

练习与答案

练习

  1. 问题 1:建立正常轨迹。 回答“怎样证明散列的键相等、默认值、更新与合并没有制造隐藏共享?”,并写出四步执行记录。
  1. 问题 2:注入单一故障。 只制造“使用 Hash.new([]) 后直接修改默认数组却没有为键写回独立对象”,应从哪里开始定位?
  1. 问题 3:覆盖正式节点。 用一个证据包串联散列的创建、Hash.new、值的获取与设定、散列的默认值、键或值的存在检查、散列的大小、删除键值与初始化散列、合并两个散列、计算单词数量,说明为什么结论可由另一位读者独立复核。

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

散列的创建

“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

Hash.new

“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

值的获取与设定

“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

散列的默认值

“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

键或值的存在检查

“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

散列的大小

“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

删除键值与初始化散列

“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

合并两个散列

“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

计算单词数量

“第15章 散列类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

资料与写作方式声明

本章以Ruby 基础教程第 5 版作者支持资料合法公开试读核定可见范围,并以目录限定未公开部分,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…