第2章 便利的对象

用Array组织有序元素,用Symbol与Hash表达键值数据,再以Regexp完成第一次结构化文本匹配,建立创建、访问、修改和遍历的边界。

学习目标

  • 能解释数组的创建与数组元素的读取与保存在“第2章 便利的对象”中的责任边界
  • 能围绕“怎样从数组、符号、散列和正则的对象形状预测一次读取或更新?”运行正常与故障轨迹并定位首个分岔
  • 能用“第2章 便利的对象的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”证明“容器身份、键语义、默认值和匹配范围在操作前后都明确可查。”

来源、版次与运行边界

“第2章 便利的对象”以作者维护的第 5 版支持页核定 2016 年 3 月 12 日首刷、两位作者、松本行弘监修以及四部分 23 章目录;逐章程序清单练习答案勘误只作为公开支持材料,不被冒充为原书全文。

对“第2章 便利的对象”而言,中文解释、示例、交互、练习和答案均为独立教学重写;站内中文章名是与官方 23 章顺序对应的课程映射,不宣称是日文小节的逐字翻译,也不从公开程序清单复制整段实现。

“第2章 便利的对象”固定在 Ruby 2.3 语境;Ruby 2.3.0 官方文档稳定版发布说明用于核对当时可用的语言和标准库行为。现代 Ruby 的差异只能另列迁移说明,不能静默改变本页示例的版本结论。

围绕“怎样从数组、符号、散列和正则的对象形状预测一次读取或更新?”,本页验收“第2章 便利的对象的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”。先预测正常轨迹,再只注入“让多个键或数组位置意外共享同一个可变默认对象”;若无法定位第一处状态分岔,就拒绝当前解释。

从单个对象转向数据形状开始

第一章的变量一次引用一个对象;真实程序还要表达“按顺序的一批对象”“由名字查值的记录”和“满足某种文本语法的片段”。Array、Hash和Regexp分别承担这三类责任。要求选择容器时先问顺序、唯一性、key类型、missing行为和mutation owner,而不是只看写法短不短。

先预测:["a", "b"][8]会抛异常还是返回nil?普通[]返回nil;fetch(8)才抛IndexError。便利API常把错误变成absence,因此边界代码要主动选择“容忍缺失”还是“缺失即错误”。

数组的创建

数组用[...]创建,可保存不同class的对象,但业务集合最好保持可说明的元素契约。空数组写[]Array.new(3, object)会让三个位置引用同一个object,而Array.new(3) { build_value }会为每个位置执行block。这个差异在nested array与mutable string中尤其危险。

languages = ["Ruby", "Lua", "Go"]
matrix = Array.new(2) { Array.new(3, 0) }
 
matrix[0][0] = 9
p languages: languages
p matrix: matrix # [[9, 0, 0], [0, 0, 0]]

Range、String split和enumerator也可产生数组,但“能转成Array”不代表应立即materialize。数据很大时先保持iterator,只有需要随机访问、重复遍历或固定snapshot时才落成数组。

元素引用、dup与浅复制

Array保存的是对象引用。copy = original.dup会复制外层数组,使追加、删除或替换某个槽位不再影响original;但两边对应槽位仍可能指向同一个mutable对象。若original[0]是String,执行copy[0].upcase!仍会通过共享String反映到original。不能自动提供隔离。

所谓deep copy必须先定义对象graph中哪些节点归谁、遇到cycle怎么办、IO或Proc等不可复制资源如何处理。通用序列化往返有类型、性能与安全限制,不应作为默认答案。更可靠的做法是让value object不可变、在业务边界显式重建需要复制的结构,并为共享identity写测试。

方法接收Array时还要声明mutation contract:是只读消费、返回新Array,还是有意修改caller拥有的集合。Ruby不会靠类型签名替你说明;方法名的!也只是惯例,并不等于所有原地方法都带bang。测试同时断言返回值和输入是否改变。

数组元素的读取与保存

索引从0开始,负索引从末尾反向计算;array[index]缺失返回nil,array.fetch(index)缺失默认抛错,也可显式给default或block。帮助区分“该位置可选”和“数据结构已损坏”。

queue = ["compile", "test"]
 
p queue[0]             # "compile"
p queue[-1]            # "test"
p queue[99]            # nil
p queue.fetch(99, "idle")
 
queue[1] = "package"
queue << "deploy"
p queue

给超出尾部的正索引赋值会用nil填充中间位置,形成稀疏数组;多数队列、列表和表格都不应依赖这种隐式扩容。追加用<</push,尾部删除用pop,头部操作在大数组上会移动元素,频繁队列操作应评估专门结构。

数组的大小

lengthsize返回元素槽数量,empty?表达是否为空。不要用if array.length判断非空,因为Ruby中的0也truthy;写unless array.empty?或业务predicate。元素可能包含nil,因此size不等于“有效数据条目数”。

数组的循环

each按当前顺序把元素交给block,返回receiver本身;each_with_index同时给元素和index;map构造变换后的新数组;select筛选;reduce聚合。第一次接触时要把“side effect遍历”和“产生新值的变换”分开,不能所有需求都写成each加外部mutable accumulator。

prices = [120, 80, 250]
 
prices.each_with_index do |price, index|
  puts "#{index}: #{price}"
end
 
taxed = prices.map { |price| (price * 1.1).round }
expensive = taxed.select { |price| price >= 150 }
p taxed: taxed, expensive: expensive

遍历期间追加、删除或重排同一数组,会让哪些元素被访问变得难以推理。建议先select出目标,再统一修改;若确需原地删除,使用语义明确的delete_if并测试相邻元素。

什么是符号

Symbol literal如:status:ready是不可变、name-like的值,常用于Hash key、method name和API option。提醒我们,Symbol不是“更快的String”这种泛化替代品。

:status"status"内容看似相近,却是不同对象和不同Hash key;JSON解析通常产生String key,keyword arguments与Ruby内部options常使用Symbol key。边界处应只做一次显式转换,并限制允许的key集合。不要把任意用户输入无条件转Symbol来掩盖schema缺失。

散列的创建与使用

Hash用key定位value。{ status: "ready" }{ :status => "ready" }的简写;任意对象都可作key,但key的hasheql?行为必须稳定。决定lookup是否可靠。

job = {
  id: 42,
  status: "ready",
  retries: 0,
}
 
p job[:status]
p job.fetch(:owner, "unassigned")
job[:status] = "running"
job[:tags] = ["ruby", "cli"]

hash[key]缺失通常返回nil,无法区分“没有key”和“key存在但value为nil”;用key?检查presence,或用fetch把缺失变成异常/default。Hash.new([])有与Array shared default类似的陷阱:读取不存在key得到同一个default对象,却不会自动写回。计数器可用Hash.new(0),集合default用block并显式写入。

Hash默认值工厂与写回

需要按key自动创建独立集合时,使用Hash.new { |hash, key| hash[key] = [] }。Block收到正在查询的Hash和missing key;只有显式hash[key] = ...才会把创建值保存。若block只返回[]而不写回,groups[:a] << item修改的是一次性对象,下一次读取仍然为空。

Default block不应执行网络请求、随机逻辑或难以观察的side effect,因为普通lookup会偷偷触发它。把昂贵加载写成明确的cache API,并区分“key不存在”“加载失败”“合法空值”。多线程环境还要单独解决check-then-create race;Hash default block本身不是同步机制。

合并Hash时,merge返回新Hash,merge!修改receiver;冲突block能决定旧值与新值如何组合。配置叠加不能简单让“后者覆盖前者”而不记录provenance,尤其是nil、false和空字符串都可能代表不同业务意图。为每层配置保留来源并在最终schema处校验,能让错误落在输入边界。

散列的循环

each do |key, value|逐对遍历;只要key用each_key,只要value用each_value。现代Ruby Hash保留插入顺序,但这不等于业务排序;报告需要按key、value或其它字段排序时应显式调用sort_by

正则表达式

Regexp literal写成/pattern/optionsregexp.match(string)成功返回MatchData,失败返回nil;=~返回起始index或nil。防止把“找到一段数字”误当成“输入是合法整数”。

line = "user=alice score=42"
pattern = /\Auser=(?<name>[a-z]+) score=(?<score>\d+)\z/
 
match = pattern.match(line)
if match
  p name: match[:name], score: Integer(match[:score], 10)
else
  warn "invalid record"
end

\A\z锚定整个字符串,^$更偏向行边界;named capture比依赖数字位置更易维护。Dynamic text进入pattern时先Regexp.escape,否则.*等字符会变成语法。第16章会系统展开重复、最短匹配、option、capture、sub/gsubscan

正则表达式还需要资源边界。由多个可回溯分支与宽泛重复组成的pattern,可能在恶意长输入上消耗异常时间;先限制输入长度,尽量使用结构清楚的pattern,并用失败路径的长样本做benchmark。Regexp适合局部文本语法,不适合替代完整HTML、CSV或编程语言parser。匹配成功后仍要执行类型转换与业务验证,例如日期字段通过数字pattern并不保证月份在1到12之间。

调试匹配时不要只打印true/false:记录pattern版本、输入长度、match起止位置和named captures,但对密码、token等敏感输入只保留经过脱敏的metadata。这样既能证明match evidence boundary,也避免诊断日志成为数据泄漏源。

正式节点与章专属证据

  • :第 1 个正式节点要能回到“容器身份、键语义、默认值和匹配范围在操作前后都明确可查。”,并说明故障发生前后的第一处差异。
  • :第 2 个正式节点要能回到“容器身份、键语义、默认值和匹配范围在操作前后都明确可查。”,并说明故障发生前后的第一处差异。
  • :第 3 个正式节点要能回到“容器身份、键语义、默认值和匹配范围在操作前后都明确可查。”,并说明故障发生前后的第一处差异。
  • :第 4 个正式节点要能回到“容器身份、键语义、默认值和匹配范围在操作前后都明确可查。”,并说明故障发生前后的第一处差异。
  • :第 5 个正式节点要能回到“容器身份、键语义、默认值和匹配范围在操作前后都明确可查。”,并说明故障发生前后的第一处差异。
  • :第 6 个正式节点要能回到“容器身份、键语义、默认值和匹配范围在操作前后都明确可查。”,并说明故障发生前后的第一处差异。
  • :第 7 个正式节点要能回到“容器身份、键语义、默认值和匹配范围在操作前后都明确可查。”,并说明故障发生前后的第一处差异。

对象与状态模型

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

怎样从数组、符号、散列和正则的对象形状预测一次读取或更新?

输入与接收者

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

状态变化

在执行前记录接收者身份,并声明数组元素的读取与保存的允许状态。

观察证据

保存第2章 便利的对象的初值、参数、编码或资源位置。

正式节点:数组的创建、数组元素的读取与保存、数组的循环、符号、散列的创建与使用、散列的循环、正则表达式

控制与消息轨迹

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

  1. 01固定数组的创建的输入和接收者
  2. 02执行数组元素的读取与保存并记录状态
  3. 03观察数组的循环的返回或副作用
  4. 04用正则表达式核对不变量并复位

运行不变量:容器身份、键语义、默认值和匹配范围在操作前后都明确可查。

边界故障探针

一次只破坏一个前提

基线满足:容器身份、键语义、默认值和匹配范围在操作前后都明确可查。

本章回顾:顺序、键与模式

  1. Array表达有序mutable集合;[]容忍越界,fetch显式处理缺失,初始化mutable元素要用block。
  2. 数组循环优先按意图选择eachmapselectreduce,遍历期间保持结构稳定。
  3. Symbol表达稳定内部名称,String表达文本;二者作为Hash key不会自动互换。
  4. Hash使用key?fetch区分missing、nil与default,并把业务排序写出来。
  5. Regexp匹配返回evidence;只有完整anchor、capture与后续转换共同满足,才能宣称输入有效。

练习与答案

练习

  1. 问题 1:建立正常轨迹。 回答“怎样从数组、符号、散列和正则的对象形状预测一次读取或更新?”,并写出四步执行记录。
  1. 问题 2:注入单一故障。 只制造“让多个键或数组位置意外共享同一个可变默认对象”,应从哪里开始定位?
  1. 问题 3:覆盖正式节点。 用一个证据包串联数组的创建、数组元素的读取与保存、数组的循环、符号、散列的创建与使用、散列的循环、正则表达式,说明为什么结论可由另一位读者独立复核。

名词解释

名词解释

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

数组的创建

“第2章 便利的对象”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

数组元素的读取与保存

“第2章 便利的对象”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

数组的循环

“第2章 便利的对象”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

符号

“第2章 便利的对象”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

散列的创建与使用

“第2章 便利的对象”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

散列的循环

“第2章 便利的对象”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

正则表达式

“第2章 便利的对象”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

资料与写作方式声明

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

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

讨论

评论区加载中…