ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# **第 6 章 循环** 与条件判断一样,循环也遍布于程序的各个角落,是程序中不可缺少的重要组成部分。本章我们将会探讨: - **程序中的循环是什么** - **写循环时需要注意的事项** - **循环的种类及其写法** ### **6.1 循环的基础** 我们在编写程序时,常常遇到“希望这个处理重复执行多次”的情况。例如: - **希望同样的处理执行 X 次** 更复杂点的例子有: - **用其他对象置换数组里的所有元素;** - **在达成某条件之前,一直重复执行处理。** 这时,我们都需要用到循环。 接下来,我们将会介绍 Ruby 中基本的循环结构。其中比较特别的是,除了用传统的循环语句实现循环外,我们还能用方法来实现循环,也就是说我们可以根据自己的需要定制循环方法。关于如何定制循环 , 我们会在第 11 章再详细说明,这里我们先介绍一些预定义的循环语法结构。 ### **6.2 循环时的注意事项** 下面两点是循环时必须注意的。 - **循环的主体是什么** - **停止循环的条件是什么** 大家也许会认为,我们自己写的循环处理,“循环的主体是什么”我们自己总会知道吧。但是,实际编写程序时,稍不注意就会发生把不应该循环的处理加入到循环中这样的错误。而且,如果是循环里再嵌套循环的结构,在哪里做怎么样的循环、循环的结果怎么处理等都可能会使程序变得难以读懂。 另外,如果把“停止循环的条件”弄错了,有可能会发生处理无法终止,或者处理还没完成但已经跳出循环等这样的情况。大家写循环结构时务必注意上述两点,避免发生错误。 ### **6.3 实现循环的方法** Ruby 中有两种实现循环的方法。 - **使用循环语句** 利用 Ruby 提供现有的循环语句,可以满足大部分循环处理的需求。 - **使用方法实现循环** 将块传给方法,然后在块里面写上需要循环的处理。一般我们在为了某种特定目的而需要定制循环结构时,才使用方法来实现循环。 下面是我们接下来要介绍的六种循环语句或方法。 - **`times` 方法** - **`while` 语句** - **`each` 方法** - **`for` 语句** - **`until` 语句** - **`loop` 方法** Ruby 的常用循环结构就介绍到这里,接下来就让我们来具体看看如何使用这些语句或方法实现循环。 ### **6.4 times 方法** 如果只是单纯执行一定次数的处理,用 `times` 方法可以很轻松实现。 假设我们希望把“满地油菜花”这个字符串连续输出 7 次。 **代码清单 6.1 times.rb** ~~~ 7.times do puts "满地油菜花" end ~~~ > **执行示例** ~~~ > ruby times.rb 满地油菜花 满地油菜花 满地油菜花 满地油菜花 满地油菜花 满地油菜花 满地油菜花 ~~~ 使用 times 方法实现循环时,需要用到块 `do ~ end`。 **循环次数`.times do`  希望循环的处理 `end`** 块的 `do ~ end` 部分可以用`{~}`代替,像下面这样: **循环次数`.times {`  希望循环的处理 `}`** 在 `times` 方法的块里,也是可以获知当前的循环次数的。 ~~~ 10.times do |i| ┊ end ~~~ 这样,就可以把当前的循环次数赋值给变量 `i`。我们来看看实际的例子(代码清单 6.2)。 **代码清单 6.2 times2.rb** ~~~ 5.times do |i| puts "第#{i} 次的循环。" end ~~~ > **执行示例** ~~~ > ruby times2.rb 第 0 次的循环。 第 1 次的循环。 第 2 次的循环。 第 3 次的循环。 第 4 次的循环。 ~~~ 请注意循环的次数是从 0 开始计算的。把循环次数的初始值设为 `1` 不失为一个好方法,但可惜我们不能这么做,因此我们只能在块里面对循环次数做调整(代码清单 6.3)。 **代码清单 6.3 times3.rb** ~~~ 5.times do |i| puts "第#{i+1} 次的循环。" end ~~~ > **执行示例** ~~~ > ruby times3.rb 第 0 次的循环。 第 1 次的循环。 第 2 次的循环。 第 3 次的循环。 第 4 次的循环。 ~~~ 但是,这样的写法会使变量 `i` 的值与实际输出的值产生差异。从降低程序复杂度来看,这并不是一个好的的编程习惯。若是对循环次数比较在意时,我们不必勉强使用 `times` 方法,可使用下面即将介绍的 `for` 语句和 `while` 语句。 ### **6.5 for 语句** `for` 语句同样是用于实现循环的。需要注意的是,与刚才介绍的 `times` 方法不同,`for` 并不是方法,而是 Ruby 提供的循环控制语句。 以下是使用 `for` 语句的典型示例(代码清单 6.4)。 **代码清单 6.4 for.rb** ~~~ 1: sum = 0 2: for i in 1..5 3: sum = sum + i 4: end 5: puts sum ~~~ > **执行示例** ~~~ > ruby for.rb 15 ~~~ 这是一个求从 1 到 5 累加的程序。`for` 语句的结构如下所示: **`for` 变量 `in` 开始时的数值`..`结束时的数值 `do`  希望循环的处理 `end`** ※ 可以省略 `do` 我们回顾一下程序代码清单 6.4。程序第 1 行将 0 赋值给变量 `sum`,程序第 5 行输出变量 `sum` 的值并换行。 第 2 行到第 4 行的 `for` 语句指定变量 `i` 的范围是从 1 到 5。也就是说,程序一边从 1 到 5 改变变量 `i` 的值,一边执行 `sum = sum + i`。如果不使用循环语句,这个程序可以改写为: ~~~ sum = 0 sum = sum + 1 sum = sum + 2 sum = sum + 3 sum = sum + 4 sum = sum + 5 puts sum ~~~ `for` 语句与 `times` 方法不一样,循环的开始值和结束值可以任意指定。例如,我们想计算从变量 `from` 到变量 `to` 累加的总数,使用 `times` 方法的程序为: ~~~ from = 10 to = 20 sum = 0 (to - from + 1).times do |i| sum = sum + (i + from) end puts sum ~~~ 使用 for 语句的程序为: ~~~ from = 10 to = 20 sum = 0 for i in from..to sum = sum + i end puts sum ~~~ 使用 `for` 语句后的程序变得更加简单了。 另外,`sum = sum + i` 这个式子也有更简单的写法: ~~~ sum += i ~~~ 本例是加法的简写,做减法、乘法时也同样可以做这样的省略。 ~~~ a -= b a *= b ~~~ 第 9 章我们会再详细讨论这方面的内容,现在暂时先记着有这么一个省略写法就可以了。 ### **6.6 普通的 for 语句** 其实上一节介绍的是 `for` 语句的特殊用法,普通的 `for` 语句如下所示: **`for` 变量 `in` 对象 `do`  希望循环的处理 `end`** ※ 可以省略 `do` 可以看出,`in` 后面的部分和之前介绍的有点不同。 但和之前的 `for` 语句相比也并非完全不一样。实际上,.. 或者 ... 都是创建范围对象时所需的符号。 当然,并非任何对象都可以指定给 `for` 语句使用。下面是使用数组对象的例子。 **代码清单 6.5 for_names.rb** ~~~ names = ["awk", "Perl", "Python", "Ruby"] for name in names puts name end ~~~ > **执行示例** ~~~ > ruby for_names.rb awk Perl Python Ruby ~~~ 本例中,循环遍历各数组的元素,并各自将其输出。 ### **6.7 while 语句** 不管哪种类型的循环,`while` 语句都可以胜任,`while` 语句的结构如下: **`while` 条件 `do`  希望循环的处理 `end`** ※ 可以省略 `do` ![{%}](https://box.kancloud.cn/2015-10-26_562e01ddd862d.png) 这几行程序的意思就是,只要条件成立,就会不断地重复循环处理。我们来看看下面的示例(代码清单 6.6)。 **代码清单 6.6 while.rb** ~~~ i = 1 while i < 3 puts i i += 1 end ~~~ > **执行示例** ~~~ > ruby while.rb 1 2 ~~~ 本例为什么会得出这样的结果呢。首先,程序将 1 赋值给变量 `i`,这时 `i` 的值为 1。接下来 `while` 语句循环处理以下内容: 1. **执行 `i < 3` 的比较。** 2. **比较结果为真(也就是 `i` 比 3 小)时,程序执行 `puts i` 和 `i += 1`。比较结果为假(也就是 `i` 大于等于 3)时,程序跳出 `while` 循环,不执行任何内容。** 3. **返回 1 处理。** 首次循环,由于 `i` 的初始值为 1,因此程序执行 `puts 1`。第 2 次循环,`i` 的值为 2,比 3 小,因此程序执行 `puts 2`。当程序执行到第 3 次循环,i 的值为 3,比 3 小的条件不成立,也就是说比较结果为假,因此,程序跳出 `while` 循环,并终止所有处理。 我们再来写一个使用 `while` 语句的程序。 把之前使用 `for` 语句写的程序(代码清单 6.4),改写为使用 `while` 语句程序(代码清单 6.7)。 **代码清单 6.7 while2.rb** ~~~ sum = 0 i = 1 while i <= 5 sum += i i += 1 end puts sum ~~~ 这时与使用 `for` 语句的程序有细微的区别。首先,变量 `i` 的条件指定方式不一样。`for` 语句的例子通过 `1..5` 指定条件的范围。`while` 语句使用比较运算符 <=,指定“`i` 小于等于 5 时(循环)”的条件。 另外,`i` 的累加方式也不一样。`while` 语句的例子在程序里直接写出了 `i` 是如何进行加 1 处理的——`i += 1`。而 `for` 语句的例子并不需要在程序里直接写如何对 `i` 进行操作,自动在每次循环后对 i 进行加 1 处理。 就像这个例子一样,只要 `for` 语句能实现的循环,我们没必要特意将它改写为 `while` 语句。程序代码清单 6.8 是使用 `while` 语句反而会更便于理解的一个例子。 **代码清单 6.8 while3.rb** ~~~ sum = 0 i = 1 while sum < 50 sum += i i += 1 end puts sum ~~~ 在这个例子里,作为循环条件的不是变量 `i`,而是变量 `sum`。循环条件现在变为“`sum` 小于 50 时执行循环处理”。一般来说,不通过计算,我们并不知道 `i` 的值为多少时 `sum` 的值才会超过 50,因此这种情况下使用 `for` 语句的循环反而会让程序变得难懂。 有时 `for` 语句写的程序易懂,有时候 `while` 语句写的程序易懂。我们会在本章的最后说明,如何区别使用 `for` 语句和 `while` 语句。 ### **6.8 until 语句** 与 `if` 语句相对的有 `unless` 语句,同样地,与 `while` 语句相对的有 `until` 语句。`until` 语句的结构与 `while` 语句完全一样,只是条件判断刚好相反,不满足条件时才执行循环处理。换句话说,`while` 语句是一直执行循环处理,直到条件不成立为止;`until` 语句是一直执行循环处理,直到条件成立为止。 **`until` 条件 `do`  希望循环的处理 `end`** ※ 可以省略 `do` ![{%}](https://box.kancloud.cn/2015-10-26_562e01dde6237.png) 代码清单 6.9 是使用了 `until` 语句的程序。 **代码清单 6.9 until.rb** ~~~ sum = 0 i = 1 until sum >= 50 sum += i i+= 1 end puts sum ~~~ 本例是将使用 `while` 语句的程序(代码清单 6.8)用 `until` 语句改写了,与 `while` 语句所使用的条件刚好相反。 其实,在 `while` 语句的条件上使用表示否定的运算符 !,也能达到和 `until` 语句相同的效果。 **代码清单 6.10 while_not.rb** ~~~ sum = 0 i = 1 while !(sum >= 50) sum += i i += 1 end puts sum ~~~ 虽然可以使用 `while` 语句的否定形式代替 `until` 语句。但是,有时对一些比较复杂的条件表达式使用否定,反而会不直观,影响程序理解,在这种情况下,我们应该考虑使用 `until` 语句。 ### **6.9 each 方法** `each` 方法将对象集合里的对象逐个取出,这与 `for` 语句循环取出数组元素非常相似。实际上,我们可以非常简单地将使用 `for` 语句的程序(代码清单 6.5)改写为使用 `each` 方法的程序(代码清单 6.11)。 **代码清单 6.11 each_names.rb** ~~~ names = ["awk","Perl","Python","Ruby"] names.each do |name| puts name end ~~~ `each` 方法的结构如下: **对象`.each do` | 变量 |  希望循环的处理 `end`** 在说明 `times` 方法我们曾提到过,块的 `do` ~ `end` 部分可换成 { ~ }。 **对象.`each {`| 变量 |  希望循环的处理 `}`** 这与下面的程序的效果是几乎一样。 **`for` 变量 `in` 对象  希望循环的处理 `end`** 在 Ruby 内部,`for` 语句是用 `each` 方法来实现的。因此,可以使用 `each` 方法的对象,同样也可以指定为 `for` 语句的循环对象。 在介绍 `for` 语句时我们举过使用范围对象的例子(代码清单 6.4),我们试着用 `each` 方法改写一下。 **代码清单 6.12 each.rb** ~~~ sum = 0 (1..5).each do |i| sum= sum + i end puts sum ~~~ 像本例这样,我们可以轻松互相改写两种用法。那到底什么时候该用 `for` 语句,什么时候该用 `each` 方法呢,我们将在本章的最后讨论这个问题。 ### **6.10 loop 方法** 还有一种循环的方法,没有终止循环条件,只是不断执行循环处理。Ruby 中的 `loop` 就是这样的循环方法。 ~~~ loop do print "Ruby" end ~~~ 执行上面的程序后,整个屏幕会不停的输出文字 `Ruby`。为了避免这样的情况发生,在实际使用 `loop` 方法时,我们需要用到接下来将要介绍的 `break`,使程序可以中途跳出循环。 > **备注** 程序不小心执行了死循环时,我们可以使用 CTRL + c 来强行终止程序。 ### **6.11 循环控制** 在进行循环处理的途中,我们可以控制程序马上终止循环,或者跳到下一个循环等。Ruby 提供了如下表(表 6.1)所示的三种控制循环的命令。 **表 6.1 循环控制命令** <table border="1" data-line-num="407 408 409 410 411 412" width="90%"><thead><tr><th> <p class="表头单元格">命令</p> </th> <th> <p class="表头单元格">用途</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格">break</p> </td> <td> <p class="表格单元格">终止程序,跳出循环</p> </td> </tr><tr><td> <p class="表格单元格">next</p> </td> <td> <p class="表格单元格">跳到下一次循环</p> </td> </tr><tr><td> <p class="表格单元格">redo</p> </td> <td> <p class="表格单元格">在相同的条件下重复刚才的处理</p> </td> </tr></tbody></table> 在控制循环的命令中,可能不太容易区分使用 `next` 和 `redo`。借着下面的例子(代码清单 6.13),我们来介绍一下如何区分这三种循环命令。 **代码清单 6.13 break_next_redo.rb** ~~~ 1: puts "break 的例子:" 2: i = 0 3: ["Perl", "Python", "Ruby", "Scheme"].each do |lang| 4: i += 1 5: if i == 3 6: break 7: end 8: p [i,lang] 9: end 10: 11: puts "next 的例子:" 12: i = 0 13: ["Perl", "Python", "Ruby", "Scheme"].each do |lang| 14: i += 1 15: if i == 3 16: next 17: end 18: p [i,lang] 19: end 20: 21: puts "redo 的例子:" 22: i = 0 23: ["Perl", "Python", "Ruby", "Scheme"].each do |lang| 24: i += 1 25: if i == 3 26: redo 27: end 28: p [i,lang] 29: end ~~~ 我们来看看本例中的 `break`、`next`、`redo` 有什么不同。程序由三部分组成,除了 `break`、`next`、`redo` 这三部分的代码外,其他地方都是相同的。下面是执行后的结果。 > **执行示例** ~~~ > ruby break_next_redo.rb break 的例子: [1, "Perl"] [2, "Python"] next 的例子: [1, "Perl"] [2, "Python"] [4, "Scheme"] redo 的例子: [1, "Perl"] [2, "Python"] [4, "Ruby"] [5, "Scheme"] ~~~ ### **6.11.1 break** `break` 会终止全体程序。在代码清单 6.13 中,`i` 为 3 时,程序会执行第 6 行的 `break`(图 6.1)。执行 `break` 后,程序跳出 `each` 方法循环,前进至程序的第 10 行。因此,程序没有输出 `Ruby` 和 `Scheme`。 ![{%}](https://box.kancloud.cn/2015-10-26_562e01ddf342f.png) **图 6.1 break** 我们再来介绍一个关于 `break` 的例子。程序代码清单 6.14 对第 3 章的 simple_grep.rb 做了点小修改,使程序最多只能输出 10 行匹配到的内容。匹配的时候,累加变量 `matches`,当达到 `max_matches` 时,程序就会终止 `each_line` 方法的循环。 **代码清单 6.14 ten_lines_grep.rb** ~~~ pattern = Regexp.new(ARGV[0]) filename = ARGV[1] max_matches = 10 # 输出的最大行数 matches = 0 # 已匹配的行数 file = File.open(filename) file.each_line do |line| if matches >= max_matches break end if pattern =~ line matches += 1 puts line end end file.close ~~~ ### **6.11.2 next** 使用 `next` 后,程序会忽略 `next` 后面的部分,跳到下一个循环开始的部分。在代码清单 6.13 中,`i` 为 3 时在执行第 16 行的 `next` 后,程序前进到 `each` 方法的下个循环(图 6.2)。也就是说,将 `Scheme` 赋值给 `lang`,并执行 `i += 1`。因此,程序并没有输出 `Ruby`,而是输出了 `Scheme`。 ![{%}](https://box.kancloud.cn/2015-10-26_562e01de22018.png) **图 6.2 next** 我们再来看看另外一个 `next` 的例子(代码清单 6.15)。程序逐行读取输入的内容,忽略空行或者以 # 开头的行,原封不动地输出除此以外所有行的内容。 执行以下命令后,我们会得到去掉 fact.rb(代码清单 6.16)的注释和空行后的 stripped_fact.rb(代码清单 6.17)。 ~~~ > ruby strip.rb fact.rb > stripped_fact.rb ~~~ **代码清单 6.15 strip.rb** ~~~ file = File.open(ARGV[0]) file.each_line do |line| next if /^\s*$/ =~ line # 空行 next if /^#/ =~ line # 以“#”开头的行 puts line end file.close ~~~ **代码清单 6.16 fact.rb** ~~~ # 求10 的阶乘 ans = 1 for i in 1..10 ans *= i end # 输出 puts "10! = #{ans}" ~~~ **代码清单 6.17 stripped_fact.rb** ~~~ ans = 1 for i in 1..10 ans *= i end puts "10! = #{ans}" ~~~ ### **6.11.3 redo** `redo` 与 `next` 非常像,与 `next` 的不同之处是,`redo` 会再执行一次相同的循环。 在代码清单 6.13 中,与 `next` 时的情况不同,`redo` 会输出 `Ruby`。这是由于,`i` 为 3 时就执行了第 26 行的 `redo`,程序只是返回循环的开头,也就是从程序的第 24 行的 `i += 1` 部分开始重新再执行处理,所以 `lang` 的值并没有从 `Ruby` 变为 `Scheme`。由于重复执行了 `i += 1`,`i` 的值变为 4,这样 `if` 语句的条件 `i == 3` 就不成立了,`redo` 也不会再执行了,程序顺理成章地输出了 `[4, "Ruby"]` 以及 `[5, "Scheme"]`(图 6.3)。 ![{%}](https://box.kancloud.cn/2015-10-26_562e01de3f45e.png) **图 6.3 redo** 另外,大家要注意 `redo` 的使用方法,稍不留神就会在同样的条件下,不断地重复处理,陷入死循环中。 `break`、`next` 和 `redo` 中,一般比较常用是 `break` 和 `next`。大家应该熟练掌握这两个命令的用法。即使是 Ruby 默认提供的库里面,实际上也很难找到 `redo` 的踪影,所以当我们在希望使用 `redo` 时,应该好好考虑是否真的有必要使用 `redo`。 ### **6.12 总结** 本章我们主要介绍了循环时所用的语句以及方法。 如果纯粹只考虑实现循环功能,任何种类的循环都可以用 `while` 语句实现。讲得极端一点,其他循环语句和方法根本也没存在的必要。即便如此,那为什么 Ruby 还提供了那么丰富的循环语句和方法呢。这是因为,程序并不只是单纯为了实现功能,同时还应该使代码更便于读写,使人更容易理解。 为了使大家更快地熟悉循环的用法,我们在最初介绍循环语句和方法的一览表中加上“主要用途”一栏(表 6.2),以供大家参考。 **表 6.2 循环语句、方法及其主要用途** <table border="1" data-line-num="566 567 568 569 570 571 572 573 574" width="90%"><thead><tr><th> <p class="表头单元格"> </p> </th> <th> <p class="表头单元格">主要用途</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格"><code>times</code>方法</p> </td> <td> <p class="表格单元格">确定循环次数时使用</p> </td> </tr><tr><td> <p class="表格单元格"><code>for</code>语句</p> </td> <td> <p class="表格单元格">从对象取出元素时使用(each 的语法糖)</p> </td> </tr><tr><td> <p class="表格单元格"><code>while</code>语句</p> </td> <td> <p class="表格单元格">希望自由指定循环条件时使用</p> </td> </tr><tr><td> <p class="表格单元格"><code>until</code>语句</p> </td> <td> <p class="表格单元格">使用 while 语句使循环条件变得难懂时使用</p> </td> </tr><tr><td> <p class="表格单元格"><code>each</code>方法</p> </td> <td> <p class="表格单元格">从对象取出元素时使用</p> </td> </tr><tr><td> <p class="表格单元格"><code>loop</code>方法</p> </td> <td> <p class="表格单元格">不限制循环次数时使用</p> </td> </tr></tbody></table> > **备注** > 语法糖(syntax sugar),是指一种为了照顾一般人习惯而产生的特殊语法 1。例如,使用常规的语法调 用方法,那么加法运算应该写成 `3.add(2)`。但是对于一般人来说,写成 `3 + 2` 会更直观,更简明易懂。 > 语法糖并不会加强编程语言的任何功能,但是对于提高程序的易读性来讲是不可或缺的。 1即一种符合人的思维模式、工作习惯等,便于理解的“甜蜜”的语法。——译者注 以上都是笔者个人意见,仅供大家参考。 确定循环次数时循环处理使用 `times` 方法,除此以外的大部分循环处理,根据情况选择 `while` 语句或者 `each` 方法,一般也能写出直观易懂的程序。请大家先熟练掌握这三种循环方法。 > **专栏** > **do~end 与 {~}** > 在 `times` 方法的示例中,我们介绍了块的两种写法,`do ~ end` 与 `{ ~ }`。从执行效果来看,两种方法虽然没有太大区别,但一般我们会遵守以下这个约定俗成的编码规则: > - > 程序是跨行写的时候使用 **`do ~ end`** > - > 程序写在 1 行的时候用 **`{ ~ }`** > 以 `times` 方法来举例,会有以下两种写法。 ~~~ 10.times do |i| puts i end ~~~ > 或者, ~~~ 10.times{|i| puts i} ~~~ > 刚开始大家可能会有点不习惯。我们可以这样理解,`do ~ end` 表示程序要执行内容是多个处理的集合,而 `{ ~ }` 则表示程序需要执行的处理只有一个,即把整个带块的方法看作一个值。 > 如果用把 `do ~ end` 代码合并在一起,程序会变成下面这样: ~~~ 10.times do |i| puts i end ~~~ > 以上写法,怎么看都给人一种很难断句的感觉。虽然实际上使用哪种写法都不会影响程序的运行,但在刚开始编写程序时,还是建议大家先遵守这个编码规则。