循环
循环
https://www.kancloud.cn/imxieke/ruby-base/107293
循环的基础
我们在编写程序时,常常遇到“希望这个处理重复执行多次”的情况。例如:
希望同样的处理执行 X 次
更复杂点的例子有:
用其他对象置换数组里的所有元素;
在达成某条件之前,一直重复执行处理。
这时,我们都需要用到循环。
接下来,我们将会介绍 Ruby 中基本的循环结构。其中比较特别的是,除了用传统的循环语句实现循环外,我们还能用方法来实现循环,也就是说我们可以根据自己的需要定制循环方法。
循环时的注意事项
下面两点是循环时必须注意的。
循环的主体是什么
停止循环的条件是什么
大家也许会认为,我们自己写的循环处理,“循环的主体是什么”我们自己总会知道吧。但是,实际编写程序时,稍不注意就会发生把不应该循环的处理加入到循环中这样的错误。而且,如果是循环里再嵌套循环的结构,在哪里做怎么样的循环、循环的结果怎么处理等都可能会使程序变得难以读懂。
另外,如果把“停止循环的条件”弄错了,有可能会发生处理无法终止,或者处理还没完成但已经跳出循环等这样的情况。大家写循环结构时务必注意上述两点,避免发生错误。
实现循环的方法
Ruby 中有两种实现循环的方法。
使用循环语句
利用 Ruby 提供现有的循环语句,可以满足大部分循环处理的需求。
使用方法实现循环
将块传给方法,然后在块里面写上需要循环的处理。一般我们在为了某种特定目的而需要定制循环结构时,才使用方法来实现循环。
下面是我们接下来要介绍的六种循环语句或方法。
times 方法
while 语句
each 方法
for 语句
until 语句
loop 方法
times 方法
如果只是单纯执行一定次数的处理,用 times 方法可以很轻松实现。
假设我们希望把“满地油菜花”这个字符串连续输出 7 次。
执行示例
使用 times 方法实现循环时,需要用到块 do ~ end
。
块的 do ~ end
部分可以用 {~}
代替,像下面这样:
在 times 方法的块里,也是可以获知当前的循环次数的。
这样,就可以把当前的循环次数赋值给变量 i
执行示例
请注意循环的次数是从 0 开始计算的。把循环次数的初始值设为 1 不失为一个好方法,但可惜我们不能这么做,因此我们只能在块里面对循环次数做调整
执行示例
但是,这样的写法会使变量 i
的值与实际输出的值产生差异。从降低程序复杂度来看,这并不是一个好的的编程习惯。若是对循环次数比较在意时,我们不必勉强使用 times
方法,可使用下面即将介绍的 for
语句和 while
语句。
for 语句
for
语句同样是用于实现循环的。需要注意的是,与刚才介绍的 times
方法不同,for
并不是方法,而是 Ruby 提供的循环控制语句。
以下是使用 for
语句的典型示例
执行示例
这是一个求从 1 到 5 累加的程序。for 语句的结构如下所示:
可以省略 do
我们回顾一下程序代码清单 6.4。程序第 1 行将 0 赋值给变量 sum,程序第 5 行输出变量 sum 的值并换行。
第 2 行到第 4 行的 for 语句指定变量 i 的范围是从 1 到 5。也就是说,程序一边从 1 到 5 改变变量 i 的值,一边执行 sum = sum + i。如果不使用循环语句,这个程序可以改写为:
for 语句与 times 方法不一样,循环的开始值和结束值可以任意指定。例如,我们想计算从变量 from 到变量 to 累加的总数,使用 times 方法的程序为:
使用 for 语句的程序为:
使用 for 语句后的程序变得更加简单了。
另外,sum = sum + i 这个式子也有更简单的写法:
本例是加法的简写,做减法、乘法时也同样可以做这样的省略。
普通的 for 语句
其实上一节介绍的是 for
语句的特殊用法,普通的 for
语句如下所示:
可以省略 do
可以看出,in
后面的部分和之前介绍的有点不同。
但和之前的 for
语句相比也并非完全不一样。实际上,..
或者 ...
都是创建范围对象时所需的符号。
当然,并非任何对象都可以指定给 for
语句使用。下面是使用数组对象的例子。
执行示例
本例中,循环遍历各数组的元素,并各自将其输出。
while 语句
不管哪种类型的循环,while 语句都可以胜任,while 语句的结构如下:
可以省略 do
这几行程序的意思就是,只要条件成立,就会不断地重复循环处理。我们来看看下面的示例。
执行示例
本例为什么会得出这样的结果呢。首先,程序将 1 赋值给变量 i
,这时 i
的值为 1。接下来 while
语句循环处理以下内容:
执行
i < 3
的比较。比较结果为真(也就是
i
比 3 小)时,程序执行puts i
和i += 1
。比较结果为假(也就是i
大于等于 3)时,程序跳出while
循环,不执行任何内容。返回 1 处理。
首次循环,由于 i
的初始值为 1,因此程序执行 puts 1
。第 2 次循环,i
的值为 2,比 3 小,因此程序执行 puts 2
。当程序执行到第 3 次循环,i 的值为 3,比 3 小的条件不成立,也就是说比较结果为假,因此,程序跳出 while
循环,并终止所有处理。
我们再来写一个使用 while
语句的程序。
把之前使用 for
语句写的程序,改写为使用 while
语句程序
这时与使用 for
语句的程序有细微的区别。首先,变量 i
的条件指定方式不一样。for
语句的例子通过 1..5
指定条件的范围。while
语句使用比较运算符 <=
,指定“i 小于等于 5 时(循环)”的条件。
另外,i
的累加方式也不一样。while
语句的例子在程序里直接写出了 i
是如何进行加 1 处理的——i += 1
。而 for
语句的例子并不需要在程序里直接写如何对 i
进行操作,自动在每次循环后对 i
进行加 1 处理。
就像这个例子一样,只要 for
语句能实现的循环,我们没必要特意将它改写为 while
语句。
在这个例子里,作为循环条件的不是变量 i
,而是变量 sum
。循环条件现在变为“sum 小于 50 时执行循环处理”。一般来说,不通过计算,我们并不知道 i
的值为多少时 sum
的值才会超过 50,因此这种情况下使用 for
语句的循环反而会让程序变得难懂。
有时 for 语句写的程序易懂,有时候 while
语句写的程序易懂。我们会在本章的最后说明,如何区别使用 for
语句和 while
语句。
until 语句
与 if
语句相对的有 unless
语句,同样地,与 while
语句相对的有 until
语句。until
语句的结构与 while
语句完全一样,只是条件判断刚好相反,不满足条件时才执行循环处理。换句话说,while
语句是一直执行循环处理,直到条件不成立为止;until
语句是一直执行循环处理,直到条件成立为止。
可以省略 do
本例是将使用 while
语句的程序用 until
语句改写了,与 while
语句所使用的条件刚好相反。
其实,在 while
语句的条件上使用表示否定的运算符 !
,也能达到和 until
语句相同的效果。
虽然可以使用 while
语句的否定形式代替 until
语句。但是,有时对一些比较复杂的条件表达式使用否定,反而会不直观,影响程序理解,在这种情况下,我们应该考虑使用 until
语句。
each 方法
each
方法将对象集合里的对象逐个取出,这与 for
语句循环取出数组元素非常相似。实际上,我们可以非常简单地将使用 for 语句的程序改写为使用 each
方法的程序
each 方法的结构如下:
在说明 times
方法我们曾提到过,块的 do ~ end
部分可换成 { ~ }
。
这与下面的程序的效果是几乎一样。
在 Ruby 内部,for
语句是用 each
方法来实现的。因此,可以使用 each
方法的对象,同样也可以指定为 for
语句的循环对象。
在介绍 for
语句时我们举过使用范围对象的例子,我们试着用 each
方法改写一下。
loop 方法
还有一种循环的方法,没有终止循环条件,只是不断执行循环处理。Ruby 中的 loop 就是这样的循环方法。
执行上面的程序后,整个屏幕会不停的输出文字 Ruby。为了避免这样的情况发生,在实际使用 loop
方法时,我们需要用到接下来将要介绍的 break
,使程序可以中途跳出循环。
程序不小心执行了死循环时,我们可以使用 CTRL + c 来强行终止程序。
循环控制
在进行循环处理的途中,我们可以控制程序马上终止循环,或者跳到下一个循环等。Ruby 提供了如下表所示的三种控制循环的命令。
命令 | 用途
| - break | 终止程序,跳出循环 next | 跳到下一次循环 redo | 在相同的条件下重复刚才的处理
我们来看看本例中的 break、next、redo 有什么不同。程序由三部分组成,除了 break、next、redo 这三部分的代码外,其他地方都是相同的。下面是执行后的结果。
break
break
会终止全体程序。在代码中,i
为 3 时,程序会执行第 6 行的 break
。执行 break
后,程序跳出 each
方法循环,前进至程序的第 10 行。因此,程序没有输出 Ruby 和 Scheme。
我们再来介绍一个关于 break
的例子。下列程序代码使程序最多只能输出 10 行匹配到的内容。匹配的时候,累加变量 matches,当达到 max_matches 时,程序就会终止 each_line 方法的循环。
next
使用 next 后,程序会忽略 next 后面的部分,跳到下一个循环开始的部分。在i 为 3 时在执行第 16 行的 next 后,程序前进到 each 方法的下个循环。也就是说,将 Scheme 赋值给 lang,并执行 i += 1。因此,程序并没有输出 Ruby,而是输出了 Scheme。
我们再来看看另外一个 next 的例子。程序逐行读取输入的内容,忽略空行或者以 # 开头的行,原封不动地输出除此以外所有行的内容。
strip.rb
fact.rb
stripped_fact.rb
执行以下命令后,我们会得到去掉 fact.rb 的注释和空行后的 stripped_fact.rb。
redo
redo
与 next
非常像,与 next
的不同之处是,redo
会再执行一次相同的循环。
与 next
时的情况不同,redo
会输出 Ruby。这是由于,i
为 3 时就执行了 redo
,程序只是返回循环的开头,也就是从程序的 i += 1
部分开始重新再执行处理,所以 lang
的值并没有从 Ruby 变为 Scheme。由于重复执行了 i += 1
,i
的值变为 4,这样 if
语句的条件 i == 3
就不成立了,redo 也不会再执行了,程序顺理成章地输出了 [4, "Ruby"] 以及 [5, "Scheme"]
另外,大家要注意 redo
的使用方法,稍不留神就会在同样的条件下,不断地重复处理,陷入死循环中。
break
、next
和 redo
中,一般比较常用是 break
和 next
。大家应该熟练掌握这两个命令的用法。即使是 Ruby 默认提供的库里面,实际上也很难找到 redo
的踪影,所以当我们在希望使用 redo
时,应该好好考虑是否真的有必要使用 redo
。
do~end 与 {~}
在 times 方法的示例中,我们介绍了块的两种写法,do ~ end
与 { ~ }
。从执行效果来看,两种方法虽然没有太大区别,但一般我们会遵守以下这个约定俗成的编码规则:
程序是跨行写的时候使用
do ~ end
程序写在 1 行的时候用
{ ~ }
以 times 方法来举例,会有以下两种写法。
或者,
刚开始大家可能会有点不习惯。我们可以这样理解,do ~ end
表示程序要执行内容是多个处理的集合,而 { ~ }
则表示程序需要执行的处理只有一个,即把整个带块的方法看作一个值。
如果用把 do ~ end
代码合并在一起,程序会变成下面这样:
以上写法,怎么看都给人一种很难断句的感觉。虽然实际上使用哪种写法都不会影响程序的运行,但在刚开始编写程序时,还是建议大家先遵守这个编码规则。