第6章 循环
按重复来源选择times、for、while、until、each或loop,掌握block作用域、Enumerator、break/next返回值与可证明终止。
学习目标
- 能解释循环的基础与times方法在“第6章 循环”中的责任边界
- 能围绕“怎样为 times、while、until、each 与 loop 选择可证明终止的驱动方式?”运行正常与故障轨迹并定位首个分岔
- 能用“第6章 循环的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”证明“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”
来源、版次与运行边界
“第6章 循环”以作者维护的第 5 版支持页核定 2016 年 3 月 12 日首刷、两位作者、松本行弘监修以及四部分 23 章目录;逐章程序清单、练习答案和勘误只作为公开支持材料,不被冒充为原书全文。
对“第6章 循环”而言,中文解释、示例、交互、练习和答案均为独立教学重写;站内中文章名是与官方 23 章顺序对应的课程映射,不宣称是日文小节的逐字翻译,也不从公开程序清单复制整段实现。
“第6章 循环”固定在 Ruby 2.3 语境;Ruby 2.3.0 官方文档与稳定版发布说明用于核对当时可用的语言和标准库行为。现代 Ruby 的差异只能另列迁移说明,不能静默改变本页示例的版本结论。
围绕“怎样为 times、while、until、each 与 loop 选择可证明终止的驱动方式?”,本页验收“第6章 循环的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”。先预测正常轨迹,再只注入“在 next 分支跳过状态推进,使循环重复同一个状态”;若无法定位第一处状态分岔,就拒绝当前解释。
从“重复几次”究竟由谁决定开始
循环语法看起来相近,但驱动重复的source不同:Integer决定次数,collection决定元素,condition决定是否继续,外部event决定何时break。决定最清晰的构造。
先预测:处理Array时用while index < items.length与items.each有什么实质差异?前者由caller维护index与边界,后者让Array的each拥有 traversal;除非确实需要随机访问或跨项状态,each更少出现off-by-one和越界。
循环的基础
任何循环都要回答初始状态、每轮读取/修改什么、何时终止、异常时资源如何释放、循环expression返回什么。把“应该会停”变成可审查证据。
Count loop的variant可以是剩余次数;逐行读取的progress来自source消耗一个record;重试循环则需要attempt上限或deadline。若找不到单调progress measure,就必须明确它是service event loop,并提供shutdown signal、backpressure和supervision。
循环时的注意事项
遍历mutable collection时修改同一结构会改变未来元素集合。删除Array元素使用delete_if/reject!等专用API,或先收集操作再提交;不要在each内部按index删除。Hash遍历时增删key同样可能报错或产生难推理行为。
循环体打开file、socket或lock时,cleanup不能只放在正常break之后;使用block-form resource API或ensure。每轮错误是跳过、重试、终止还是累计,必须写成policy,不能笼统rescue StandardError; next。
循环的可观测性也要有上界。逐项打印会把O(n)计算变成I/O瓶颈并淹没有效日志;记录总数、成功/跳过/失败计数、耗时分布和少量脱敏样本。长循环定期检查cancel/deadline,但检查频率本身要纳入cost。Benchmark时使用真实分布,区分iterator dispatch、block工作、allocation与I/O,不能只凭“for更像底层所以更快”下结论。
批处理需要决定partial success语义:中途第501项失败时,是rollback前500项、保留并返回resume token,还是进入dead-letter集合。循环只负责重复,transaction和checkpoint属于更高层protocol;把它们明确传入,避免一个局部next悄悄改变业务一致性。
实现循环的方法
Ruby倾向让对象提供iteration method:Integer有times,Range有each,Array/Hash实现Enumerable协议。选择method时先表达意图,再考虑performance;map表示一对一变换,select表示过滤,reduce表示聚合,普通each表示side effect traversal。
records = %w[ready invalid ready]
summary = records.each_with_object(Hash.new(0)) do |status, counts|
counts[status] += 1
end
p summary外部accumulator可用,但each_with_object把owner和返回值放进调用结构。大量chain会创建intermediate collections时可用lazy Enumerator,不过要理解何时真正求值、错误在哪一步出现以及source能否重复遍历。
times方法
n.times把0到n-1依次yield给block,返回n。n为0或负数时block不执行。适合重试attempt、生成固定元素和小规模实验。
attempts = 3
success = false
attempts.times do |index|
puts "attempt #{index + 1}/#{attempts}"
success = perform_once
break if success
end
raise "all attempts failed" unless success重试还需delay、deadline、可重试异常集合与idempotency;times只提供次数。生产代码不要无差别重试认证失败、参数错误或非幂等写入。测试第1次成功、中间成功、全部失败和执行抛异常。
for语句
for item in items内部使用items.each,但loop variable不会像block parameter那样限制在block scope;循环结束后item仍可能在surrounding scope可见。是两种写法最容易忽略的差异。
item = :before
for item in [:a, :b]
# work
end
p item # :b
item = :before
[:a, :b].each do |entry|
# entry is block-local
end
p item # :beforeRuby社区通常偏好each,因为scope、chain和自定义Enumerable更统一;for在强调传统syntax或简单Range时仍可读。不要仅机械替换,重构时检查外围是否依赖loop variable残留值。
普通的for语句
原书区分Ruby的for与其他语言的初始化/条件/更新三段式for。Ruby没有C式for (i=0; i<n; i++);该需求用n.times、Range each或while。先确定driver,避免把另一语言的surface syntax强搬进Ruby。
while语句
while condition在每轮前检查;若初始falsey,body执行0次。适合状态在body中推进且终止predicate明确的场景。Assignment与condition混写容易触发误读,应先读取/转换,再判断。
index = 0
total = 0
while index < values.length
total += values.fetch(index)
index += 1
end
p total这里invariant是0 <= index <= values.length,variant是values.length - index,每轮减少1。若values在循环中增长,原证明失效;snapshot或固定上界必须写明。
until语句
until done?在condition falsey时执行,相当于while !done?。适合positive终止名称,例如“直到queue为空”;复杂组合或double negative改用while加命名predicate。
Polling until还需sleep/backoff和deadline,否则会busy loop占满CPU。等待外部event优先使用blocking I/O、condition variable或event API;until不是同步原语,也不能避免race。
each方法
each由receiver定义yield什么:Array给元素,Hash通常给key/value,Range给序列值,IO给line。支撑Enumerable的map/select/find等方法。
没有block时许多each返回Enumerator,可后续with_index、lazy或由外部next驱动。Enumerator可能一次性消耗IO,也可能可rewind;API必须说明。不要默认任何enumerable都能求size或重复遍历。
loop方法
loop do ... end表达没有自然precondition的重复,通常内部用break结束。它比while true更Ruby化,也会在没有block时返回Enumerator。Event loop、读取直到EOF和搜索直到找到可用loop,但必须有shutdown/error path。
result = loop do
line = input.gets
break :eof if line.nil?
next if line.strip.empty?
break line if valid?(line)
end
p resultBreak的argument成为loop expression result,因此上例可区分:eof与找到的line。若只靠nil表示两种情况,caller会失去证据。
循环控制:break与next
break退出最近loop/iterator invocation;next结束当前iteration并进入下一轮;redo不重新获取下一项而重做当前iteration;return退出enclosing method。决定cleanup和result。
Nested loops中break只退出最近一层;需要退出多层时提取method并return、使用catch/throw的明确protocol,或重构为find组合。Redo尤其危险,必须有独立attempt/deadline变化,否则同一输入永久重跑。
正式节点与章专属证据
- ↡在“第6章 循环”中,循环的基础必须连接输入、状态变化与可复核结果。 :第 1 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
- ↡在“第6章 循环”中,times方法必须连接输入、状态变化与可复核结果。 :第 2 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
- ↡在“第6章 循环”中,for语句必须连接输入、状态变化与可复核结果。 :第 3 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
- ↡在“第6章 循环”中,while语句必须连接输入、状态变化与可复核结果。 :第 4 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
- ↡在“第6章 循环”中,until语句必须连接输入、状态变化与可复核结果。 :第 5 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
- ↡在“第6章 循环”中,each方法必须连接输入、状态变化与可复核结果。 :第 6 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
- ↡在“第6章 循环”中,loop方法必须连接输入、状态变化与可复核结果。 :第 7 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
- ↡在“第6章 循环”中,break与next必须连接输入、状态变化与可复核结果。 :第 8 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
对象与状态模型
从输入、接收者到可观察证据
怎样为 times、while、until、each 与 loop 选择可证明终止的驱动方式?
输入与接收者
固定循环的基础所需的原始值、Ruby 版本和调用入口。
状态变化
在执行前记录接收者身份,并声明times方法的允许状态。
观察证据
保存第6章 循环的初值、参数、编码或资源位置。
正式节点:循环的基础、times方法、for语句、while语句、until语句、each方法、loop方法、break与next
控制与消息轨迹
在相同初值下定位首个分岔
- 01固定循环的基础的输入和接收者
- 02执行times方法并记录状态
- 03观察for语句的返回或副作用
- 04用break与next核对不变量并复位
运行不变量:每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。
边界故障探针
一次只破坏一个前提
本章回顾:按driver与exit boundary选循环
- Count、collection、condition与event对应不同iteration driver;每个循环都要有progress或shutdown证明。
- Times固定次数,for与each遍历receiver但变量scope不同,while/until按condition驱动。
- Each协议产生Enumerator并支撑Enumerable;是否有序、可重复和有size由receiver决定。
- Loop适合显式break的开放重复,break还可提供loop expression result。
- Break、next、redo与return离开不同边界;异常、cleanup和nested loop必须按边界测试。
练习与答案
练习
- 问题 1:建立正常轨迹。 回答“怎样为 times、while、until、each 与 loop 选择可证明终止的驱动方式?”,并写出四步执行记录。
- 问题 2:注入单一故障。 只制造“在 next 分支跳过状态推进,使循环重复同一个状态”,应从哪里开始定位?
- 问题 3:覆盖正式节点。 用一个证据包串联循环的基础、times方法、for语句、while语句、until语句、each方法、loop方法、break与next,说明为什么结论可由另一位读者独立复核。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 循环的基础
“第6章 循环”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- times方法
“第6章 循环”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- for语句
“第6章 循环”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- while语句
“第6章 循环”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- until语句
“第6章 循环”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- each方法
“第6章 循环”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- loop方法
“第6章 循环”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- break与next
“第6章 循环”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。