大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
控制流
这一课你将能做到
- 熟练使用 if/unless/case、while/until/for 处理分支与循环
- 用 each/times/map/select/inject 等迭代器写出地道的 Ruby 循环,而不是传统 for 循环
- 正确使用 break/next/redo/retry 控制迭代行为,并理解迭代器与循环的本质区别
为什么要学
控制流是任何语言的核心,但 Ruby 的控制流有自己的「味道」:除非必要,不用 for 循环;unless 后置让条件读起来像自然语言;迭代器(iterator)是 Ruby 的灵魂——Rails 的 find_each、Sidekiq 的批量处理,底层都是迭代器。控制流不熟,Ruby 代码能写,但写不地道。
人话:控制流是程序的「交通指挥」。传统的 if/while/for 是手动挡,Ruby 的迭代器是自动挡——你告诉它「对每个元素做什么」,而不是「怎么一步步做」。
先建立图像
控制流选择指南
┌──────────────────┬──────────────────────────┐
│ 场景 │ 推荐写法 │
├──────────────────┼──────────────────────────┤
│ 条件分支(2-3 个) │ if / unless / 三目 │
│ 条件分支(多个值) │ case-when │
│ 固定次数循环 │ 3.times { } │
│ 遍历集合 │ each / map / select / inject │
│ 条件循环 │ while / until / loop { } │
│ 提前退出迭代 │ break / next │
└──────────────────┴──────────────────────────┘
核心心法:Ruby 程序员首选迭代器,其次才是循环。
跟着做
步骤 1:if / unless / case
score = 85
# if-elsif-else
if score >= 90
puts "优秀"
elsif score >= 60
puts "及格"
else
puts "不及格"
end
# unless:if not 的语义化写法,否定条件更自然
unless score < 60
puts "通过了"
end
# 后置写法:一行时最简洁
puts "高分" if score >= 90
puts "继续加油" unless score >= 90
# case-when:多值匹配,区间、类型、正则都能用
grade = case score
when 90..100 then "A"
when 80...90 then "B"
when 60...80 then "C"
else "D"
end
puts "等级:#{grade}"
# case 还能按类型匹配
value = "hello"
desc = case value
when Integer then "整数"
when String then "字符串"
when Array then "数组"
else "其他"
end
puts desc # => 字符串
步骤 2:while / until / loop
# while:条件为真时循环
count = 0
while count < 3
puts "while: #{count}"
count += 1
end
# until:条件为假时循环(while not 的语义化)
until count >= 5
puts "until: #{count}"
count += 1
end
# loop:无限循环,配合 break 退出
number = 0
loop do
number += 1
puts number
break if number >= 3
end
# 后置修饰符:简洁但注意——do-while 语义!
num = 0
begin
num += 1
puts num
end while num < 2 # 先执行一次,再判断条件
步骤 3:迭代器——Ruby 的灵魂
# each:最基础的遍历
[1, 2, 3].each do |n|
puts "元素: #{n}"
end
# times:固定次数
3.times { |i| puts "第 #{i} 次" } # i 从 0 开始
# map:变换,返回新数组
squares = [1, 2, 3].map { |n| n * n }
puts squares.inspect # => [1, 4, 9]
# select / reject:过滤
evens = [1, 2, 3, 4, 5].select { |n| n.even? }
odds = [1, 2, 3, 4, 5].reject { |n| n.even? }
puts evens.inspect # => [2, 4]
puts odds.inspect # => [1, 3, 5]
# inject / reduce:累积
sum = [1, 2, 3, 4].inject(0) { |acc, n| acc + n }
puts sum # => 10
# 简写:传符号
puts [1, 2, 3, 4].inject(:+) # => 10
puts [1, 2, 3, 4].sum # => 10(Ruby 2.4+)
# each_with_index:带索引遍历
["a", "b", "c"].each_with_index do |item, idx|
puts "#{idx}: #{item}"
end
# each_with_object:边遍历边构造新对象
hash = ["a", "b"].each_with_object({}) do |letter, h|
h[letter] = letter.upcase
end
puts hash.inspect # => {"a"=>"A", "b"=>"B"}
步骤 4:break / next / redo
# break:跳出整个迭代
[1, 2, 3, 4, 5].each do |n|
break if n > 3
puts n
end
# 输出 1, 2, 3
# next:跳过当前元素,继续下一个
[1, 2, 3, 4, 5].each do |n|
next if n.even?
puts n
end
# 输出 1, 3, 5
# redo:重新执行当前迭代(不推进到下一个元素)
attempts = 0
[1, 2, 3].each do |n|
attempts += 1
puts "尝试 #{n}(第 #{attempts} 次)"
redo if attempts < 2 # 前两次都重来
end
# 输出:尝试 1(第 1 次),尝试 1(第 2 次),尝试 2(第 3 次)...
# each 的返回值是原数组本身,这很重要!
result = [1, 2].each { |n| puts n }
puts result.inspect # => [1, 2],不是 nil!
讲清楚要点
- 迭代器 vs 循环:迭代器(each/map/select)是对象的方法,把「怎么做」委托给对象;循环(while/for)是语言结构,显式控制流程。Ruby 社区文化:能用迭代器就不用循环。
for循环在 Ruby 里几乎不用:for i in [1,2,3]会创建局部变量泄露到外部作用域,且性能差。除非面试,生产代码里见到for可以提 PR 改掉。next不是continue:在 Ruby 迭代器里,next跳过当前元素到下一个,语义和 Java 的continue一样;但next还能带返回值:next n * 2。redo和retry的区别:redo重新执行当前迭代(不重新取值);retry重新进入整个迭代器(Ruby 2.2+ 在迭代器里禁用,只在 begin-rescue 里可用)。日常几乎只用redo做重试逻辑。while后置是 do-while:begin ... end while cond先执行一次再判断;普通while cond ... end先判断。这个区别和 C 语言一致,但 Ruby 中较少用。
你可能会懵的地方
-
each和map都能遍历,什么时候用哪个? 要「副作用」(打印、写库、发请求)用each;要「变换结果」(转成新数组)用map。map的返回值是新数组,each返回原数组。 -
select和detect/find的区别?select返回所有匹配元素的数组;detect(别名find)返回第一个匹配的元素或 nil。找单个用find,过滤多个用select。 -
inject的括号参数是什么?inject(0)里的 0 是初始累积值。不给初始值时,第一个元素当初始值:[1,2,3].inject(:+)等价1 + 2 + 3。给初始值更安全,空数组不会报错。 -
loop和while true有什么区别?loop是 Kernel 方法,配合break使用,还能捕获StopIteration异常;while true是语言结构。loop更 Ruby 风格,还能loop do ... break value end返回值。 -
迭代器里能用
return吗? 在方法里调用迭代器,return会直接退出整个方法,不是退出迭代器。这是 block 的闭包特性,和 JavaScript 的forEach里return只跳过当前元素完全不同!
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 用 for 循环遍历数组 | 变量泄露、不地道 | 用 each/map/select |
each 里想收集结果 | each 返回原数组 | 用 map 或 inject |
迭代器里 return 想跳过 | 退出整个方法 | 用 next |
while 后置以为先判断 | do-while 语义,先执行一次 | 先判断用 while ... end 前置 |
if score > 90 写成 if score > 90 then | then 在单行 case 才常用 | if 里省略 then,保持简洁 |
卡点急救
- 迭代器里变量没生效:block 参数
|n|和外部变量同名会遮蔽。改参数名或检查作用域。 break没退出循环:可能在嵌套迭代器里,break只退当前层。用catch/throw或重构逻辑。- 无限循环:
loop do里忘了break条件。Ctrl+C 终止,检查条件逻辑。
面试常问
-
Ruby 的迭代器和循环有什么区别?为什么推荐迭代器? 迭代器把遍历逻辑封装在对象内部,代码更声明式、更少出错;循环是命令式,暴露索引和边界。Ruby 社区认为迭代器更符合「告诉它做什么,而不是怎么做」的哲学。
-
each的返回值是什么?为什么? 返回调用它的原数组。设计意图:each 用于副作用,返回值不重要;如果需要变换结果,应该用 map。这个设计让each链式调用时不会意外创建新数组。 -
break和next在 block 里的行为?break退出整个迭代器,可以带返回值:break "done";next跳过当前元素到下一个,也可以带返回值:next n * 2。两者都是 block 的局部控制流。
自测三问
each、map、select、inject各自的返回值和用途?unless和if !等价吗?什么场景用 unless 更好?- 在迭代器里想「跳过当前元素」和「退出整个迭代」分别用什么关键字?
小结一句话
Ruby 控制流的精髓是:分支用 if/unless/case,遍历用 each/map/select/inject,循环是最后的手段。
延伸
- Ruby 迭代器官方文档
- 下一课:方法与 Blocks——理解 Ruby 的代码块和 yield 机制。