第4章 对象、变量和常量
从对象身份与类关系出发,比较局部、实例、类和全局变量的owner,掌握常量、保留字、多重赋值、交换与数组解构。
学习目标
- 能解释对象与类在“第4章 对象、变量和常量”中的责任边界
- 能围绕“怎样证明变量名、对象身份、作用域与常量查找没有被混为一谈?”运行正常与故障轨迹并定位首个分岔
- 能用“第4章 对象、变量和常量的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”证明“每次读取都能指出名字所属作用域、当前对象和允许的重新绑定规则。”
来源、版次与运行边界
“第4章 对象、变量和常量”以作者维护的第 5 版支持页核定 2016 年 3 月 12 日首刷、两位作者、松本行弘监修以及四部分 23 章目录;逐章程序清单、练习答案和勘误只作为公开支持材料,不被冒充为原书全文。
对“第4章 对象、变量和常量”而言,中文解释、示例、交互、练习和答案均为独立教学重写;站内中文章名是与官方 23 章顺序对应的课程映射,不宣称是日文小节的逐字翻译,也不从公开程序清单复制整段实现。
“第4章 对象、变量和常量”固定在 Ruby 2.3 语境;Ruby 2.3.0 官方文档与稳定版发布说明用于核对当时可用的语言和标准库行为。现代 Ruby 的差异只能另列迁移说明,不能静默改变本页示例的版本结论。
围绕“怎样证明变量名、对象身份、作用域与常量查找没有被混为一谈?”,本页验收“第4章 对象、变量和常量的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”。先预测正常轨迹,再只注入“把赋值理解为深复制,修改别名后仍期待原对象不变”;若无法定位第一处状态分岔,就拒绝当前解释。
从“变量装着值”这个误解开始
在Ruby里,变量更像贴在对象引用上的名字,而不是内嵌对象的盒子。执行copy = original不会自动复制String、Array或自定义对象;两个名字可能指向同一object,任何原地修改都能从两边观察到。是理解参数传递、容器元素与缓存副作用的起点。
先预测:a = "ruby"; b = a; b.upcase!之后a是什么?它也看到"RUBY";若写b = b.upcase,则b改指向新String,a仍指向原对象。前者是mutation,后者包含新对象与rebinding。
对象
对象具有identity、可观察state和class,并通过method响应消息。object_id可辅助一次进程内的identity实验,equal?判断是否同一对象;==由class定义value equality;eql?与hash共同影响Hash key。要求调用者根据用途选方法。
first = "ruby"
same = first
equal_value = "ruby"
p first.equal?(same) # true
p first.equal?(equal_value) # false
p first == equal_value # true
p first.eql?(equal_value) # true for these Stringsfreeze阻止某些对象的后续修改,但不是递归deep freeze:冻结Array不会自动冻结内部String。把可变对象当Hash key后再改变参与hash/eql?的state,会让lookup失效;key应使用稳定immutable value,或在修改后谨慎rehash。对象边界不只看能否调用方法,还要看谁拥有mutation权。
类
Class描述实例如何构造以及method lookup从何开始。value.class返回Class对象,Class本身也是class;is_a?沿ancestor chain判断实例是否属于某class/module,通常比精确比较value.class == Expected更支持subclass与mixin。
class Counter
def initialize(start)
@value = start
end
def increment
@value += 1
end
end
counter = Counter.new(10)
p counter.increment
p Counter.ancestorsRuby方法调用依赖行为而非必须先通过class检查。若对象只需响应each或write,直接依赖这个protocol并在边界测试;过度使用is_a?常把开放的message model收窄成硬编码type switch。第8章会展开继承、module、mixin和duck typing。
变量:名字前缀说明owner
Ruby没有变量类型声明,但变量类别通过名字形态决定。比“前缀记忆表”更重要。
局部变量如total由lexical scope决定;parser看到赋值后才把名字识别为local,否则无receiver调用total可能被解析为method call。Instance variable如@total属于某个object,未初始化读取通常得到nil。Class variable如@@total在inheritance hierarchy共享,容易让一个subclass影响另一个。Global variable如$total全进程可见,是隐藏依赖。
局部变量、实例变量、类变量与全局变量
局部变量适合一次method/block activation内的中间状态;instance variable保存object长期state,应只通过维持不变量的方法修改。Class variable应谨慎使用:如果目标是“每个class自己拥有一个值”,更常用class object的instance variable @count,避免ancestor共享规则。Global state优先封装进显式object并注入调用者。
class Sequence
@created = 0
class << self
attr_reader :created
def build
@created += 1
new(@created)
end
end
def initialize(number)
@number = number
end
end这里Sequence作为object拥有@created,每个instance拥有自己的@number。相同拼写的instance variable附着在不同receiver上,不共享。并发访问@created += 1仍需要同步;选择class-instance variable只解决owner,不自动保证atomicity。
常量
以大写字母开头的名字通常是constant,按lexical nesting与ancestor规则解析。Constant assignment后重新赋值会warning,但Ruby并不因此冻结引用对象:OPTIONS = []后仍可OPTIONS << :fast。要求需要immutable data时显式freeze并处理nested对象。
Constant适合class/module、不会改变的configuration schema和真正固定值;runtime可变配置不应伪装成constant。Namespace使用module/class嵌套,引用外层constant时明确Outer::NAME。Autoload与framework loader可能增加解析规则,核心Ruby实验先用普通require固定来源。
保留字
class、module、def、if、unless、case、while、do、end、begin、rescue、ensure、yield、self、nil、true和false等是parser保留字,不能作为普通local/method定义名称。保留字与内置method不同:puts可被重新定义或以receiver限定,if属于grammar。
Dynamic Hash key或外部字段可包含"class",但转成keyword、method或constant前必须有mapping。代码生成器要先处理keyword collision,不能只做字符替换。静态syntax check能发现grammar冲突,却不能证明生成的API语义合理。
多重赋值
多重赋值先求值右侧,再把结果分配给左侧目标,因此可安全交换变量。解释了swap、解构和方法多返回值。
left = "L"
right = "R"
left, right = right, left
head, *middle, tail = [10, 20, 30, 40]
p left: left, right: right
p head: head, middle: middle, tail: tail左侧位置多于值时用nil补齐;值多于无splat的目标时多余值被丢弃。这个便利会掩盖input shape变化,外部data应先检查length/schema,再解构。Splat总是收集Array;只有一个目标但希望拆开Array时,需要逗号或splat表达解构意图。
交换变量的值
a, b = b, a不会让第二个assignment读到已修改的a,因为右侧在分配前完成。若右侧method有side effect,仍按从左到右求值;不要把复杂I/O和状态修改藏进一个看似简单的swap表达式。
获取数组的元素
first, second = values取前两项;first, *rest保留剩余项;first, = values只取第一项;nested pattern可拆嵌套Array。解构返回的是元素引用,不是deep copy,修改nested mutable元素仍可能影响原数组。
正式节点与章专属证据
- ↡在“第4章 对象、变量和常量”中,对象必须连接输入、状态变化与可复核结果。 :第 1 个正式节点要能回到“每次读取都能指出名字所属作用域、当前对象和允许的重新绑定规则。”,并说明故障发生前后的第一处差异。
- ↡在“第4章 对象、变量和常量”中,类必须连接输入、状态变化与可复核结果。 :第 2 个正式节点要能回到“每次读取都能指出名字所属作用域、当前对象和允许的重新绑定规则。”,并说明故障发生前后的第一处差异。
- ↡在“第4章 对象、变量和常量”中,局部变量、实例变量、类变量与全局变量必须连接输入、状态变化与可复核结果。 :第 3 个正式节点要能回到“每次读取都能指出名字所属作用域、当前对象和允许的重新绑定规则。”,并说明故障发生前后的第一处差异。
- ↡在“第4章 对象、变量和常量”中,常量必须连接输入、状态变化与可复核结果。 :第 4 个正式节点要能回到“每次读取都能指出名字所属作用域、当前对象和允许的重新绑定规则。”,并说明故障发生前后的第一处差异。
- ↡在“第4章 对象、变量和常量”中,保留字必须连接输入、状态变化与可复核结果。 :第 5 个正式节点要能回到“每次读取都能指出名字所属作用域、当前对象和允许的重新绑定规则。”,并说明故障发生前后的第一处差异。
- ↡在“第4章 对象、变量和常量”中,多重赋值必须连接输入、状态变化与可复核结果。 :第 6 个正式节点要能回到“每次读取都能指出名字所属作用域、当前对象和允许的重新绑定规则。”,并说明故障发生前后的第一处差异。
- ↡在“第4章 对象、变量和常量”中,交换变量的值必须连接输入、状态变化与可复核结果。 :第 7 个正式节点要能回到“每次读取都能指出名字所属作用域、当前对象和允许的重新绑定规则。”,并说明故障发生前后的第一处差异。
- ↡在“第4章 对象、变量和常量”中,获取数组的元素必须连接输入、状态变化与可复核结果。 :第 8 个正式节点要能回到“每次读取都能指出名字所属作用域、当前对象和允许的重新绑定规则。”,并说明故障发生前后的第一处差异。
对象与状态模型
从输入、接收者到可观察证据
怎样证明变量名、对象身份、作用域与常量查找没有被混为一谈?
输入与接收者
固定对象所需的原始值、Ruby 版本和调用入口。
状态变化
在执行前记录接收者身份,并声明类的允许状态。
观察证据
保存第4章 对象、变量和常量的初值、参数、编码或资源位置。
正式节点:对象、类、局部变量、实例变量、类变量与全局变量、常量、保留字、多重赋值、交换变量的值、获取数组的元素
控制与消息轨迹
在相同初值下定位首个分岔
- 01固定对象的输入和接收者
- 02执行类并记录状态
- 03观察局部变量、实例变量、类变量与全局变量的返回或副作用
- 04用获取数组的元素核对不变量并复位
运行不变量:每次读取都能指出名字所属作用域、当前对象和允许的重新绑定规则。
边界故障探针
一次只破坏一个前提
本章回顾:先找对象,再找名字owner
- Assignment复制reference;mutation、rebinding与copy是不同操作,identity/value/hash equality也不同。
- Class规定实例构造与method lookup,但Ruby接口常依赖可响应的行为而非精确class。
- Local、instance、class和global变量扩大不同scope;优先选择最窄owner并显式注入共享依赖。
- Constant约束名称绑定意图,不自动冻结对象;保留字属于grammar而非普通内置method。
- Multiple assignment先求值右侧再分配,便于swap与destructure,也可能静默补nil或丢多余值。
练习与答案
练习
- 问题 1:建立正常轨迹。 回答“怎样证明变量名、对象身份、作用域与常量查找没有被混为一谈?”,并写出四步执行记录。
- 问题 2:注入单一故障。 只制造“把赋值理解为深复制,修改别名后仍期待原对象不变”,应从哪里开始定位?
- 问题 3:覆盖正式节点。 用一个证据包串联对象、类、局部变量、实例变量、类变量与全局变量、常量、保留字、多重赋值、交换变量的值、获取数组的元素,说明为什么结论可由另一位读者独立复核。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 对象
“第4章 对象、变量和常量”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 类
“第4章 对象、变量和常量”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 局部变量、实例变量、类变量与全局变量
“第4章 对象、变量和常量”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 常量
“第4章 对象、变量和常量”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 保留字
“第4章 对象、变量和常量”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 多重赋值
“第4章 对象、变量和常量”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 交换变量的值
“第4章 对象、变量和常量”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 获取数组的元素
“第4章 对象、变量和常量”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。