第2章 便利的对象
用Array组织有序元素,用Symbol与Hash表达键值数据,再以Regexp完成第一次结构化文本匹配,建立创建、访问、修改和遍历的边界。
从单个对象转向数据形状开始
第一章的变量一次引用一个对象;真实程序还要表达“按顺序的一批对象”“由名字查值的记录”和“满足某种文本语法的片段”。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,头部操作在大数组上会移动元素,频繁队列操作应评估专门结构。
数组的大小
length与size返回元素槽数量,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的hash与eql?行为必须稳定。决定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/options。regexp.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/gsub与scan。
正则表达式还需要资源边界。由多个可回溯分支与宽泛重复组成的pattern,可能在恶意长输入上消耗异常时间;先限制输入长度,尽量使用结构清楚的pattern,并用失败路径的长样本做benchmark。Regexp适合局部文本语法,不适合替代完整HTML、CSV或编程语言parser。匹配成功后仍要执行类型转换与业务验证,例如日期字段通过数字pattern并不保证月份在1到12之间。
调试匹配时不要只打印true/false:记录pattern版本、输入长度、match起止位置和named captures,但对密码、token等敏感输入只保留经过脱敏的metadata。这样既能证明match evidence boundary,也避免诊断日志成为数据泄漏源。
本章回顾:顺序、键与模式
- Array表达有序mutable集合;
[]容忍越界,fetch显式处理缺失,初始化mutable元素要用block。 - 数组循环优先按意图选择
each、map、select或reduce,遍历期间保持结构稳定。 - Symbol表达稳定内部名称,String表达文本;二者作为Hash key不会自动互换。
- Hash使用
key?和fetch区分missing、nil与default,并把业务排序写出来。 - Regexp匹配返回evidence;只有完整anchor、capture与后续转换共同满足,才能宣称输入有效。