第7章 方法
从receiver与method lookup理解简单、带块和运算符调用,区分实例/类/函数式方法,设计返回值、block、可变参数和关键字参数契约。
学习目标
- 能解释简单的方法调用与带块的方法调用在“第7章 方法”中的责任边界
- 能围绕“怎样把接收者、参数、块、返回值与方法可见性写成可检查契约?”运行正常与故障轨迹并定位首个分岔
- 能用“第7章 方法的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”证明“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”
来源、版次与运行边界
“第7章 方法”以作者维护的第 5 版支持页核定 2016 年 3 月 12 日首刷、两位作者、松本行弘监修以及四部分 23 章目录;逐章程序清单、练习答案和勘误只作为公开支持材料,不被冒充为原书全文。
对“第7章 方法”而言,中文解释、示例、交互、练习和答案均为独立教学重写;站内中文章名是与官方 23 章顺序对应的课程映射,不宣称是日文小节的逐字翻译,也不从公开程序清单复制整段实现。
“第7章 方法”固定在 Ruby 2.3 语境;Ruby 2.3.0 官方文档与稳定版发布说明用于核对当时可用的语言和标准库行为。现代 Ruby 的差异只能另列迁移说明,不能静默改变本页示例的版本结论。
围绕“怎样把接收者、参数、块、返回值与方法可见性写成可检查契约?”,本页验收“第7章 方法的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”。先预测正常轨迹,再只注入“省略接收者和括号后误判参数边界,调用了非预期方法”;若无法定位第一处状态分岔,就拒绝当前解释。
从“函数名”背后的receiver开始
Ruby中的方法调用总有receiver;写object.run时显式,写puts value时receiver是当前self且调用受visibility约束。把调用从孤立名字还原成object interaction。
先预测:方法内局部变量size = 10之后写size,Ruby会调用self.size还是读取local?Parser已看到assignment,会读local。若要调用receiver method可写size()或self.size(private method例外)。名字解析会影响看似相同的无括号调用。
方法的调用
完整调用包含receiver、method name、positional/keyword arguments、可选block以及返回/异常。应能从签名、名称、文档和测试共同确认。
括号通常可省略,但DSL风格不应牺牲边界清晰度。Nested call、多个arguments或条件expression中优先保留括号;setter、operator和index call虽有特殊syntax,本质仍参与method dispatch。
简单的方法调用
name = "ruby"
p name.upcase
p name.start_with?("ru")
p name.public_send(:slice, 0, 2)send可调用包括private在内的方法,public_send尊重public visibility,更适合外部dispatch。Dynamic method name必须来自allowlist;把用户输入直接转Symbol并send会暴露不应调用的API。Normal call让静态搜索、重构和审计更可靠,dynamic dispatch只在确有数据驱动需求时使用。
带块的方法调用
Method可接收一个implicit block,通过yield调用;caller用{}或do...end提供。Brace与do/end的binding precedence不同,和无括号调用组合可能把block交给错误method。复杂chain用括号或分行验证block owner。
result = [1, 2, 3].map do |number|
number * number
end
p result # [1, 4, 9]Block不是普通argument,但可用&block捕获为Proc或把Proc以&转换回block;捕获会有额外语义/成本,应只在需要保存、转发或检查时使用。第11章会系统解释block,第21章展开Proc。
运算符形式的方法调用
left + right大体发送+给left,items[index]调用[],items[index] = value调用[]=。Operator precedence与assignment规则仍由parser固定,不能靠重写method改变。第9章会讨论operator定义与下标方法。
方法的分类:实例方法、类方法与函数式方法
分类的核心是receiver是谁。比“def写在哪里”更能预测行为。
实例方法
Class/module body中def process定义instance method,receiver是某个instance,可读取该对象@variables。Instance method不应偷偷读class/global mutable state;共享依赖通过constructor或method parameter注入,才能独立测试。
类方法
def self.build在当前class object上定义singleton method,常用作factory、parser或namespace operation。Class method访问的@cache属于class object,不是instance,也不是自动被subclass复制。继承class-level configuration前先设计查找和override规则。
class Report
def self.from_lines(lines)
new(lines.map(&:strip))
end
def initialize(rows)
@rows = rows.freeze
end
def size
@rows.size
end
endFactory命名from_lines比重载new的隐式branch更明确;返回Report还是subclass、是否copy input和解析异常都属于contract。
函数式方法
原书所称函数式方法是省略receiver调用的方法,常见puts、print、sleep等来自Kernel并作为Object的private instance methods可用。Private method不能用普通显式receiver(某些setter/语法例外),强调它依赖当前context而非公共消息。
把大量utility塞进Kernel会污染所有objects。项目helper放module并通过module_function、明确receiver或include到有限scope;pure transformation可做module function,但有state的服务优先object。
方法的定义
def name(parameters) ... end创建method;method body不会形成closure捕获definition周围的local variables,这与block/lambda不同。需要外部state时用instance variable、constant或显式dependency,不要假定def能看到外层local。
Signature应只接受需要的shape。宽泛*args, **options, &block方便forwarding,却使错误延迟到深层;公共API先列required参数和允许keywords,unknown keyword应尽早失败。Ruby 2.3的keyword与Hash转换规则和现代Ruby不同,跨版本library必须建立compatibility tests。
方法的返回值
Ruby默认返回最后求值expression,也可用return提前退出。不应依赖“最后一行刚好是什么”。
def normalize_name(value)
return nil if value.nil?
text = String(value).strip
return nil if text.empty?
text.downcase
end这个method返回normalized String或nil;若invalid input应该报错而不是absence,则改为raise并命名。不要同时返回false、nil、String和exception而不说明;使用value object或Result-style结构承载多状态。
定义带块的方法
yield执行caller block;block_given?检查是否提供。Method可在resource setup后yield,并用ensure cleanup,这就是File.open block form的模式。Yield次数、参数、异常传播和method最终返回值必须明确。
def with_timer(clock: Process, output: $stdout)
started = clock.clock_gettime(Process::CLOCK_MONOTONIC)
result = yield
result
ensure
if started
elapsed = clock.clock_gettime(Process::CLOCK_MONOTONIC) - started
output.puts format("%.3fs", elapsed)
end
endRuby 2.3支持monotonic clock;ensure内错误可能遮蔽原异常,cleanup/observability代码要尽量不失败。Block中的break、return与Proc/lambda控制差异将在后续章节细化。
参数个数不确定的方法
Rest parameter *values收集剩余positional arguments为Array;调用处splat展开Array。Rest适合天然variadic domain,如format arguments或sum;若只有两三种结构,显式overload-like branch/value object更易验证。
Required参数可在rest前后出现,分配规则要用目标Ruby验证。Arity错误应保留ArgumentError,不要rescue后猜caller意图。Forwarding还要避免丢block或keywords,跨版本代码尤其需要matrix。
关键字参数
Keyword让布尔和同类型参数可读:render(limit: 20, strict: true)。Required keyword在Ruby 2.3可用name:;default在call time求值。不等于接受任意options Hash。
关于方法调用的一些补充
Ruby 2.3中Hash与keyword arguments之间存在隐式转换,后续版本逐步分离;维护多版本gem时不要只在一个runtime测试。Method visibility分public/protected/private;visibility表达调用边界,不是安全sandbox。Reflection/dynamic send仍可能绕过约定,真正不可信输入要在capability边界隔离。
Method lookup会经过singleton class、prepended modules、class、included modules和superclasses;本章先固定receiver,详细lookup在第8章展开。Debug时记录receiver.class、method(:name).owner和source_location,比只看method name更能定位实际实现。
正式节点与章专属证据
- ↡在“第7章 方法”中,简单的方法调用必须连接输入、状态变化与可复核结果。 :第 1 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
- ↡在“第7章 方法”中,带块的方法调用必须连接输入、状态变化与可复核结果。 :第 2 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
- ↡在“第7章 方法”中,运算符形式的方法调用必须连接输入、状态变化与可复核结果。 :第 3 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
- ↡在“第7章 方法”中,实例方法、类方法与函数式方法必须连接输入、状态变化与可复核结果。 :第 4 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
- ↡在“第7章 方法”中,方法的返回值必须连接输入、状态变化与可复核结果。 :第 5 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
- ↡在“第7章 方法”中,定义带块的方法必须连接输入、状态变化与可复核结果。 :第 6 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
- ↡在“第7章 方法”中,参数个数不确定的方法必须连接输入、状态变化与可复核结果。 :第 7 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
- ↡在“第7章 方法”中,关键字参数必须连接输入、状态变化与可复核结果。 :第 8 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
对象与状态模型
从输入、接收者到可观察证据
怎样把接收者、参数、块、返回值与方法可见性写成可检查契约?
输入与接收者
固定简单的方法调用所需的原始值、Ruby 版本和调用入口。
状态变化
在执行前记录接收者身份,并声明带块的方法调用的允许状态。
观察证据
保存第7章 方法的初值、参数、编码或资源位置。
正式节点:简单的方法调用、带块的方法调用、运算符形式的方法调用、实例方法、类方法与函数式方法、方法的返回值、定义带块的方法、参数个数不确定的方法、关键字参数
控制与消息轨迹
在相同初值下定位首个分岔
- 01固定简单的方法调用的输入和接收者
- 02执行带块的方法调用并记录状态
- 03观察运算符形式的方法调用的返回或副作用
- 04用关键字参数核对不变量并复位
运行不变量:调用点匹配唯一意图,所有返回路径满足参数和结果约束。
边界故障探针
一次只破坏一个前提
本章回顾:方法是receiver上的可执行契约
- 每次调用都有receiver;无receiver写法使用self,局部变量解析和visibility会改变结果。
- Simple、block与operator call共享dispatch基础,但block ownership和parser precedence必须明确。
- Instance/class/functional/singleton方法的区别落在receiver与state owner。
- Return shape、exception、mutation、yield次数和cleanup共同构成method result contract。
- Required/optional/rest/keyword参数表达不同shape;越宽泛的signature越需要边界验证。
练习与答案
练习
- 问题 1:建立正常轨迹。 回答“怎样把接收者、参数、块、返回值与方法可见性写成可检查契约?”,并写出四步执行记录。
- 问题 2:注入单一故障。 只制造“省略接收者和括号后误判参数边界,调用了非预期方法”,应从哪里开始定位?
- 问题 3:覆盖正式节点。 用一个证据包串联简单的方法调用、带块的方法调用、运算符形式的方法调用、实例方法、类方法与函数式方法、方法的返回值、定义带块的方法、参数个数不确定的方法、关键字参数,说明为什么结论可由另一位读者独立复核。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 简单的方法调用
“第7章 方法”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 带块的方法调用
“第7章 方法”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 运算符形式的方法调用
“第7章 方法”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 实例方法、类方法与函数式方法
“第7章 方法”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 方法的返回值
“第7章 方法”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 定义带块的方法
“第7章 方法”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 参数个数不确定的方法
“第7章 方法”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 关键字参数
“第7章 方法”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。