深入 Ruby 04:Array、Hash 与 Range
清单、索引和范围判断分别适合 Array、Hash 与 Range,但容器名称还不足以预测行为。数组里的对象可能共享,Hash 的默认值可能根本没有写进容器,Range 的 cover? 与 include? 也可能得到不同答案。本篇从这些差异解释集合契约。
文章卡
| 项目 | 内容 |
|---|---|
| 先修 | 第 02–03 篇;引用、字符串与数值相等 |
| 实验 | bundle exec ruby labs/04/run.rb |
| 核心问题 | 谁共享对象,键怎样相等,范围如何包含? |
| 验收 | Array 构造、Hash 默认值、数字键、字符串键与区间反例 |
| 工程应用 | 任务顺序用 Array,唯一 ID 用 Hash,优先级用数值 Range |
数组保存引用,不隐含复制
Array.new(2, []) 只求值一次作为默认项的数组,然后把同一个对象放进两个位置。本篇验证:
1 | |
block 形式每次产生新对象,才得到两个独立的空数组。这里不是 Ruby 随意复制失败,而是构造方式明确不同。重复存放不可变值可以复用同一引用,重复存放可变容器时要判断这种共享是否符合需求。Array
任务清单同样是引用图。对最外层 source.dup 只能得到一个新数组:
1 | |
新数组仍指向原来的 Hash,Hash 又指向原来的标签数组。Taskbook 的复制按已知结构进行,不使用 Marshal 或任意递归复制器。未来如果新增字段,需要更新复制策略和别名断言,否则旧验收无法覆盖新引用路径。
缺失元素与合法 nil 分开处理
数组索引不存在通常得到 nil;Hash 下标找不到键也通常得到 nil。若数据本身允许 nil,两种情况就无法靠返回值区分。需要“必须存在”的读取时,使用 fetch 或显式检查存在性,并为缺失定义错误。
Taskbook 的字段不允许随意缺失,因此先检查字段集合,再读取每个值。代码不会用 task[:priority] || 1 让缺失数据悄悄获得默认优先级。默认值适合配置中确实允许省略的项目,不适合掩盖输入格式错误。
Array 保留任务输入顺序,筛选也保留顺序。索引 Hash 用于检测唯一 ID,不代表业务输出顺序应改成数字排序。输出是否排序属于公开行为,加入排序需要调整验收,不能作为内部容器转换的附带效果。
Hash 默认值不是自动插入
这个 Hash 看起来能按键收集数组,实际却共享同一个默认数组:
1 | |
:one 和 :two 都未插入。缺失读取返回同一个默认对象,修改它后,另一个缺失键也看到变化。empty? 为真这一项断言说明“能读到内容”与“键已经存在”不是同一事实。Hash 默认值
需要按键创建并存储容器时,明确赋值:
1 | |
读取 :two 本身也插入了一个空数组。这个 default proc 具有副作用,因此“查询不存在的键”会改变 Hash。对于只读索引,如果不希望这种变化,使用 fetch(key, []) 返回临时默认值或明确查询;不要把默认 proc 当作线程安全的按键初始化方案。
键相等要遵守 eql? 与 hash
数字 1 与 1.0 在 == 下相等,但在 Hash 中可以是两个不同键:
1 | |
因此,把浮点 ID 转成“数值相等的 ID”之前,要先明确领域契约。Taskbook 要求 Integer,不让 1.0 参与整数唯一性判断。若自定义对象成为键,需要使 eql? 相等的对象具有相同 hash,并考虑键进入容器后是否还能改变;第 10 篇给出完整的自定义键实验。
普通 Hash 对非冻结 String 键有特殊处理。lab 将可变字符串 'task' 作为键后再修改原字符串,仍能用旧内容查到记录,新内容查不到。这个行为保护了普通字符串键的内容,却不能外推到所有可变键对象,也不能证明 Hash 中的值被复制。
序列化边界还会改变键类型。JSON 使用字符串键,核心库使用 Symbol 键;转换需要统一并检测允许字段。把两个不同键默默合并,可能令输入值覆盖关系依赖处理顺序。
Range 比较区间与枚举成员
数值优先级可写成 (1..5).cover?(priority),.. 包含末端,... 排除末端。这个表达式适合检查已确认为 Integer 的值,单独使用它并不证明类型合法。
字符串范围能显示 cover? 与 include? 的区别:
1 | |
'cc' 在端点的排序边界之内,但不在这个范围按字符串后继生成的单字符成员中。对非数值范围,区间覆盖与枚举成员可能有不同代价和结果;不能靠方法名称把它们当作同义词。Range
没有末端的整数 Range 可以迭代,但直接 to_a 不能得到有限结果。第 08 篇将它与 Lazy 和 take 组合,并观察实际求值数量。范围能描述无界集合,不等于程序可以一次性把集合放进内存。
选择容器时同时选择约定
Taskbook 的 Array 负责顺序,Hash 负责字段与 ID 索引,Range 负责数值边界。默认对象共享、查询副作用、浅复制与键相等都写进独立 lab。累计应用不必为每个教学机制新增一个容器类,但应在真正会影响行为的位置留下检查。
练习是把任务按标签分组:每个标签对应独立数组,清单中同一个任务可以属于多个标签。先用 Hash.new([]) 写出失败实验,再改为存储默认 proc,检查至少两个不同标签及缺失读取。另一个练习是给整数 ID 和字符串 ID 定义明确的接受规则,确认 '1'、1 与 1.0 不会意外共用唯一性判断。
本篇运行输出 array_alias=[[:changed], [:changed]]、hash_missing=[1] 与 range=[true, false],最终为 PASS 04。这些观测证明固定反例的行为,并未测量容器在大规模任务上的性能。
