第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
end

Setter方法名以=结尾,内部使用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碰运气。

正式节点与章专属证据

  • :第 1 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
  • :第 2 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
  • :第 3 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
  • :第 4 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
  • :第 5 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
  • :第 6 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
  • :第 7 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。
  • :第 8 个正式节点要能回到“加括号后的语法树、方法调用和中间值与最终结果一致。”,并说明故障发生前后的第一处差异。

对象与状态模型

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

怎样从解析优先级、接收者分派与返回类型解释运算符表达式?

输入与接收者

固定赋值运算符所需的原始值、Ruby 版本和调用入口。

状态变化

在执行前记录接收者身份,并声明逻辑运算符的应用的允许状态。

观察证据

保存第9章 运算符的初值、参数、编码或资源位置。

正式节点:赋值运算符、逻辑运算符的应用、条件运算符、范围运算符、运算符的优先级、定义二元运算符、定义一元运算符、下标方法

控制与消息轨迹

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

  1. 01固定赋值运算符的输入和接收者
  2. 02执行逻辑运算符的应用并记录状态
  3. 03观察条件运算符的返回或副作用
  4. 04用下标方法核对不变量并复位

运行不变量:加括号后的语法树、方法调用和中间值与最终结果一致。

边界故障探针

一次只破坏一个前提

基线满足:加括号后的语法树、方法调用和中间值与最终结果一致。

本章回顾:先按grammar分组,再按receiver dispatch

  1. Assignment和compound assignment可能触发getter/operator/setter,read-modify-write不自动atomic。
  2. Logical operators short-circuit并返回operand;||=不能区分未计算与缓存false/nil。
  3. Ternary只用于短小同shape分支,Range明确inclusive/exclusive endpoint与membership语义。
  4. Precedence和block binding由parser固定,复杂policy用括号和中间变量展示expression tree。
  5. 自定义binary/unary/index operator须保持熟悉的代数与容器契约,不隐藏重副作用。

练习与答案

练习

  1. 问题 1:建立正常轨迹。 回答“怎样从解析优先级、接收者分派与返回类型解释运算符表达式?”,并写出四步执行记录。
  1. 问题 2:注入单一故障。 只制造“凭数学直觉读取优先级,忽略运算符实际派发到接收者方法”,应从哪里开始定位?
  1. 问题 3:覆盖正式节点。 用一个证据包串联赋值运算符、逻辑运算符的应用、条件运算符、范围运算符、运算符的优先级、定义二元运算符、定义一元运算符、下标方法,说明为什么结论可由另一位读者独立复核。

名词解释

名词解释

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

赋值运算符

“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

逻辑运算符的应用

“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

条件运算符

“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

范围运算符

“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

运算符的优先级

“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

定义二元运算符

“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

定义一元运算符

“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

下标方法

“第9章 运算符”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

资料与写作方式声明

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

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

讨论

评论区加载中…