第9章 运算符
理解赋值、逻辑、条件、范围与优先级的求值顺序,并以可替换语义定义二元、一元和下标运算符。
学习目标
- 能解释赋值运算符与逻辑运算符的应用在“第9章 运算符”中的责任边界
- 能围绕“怎样从解析优先级、接收者分派与返回类型解释运算符表达式?”运行正常与故障轨迹并定位首个分岔
- 能用“第9章 运算符的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”证明“加括号后的语法树、方法调用和中间值与最终结果一致。”
来源、版次与运行边界
“第9章 运算符”以作者维护的第 5 版支持页核定 2016 年 3 月 12 日首刷、两位作者、松本行弘监修以及四部分 23 章目录;逐章程序清单、练习答案和勘误只作为公开支持材料,不被冒充为原书全文。
对“第9章 运算符”而言,中文解释、示例、交互、练习和答案均为独立教学重写;站内中文章名是与官方 23 章顺序对应的课程映射,不宣称是日文小节的逐字翻译,也不从公开程序清单复制整段实现。
“第9章 运算符”固定在 Ruby 2.3 语境;Ruby 2.3.0 官方文档与稳定版发布说明用于核对当时可用的语言和标准库行为。现代 Ruby 的差异只能另列迁移说明,不能静默改变本页示例的版本结论。
围绕“怎样从解析优先级、接收者分派与返回类型解释运算符表达式?”,本页验收“第9章 运算符的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”。先预测正常轨迹,再只注入“凭数学直觉读取优先级,忽略运算符实际派发到接收者方法”;若无法定位第一处状态分岔,就拒绝当前解释。
从“运算符只是特殊符号”开始纠正
Ruby许多运算符最终会向左侧receiver发送method message,但parser先按固定grammar决定token、arity、precedence与assignment target。解释了为什么重定义+不能改变*优先级,也不能创造新operator token。
先预测:2 + 3 * 4会先调用哪个method?Parser分成2 + (3 * 4),因此先对3调用*,再对2调用+。Debug复杂表达式时先画syntax tree,再追method owner。
赋值运算符
name = value返回被赋值value;object.attr = value调用setter,items[key] = value调用[]=,但expression结果通常仍表现为right-hand value。Compound assignment如x += y大体执行read、operator、write,不等于receiver有一个+= method。
提醒我们counter += 1不自动atomic。Getter和setter有I/O或dynamic behavior时,obj.value += 1可能调用它们各一次并暴露中间失败;并发shared state需lock/atomic abstraction。
class Meter
attr_reader :value
def initialize(value = 0)
self.value = value
end
def value=(candidate)
number = Integer(candidate)
raise ArgumentError, "negative" if number < 0
@value = number
end
endSetter方法名以=结尾,内部使用self.value =以免被解析为local assignment。Assignment嵌入condition容易把=误写成==;启用warning并拆成两行。Parallel assignment和splat规则已在第4章说明。
逻辑运算符的应用
&&和|| short-circuit并返回operand;and/or也short-circuit,但precedence很低。value ||= default大体只在value为nil或false时写default,因此不能保留合法false;memoization若计算结果可false,每次都会重算。
def enabled(configured, default)
configured.nil? ? default : configured
end
@loaded = true
@cached_value = load_value unless @loaded第二种写法用独立loaded flag保存false/nil result。避免把truthiness当presence。
逻辑chain适合无副作用predicate;authorization、charge、write等重要动作使用显式if,记录拒绝原因与异常。not与!、and与&&优先级不同,项目内保持一致风格。
条件运算符
condition ? when_true : when_false是expression,适合两个短小、同shape、无复杂side effect的分支。限制嵌套。
label = active? ? "active" : "inactive"Nested ternary、包含assignment/rescue或跨行复杂调用改成if/elsif。Colon还用于Symbol/Hash/keyword syntax,空格与格式化必须让parser角色清楚。
范围运算符
start..finish包含终点,start...finish排除终点。Range可用于include?/cover?、case和iteration;cover?更偏边界比较,include?在某些类型/版本可能枚举或使用离散membership,performance与语义需测试。
inclusive = 1..5
exclusive = 1...5
p inclusive.to_a # [1, 2, 3, 4, 5]
p exclusive.to_a # [1, 2, 3, 4]
p((200..299).cover?(204))对时间窗口尤其重要:相邻窗口常用半开区间[start, finish)避免重复计数。Ruby 2.3对beginless/endless Range尚不支持,教程示例保持双端明确。
运算符的优先级
常见顺序包括一元、幂、乘除、加减、shift、比较、equality、逻辑、range、ternary、assignment等,但完整规则应查目标版本。不是可由operator method改变的行为。
result = work and audit分组近似(result = work) and audit,而result = work && audit近似result = (work && audit)。两者都可能合法却含义不同。Business condition用&&/||加括号;and/or只在团队明确约定的control-flow idiom中使用。
代码审查时把“看起来熟悉”改成“写出唯一分组”,尤其关注一元负号与幂、range与condition、rescue modifier和block。目标不是背表,而是让下一位维护者无需猜测。
# 明确授权policy,不要求读者背优先级
allowed = authenticated? && (admin? || owns?(resource))
# 明确block owner
log(items.map { |item| normalize(item) })Brace block通常比do/end绑定更紧,嵌套无括号调用最易出错。Format工具能统一surface,却不能替你证明block交给哪个method;拆变量并写behavior test。
定义运算符
自定义operator不是为了让DSL更“像自然语言”,而是当domain已有熟悉、稳定且可组合的运算语义时提供简洁surface。防止+暗中修改receiver或执行网络请求。
定义二元运算符
class Vector2
attr_reader :x, :y
def initialize(x, y)
@x = Float(x)
@y = Float(y)
freeze
end
def +(other)
self.class.new(x + other.x, y + other.y)
end
def ==(other)
other.is_a?(self.class) && x == other.x && y == other.y
end
end+返回新immutable Vector2,不修改operands。测试identity element、commutativity/associativity只在浮点误差允许的语义范围内;unsupported type应抛清楚错误或实现coerce protocol,不能静默返回nil。
定义一元运算符
Unary plus/minus method名为+@和-@,!也可定义但改变truth intuition风险高。Vector可定义-@返回坐标取反的新value。Unary operator无显式operand,仍从receiver读取state。
下标方法
[](key)与[]=(key, value)让object表现为indexed/keyed collection。必须与用户对Array/Hash的直觉一致或清楚记录差异。
class Palette
def initialize(colors)
@colors = colors.dup
end
def [](index)
@colors.fetch(index)
end
def []=(index, color)
raise ArgumentError, "empty color" if color.to_s.empty?
@colors[index] = color
end
end这里read用fetch令缺失显式,write验证不变量。若支持Range、negative index或Symbol key,逐项加入tests;不要接受任意key后在深层method碰运气。
正式节点与章专属证据
- ↡在“第9章 运算符”中,赋值运算符必须连接输入、状态变化与可复核结果。 :第 1 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
- ↡在“第9章 运算符”中,逻辑运算符的应用必须连接输入、状态变化与可复核结果。 :第 2 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
- ↡在“第9章 运算符”中,条件运算符必须连接输入、状态变化与可复核结果。 :第 3 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
- ↡在“第9章 运算符”中,范围运算符必须连接输入、状态变化与可复核结果。 :第 4 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
- ↡在“第9章 运算符”中,运算符的优先级必须连接输入、状态变化与可复核结果。 :第 5 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
- ↡在“第9章 运算符”中,定义二元运算符必须连接输入、状态变化与可复核结果。 :第 6 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
- ↡在“第9章 运算符”中,定义一元运算符必须连接输入、状态变化与可复核结果。 :第 7 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
- ↡在“第9章 运算符”中,下标方法必须连接输入、状态变化与可复核结果。 :第 8 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
对象与状态模型
从输入、接收者到可观察证据
怎样从解析优先级、接收者分派与返回类型解释运算符表达式?
输入与接收者
固定赋值运算符所需的原始值、Ruby 版本和调用入口。
状态变化
在执行前记录接收者身份,并声明逻辑运算符的应用的允许状态。
观察证据
保存第9章 运算符的初值、参数、编码或资源位置。
正式节点:赋值运算符、逻辑运算符的应用、条件运算符、范围运算符、运算符的优先级、定义二元运算符、定义一元运算符、下标方法
控制与消息轨迹
在相同初值下定位首个分岔
- 01固定赋值运算符的输入和接收者
- 02执行逻辑运算符的应用并记录状态
- 03观察条件运算符的返回或副作用
- 04用下标方法核对不变量并复位
运行不变量:加括号后的语法树、方法调用和中间值与最终结果一致。
边界故障探针
一次只破坏一个前提
本章回顾:先按grammar分组,再按receiver dispatch
- Assignment和compound assignment可能触发getter/operator/setter,read-modify-write不自动atomic。
- Logical operators short-circuit并返回operand;
||=不能区分未计算与缓存false/nil。 - Ternary只用于短小同shape分支,Range明确inclusive/exclusive endpoint与membership语义。
- Precedence和block binding由parser固定,复杂policy用括号和中间变量展示expression tree。
- 自定义binary/unary/index operator须保持熟悉的代数与容器契约,不隐藏重副作用。
练习与答案
练习
- 问题 1:建立正常轨迹。 回答“怎样从解析优先级、接收者分派与返回类型解释运算符表达式?”,并写出四步执行记录。
- 问题 2:注入单一故障。 只制造“凭数学直觉读取优先级,忽略运算符实际派发到接收者方法”,应从哪里开始定位?
- 问题 3:覆盖正式节点。 用一个证据包串联赋值运算符、逻辑运算符的应用、条件运算符、范围运算符、运算符的优先级、定义二元运算符、定义一元运算符、下标方法,说明为什么结论可由另一位读者独立复核。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 赋值运算符
“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 逻辑运算符的应用
“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 条件运算符
“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 范围运算符
“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 运算符的优先级
“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 定义二元运算符
“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 定义一元运算符
“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 下标方法
“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。