第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.lengthitems.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 # :before

Ruby社区通常偏好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_indexlazy或由外部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 result

Break的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变化,否则同一输入永久重跑。

正式节点与章专属证据

  • :第 1 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
  • :第 2 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
  • :第 3 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
  • :第 4 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
  • :第 5 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
  • :第 6 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
  • :第 7 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。
  • :第 8 个正式节点要能回到“每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。”,并说明故障发生前后的第一处差异。

对象与状态模型

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

怎样为 times、while、until、each 与 loop 选择可证明终止的驱动方式?

输入与接收者

固定循环的基础所需的原始值、Ruby 版本和调用入口。

状态变化

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

观察证据

保存第6章 循环的初值、参数、编码或资源位置。

正式节点:循环的基础、times方法、for语句、while语句、until语句、each方法、loop方法、break与next

控制与消息轨迹

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

  1. 01固定循环的基础的输入和接收者
  2. 02执行times方法并记录状态
  3. 03观察for语句的返回或副作用
  4. 04用break与next核对不变量并复位

运行不变量:每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。

边界故障探针

一次只破坏一个前提

基线满足:每轮都保持处理区间不变量,并让剩余工作量严格减少或显式退出。

本章回顾:按driver与exit boundary选循环

  1. Count、collection、condition与event对应不同iteration driver;每个循环都要有progress或shutdown证明。
  2. Times固定次数,for与each遍历receiver但变量scope不同,while/until按condition驱动。
  3. Each协议产生Enumerator并支撑Enumerable;是否有序、可重复和有size由receiver决定。
  4. Loop适合显式break的开放重复,break还可提供loop expression result。
  5. Break、next、redo与return离开不同边界;异常、cleanup和nested loop必须按边界测试。

练习与答案

练习

  1. 问题 1:建立正常轨迹。 回答“怎样为 times、while、until、each 与 loop 选择可证明终止的驱动方式?”,并写出四步执行记录。
  1. 问题 2:注入单一故障。 只制造“在 next 分支跳过状态推进,使循环重复同一个状态”,应从哪里开始定位?
  1. 问题 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章 循环”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

资料与写作方式声明

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

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

讨论

评论区加载中…