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

Factory命名from_lines比重载new的隐式branch更明确;返回Report还是subclass、是否copy input和解析异常都属于contract。

函数式方法

原书所称函数式方法是省略receiver调用的方法,常见putsprintsleep等来自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
end

Ruby 2.3支持monotonic clock;ensure内错误可能遮蔽原异常,cleanup/observability代码要尽量不失败。Block中的breakreturn与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更能定位实际实现。

正式节点与章专属证据

  • :第 1 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
  • :第 2 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
  • :第 3 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
  • :第 4 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
  • :第 5 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
  • :第 6 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
  • :第 7 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。
  • :第 8 个正式节点要能回到“调用点匹配唯一意图,所有返回路径满足参数和结果约束。”,并说明故障发生前后的第一处差异。

对象与状态模型

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

怎样把接收者、参数、块、返回值与方法可见性写成可检查契约?

输入与接收者

固定简单的方法调用所需的原始值、Ruby 版本和调用入口。

状态变化

在执行前记录接收者身份,并声明带块的方法调用的允许状态。

观察证据

保存第7章 方法的初值、参数、编码或资源位置。

正式节点:简单的方法调用、带块的方法调用、运算符形式的方法调用、实例方法、类方法与函数式方法、方法的返回值、定义带块的方法、参数个数不确定的方法、关键字参数

控制与消息轨迹

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

  1. 01固定简单的方法调用的输入和接收者
  2. 02执行带块的方法调用并记录状态
  3. 03观察运算符形式的方法调用的返回或副作用
  4. 04用关键字参数核对不变量并复位

运行不变量:调用点匹配唯一意图,所有返回路径满足参数和结果约束。

边界故障探针

一次只破坏一个前提

基线满足:调用点匹配唯一意图,所有返回路径满足参数和结果约束。

本章回顾:方法是receiver上的可执行契约

  1. 每次调用都有receiver;无receiver写法使用self,局部变量解析和visibility会改变结果。
  2. Simple、block与operator call共享dispatch基础,但block ownership和parser precedence必须明确。
  3. Instance/class/functional/singleton方法的区别落在receiver与state owner。
  4. Return shape、exception、mutation、yield次数和cleanup共同构成method result contract。
  5. Required/optional/rest/keyword参数表达不同shape;越宽泛的signature越需要边界验证。

练习与答案

练习

  1. 问题 1:建立正常轨迹。 回答“怎样把接收者、参数、块、返回值与方法可见性写成可检查契约?”,并写出四步执行记录。
  1. 问题 2:注入单一故障。 只制造“省略接收者和括号后误判参数边界,调用了非预期方法”,应从哪里开始定位?
  1. 问题 3:覆盖正式节点。 用一个证据包串联简单的方法调用、带块的方法调用、运算符形式的方法调用、实例方法、类方法与函数式方法、方法的返回值、定义带块的方法、参数个数不确定的方法、关键字参数,说明为什么结论可由另一位读者独立复核。

名词解释

名词解释

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

简单的方法调用

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

带块的方法调用

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

运算符形式的方法调用

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

实例方法、类方法与函数式方法

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

方法的返回值

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

定义带块的方法

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

参数个数不确定的方法

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

关键字参数

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

资料与写作方式声明

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

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

讨论

评论区加载中…