第五章
循环和迭代
大部分的程序设计中都会涉及到重复。也许你想你的程序连续发出十次”哔”的声音,或者
从一个文件中读入多行文本并将其打印出来,除非用户输入按键打印一个警告。Ruby 提供
了很多执行这种重复工作的途径。
For 循环
在很多的程序设计语言中,当你想将一段代码运行指定次数,你可以将其放到一个for 循环
中。在大部分语言中,你给 for 循环一个变量,并将其初始化为一个初值,在每次循环中递
增 1,直到它的值等于指定的终止值。当该变量值等于终止值,for 循环停止。这里有一个
在 Pascal 语言中的一个传统的 for 循环:
(* This is Pascal code, not Ruby! *)
for i := 1 to 3 do
writeln( i );
for_loop.rb
你可以回顾一下上一章中我们所讲到的 Ruby 的 for 循环跟这个一点都不像。Ruby 中不再
提供一个初始值和终止值,而是给 for 循环提供一个项列表,然后一个一个地迭代该列表,
依此将每项值赋给循环变量,直到列表的结尾。
例如,这是一个 for 循环,在该循环中迭代一个数组中的所有项,然后将其值依序打印出来:
# This is Ruby code...
for i in [1,2,3] do
puts( i )
end
这个 for 循环更像一些其他语言中提供的 for each 迭代。循环迭代的项没必要都是整型数,
这样也是可以的...
for s in ['one','two','three'] do
puts( s )
end
2.
Ruby 的作者将集合类型 Array,Sets,Hashes和 Strings(和 String 一样,但事实上是字符集
合)实现的 each 方法描述为 for 循环的”语法糖”。为了比较,这是一个上面循环的 each
方法表现形式:
each_loop.rb
[1,2,3].each do |i|
puts( i )
end
如你所见,这两种方式几乎没有差别。将 for 循环转化为 each 迭代式,我所需要做的就是
删掉 for 和 in,然后在数组后追加一个.each。然后我将迭代变量 i 放到 do 后面一对竖线中。
比较以下的例子,看看 for 循环和 each 迭代之间有多么的相似:
for_each.rb
# --- Example 1 ---
# i) for
for s in ['one','two','three'] do
puts( s )
end
# ii) each
['one','two','three'].each do |s|
puts( s )
end
# --- Example 2 ---
# i) for
for x in [1, "two", [3,4,5] ] do puts( x ) end
# ii) each
[1, "two", [3,4,5] ].each do |x| puts( x ) end
另外,在 for 循环分多行书写时,do 关键字是可选的(可以不写),但是如果当整个循环在一
行书写时,do 关键字是必须的:
# Here the „do‟ keyword can be omitted
3.
for s in['one','two','three']
puts( s )
end
# But here it is required
for s in ['one','two','three'] do puts( s ) end
for_to.rb
如何书写一个”标准的” for 循环 ...
如果你依然想念传统的 for 循环,你可以通过使用 for 循环来迭代一个区间伪造一个。例
如,现在需要使用一个 for 循环来将从 1 到 10 的数字依次打印出来:
for i in (1..10) do
puts( i )
end
for_each2.rb
这个例子是关于如何使用 for 和 each 来迭代区间中的值的:
# for
for s in 1..3
puts( s )
end
# each
(1..3).each do |s|
puts(s)
end
另外,注意一个像 1..3 这样的区间表达式,在使用 each 方法时必须使用圆括号括起来,否
则 Ruby 将会假定你想使用常量的 each 方法而不是整个表达式的 each 方法。但是在 for 循
环中,这个括号是可选的。
复合迭代参数
multi_array.rb
你可以回顾一下上一章,我们在一个 for 循环中使用了多个循环变量。我们通过这种方式来
4.
迭代一个多维数组。在每一个 for 循环中,一个变量被赋值为外部数组中的一行(也就是一个
子数组):
# Here multiarr is an array containing two „rows‟
# (sub-arrays) at index 0 and 1
multiarr = [ ['one','two','three','four'],
[1,2,3,4]
]
# This for loop runs twice (once for each „row‟ of multiarr)
for (a,b,c,d) in multiarr
print("a=#{a}, b=#{b}, c=#{c}, d=#{d}n" )
end
上面的循环将打印如下结果:
a=one, b=two, c=three, d=four
a=1, b=2, c=3, d=4
我们可以通过传递四”块参数“(a,b,c,d)来使用 each 方法迭代这个四项数组,块参数由 do
和 end 来界定:
multiarr.each do |a,b,c,d|
print("a=#{a}, b=#{b}, c=#{c}, d=#{d}n" )
end
块参数
在 Ruby 中,一个迭代体称为一个”块”,在迭代顶部包含在一对竖线中的变量都称为”块
变量”。也就是说,一个块像一个函数般运行,块参数和函数参数列表相似。 each 方法运
行在块中的代码,并将一个集合 ( 如数组 multiarr) 提供的值传递给块中代码。在上面的例
子中, each 方法重复传递一个四元素的数组给循环中的代码块,这些元素用来初始化这四
个块参数 a,b,c,d 。块还可以被用于其他的事情,如迭代集合等。我将在第十章详细介绍块。
块
block_syntax.rb
Ruby 还有一种语法可以用来界定块。不使用 do..end,而使用花括号{..},如下:
5.
# do..end
[[1,2,3],[3,4,5],[6,7,8]].each do
|a,b,c|
puts( "#{a}, #{b}, #{c}" )
end
# curly braces {..}
[[1,2,3],[3,4,5],[6,7,8]].each{
|a,b,c|
puts( "#{a}, #{b}, #{c}" )
}
不论你界定哪个块,你必须确认开放界定符'{'或者'do'必须是和 each 方法在同一行。在
each 方法和开放块界定符之间插入行是一个语法错误。
While 循环
Ruby 也有一些其他的循环结构。这是一个 while 循环:
while tired
sleep
end
或者,可以这样写:
sleep while tired
即使这两个例子的语法不一样,但是它们执行的效果是一样的。在第一个例子中,while 和
end 之间的代码(调用一个名为 sleep 的方法)将一直执行,只要 Boolean 条件(这里该值由
一个名为 tired 方法返回)一直为 true。和 for 循环一样,当判断语句和循环体需要执行代码
分别的单独行的时候 do 关键字可以省略;当条件判断和执行代码在同一行时,必须显式书
写 do 关键字。
While 变体
在第二个循环版本中(sleep while tired),执行代码(sleep)位于条件判断(while tired)之前。
6.
这种语法成为”while modifier”。如果你想使用这个语法执行多个表达式,你可以将这些
表达式写到 begin和 end 关键字之间。
begin
sleep
snore
end while tired
1loops.rb
这个例子描述了这种语法的众多替代方式:
$hours_asleep = 0
def tired
if $hours_asleep >= 8 then
$hours_asleep = 0
return false
else
$hours_asleep += 1
return true
end
end
def snore
puts('snore....')
end
def sleep
puts("z" * $hours_asleep )
end
7.
while tired dosleep end # a single-line while loop
while tired # a multi-line while loop
sleep
end
sleep while tired # single-line while modifier
begin
sleep
snore
end while tired
上面的最后一个例子(那个多行的 while 变体)需要着重强调一下,因为它将引入一些很重要
的特性。当一个由 begin 和 end 界定的代码块在 while 条件判断之前,那么这段代码将至
少执行一次。在一些其他的 while 循环类型中,这些代码将永远不会执行除非布尔条件的值
为 true。
确保一个循环至少执行一次
通常一个 while 循环执行 0 次或者多次,因为布尔判断总是在循环执行之前进行的;如果布
尔判断在外面返回 false ,那么在循环内部的代码将永远不会被执行。
然而当我们将 while 测试放到一个由 begin 和 end 包含的代码段之后,这个循环中的代码
就将至少执行一次
2loops.rb
为了突出这两种 while 循环的区别,运行一下 2loops.rb 就可以看出来了。
这些例子应该有助于更为清楚地描述该区别:
x = 100
# The code in this loop never runs
while (x < 100) do puts('x < 100') end
# The code in this loop never runs
puts('x < 100') while (x < 100)
# But the code in loop runs once
begin puts('x < 100') end while (x < 100)
8.
Until 循环
Ruby 还提供了一个Until 循环,可以看作是一个”while not”循环。它的语法和基本特征
和 while 循环相似,条件判断语句和循环体内代码可以写在同一行(此时 do 关键字是必须的)
或者将其分别写到单独的行(此时 do 是可选的)。
同样也有一个 until 变体,可以让你将代码方在条件测试语句之前,并且可以使用 begin 和
end 来界定代码,以便确保该代码至少会运行一次。
until.rb
这里有些 until 循环的例子:
i = 10
until i == 10 do puts(i) end # never executes
until i == 10 # never executes
puts(i)
i += 1
end
puts(i) until i == 10 # never executes
begin # executes once
puts(i)
end until i == 10
While 和 until 循环都像一个 for 循环,可以用来迭代数组和其他集合。例如,这是迭代数
组中所有元素的一个例子:
while i < arr.length
puts(arr[i])
i += 1
9.
end
untili == arr.length
puts(arr[i])
i +=1
end
循环
3loops.rb
在 3loops.rb 中的例子看起来应该很熟悉——除最后一个之外:
loop {
puts(arr[i])
i+=1
if (i == arr.length) then
break
end
}
这个例子使用 loop 方法来重复执行一个由花括号括起来的代码段。这就像我们之前使用
each 方法来执行块循环一般。又一次,我们可以选择块的界定符———花括号或者 do 和
end 关键字:
puts( "nloop" )
i=0
loop do
puts(arr[i])
i+=1
if (i == arr.length) then
break
end
10.
end
这段代码通过一个计数变量 i 来迭代数组arr,当(i==arr.length)为 true 时跳出循环。你必
须使用这种方式来跳出循环,不像 while 或者 until,loop 方法没有通过一个条件判断的值
来判断是否要继续循环。如果没有 break 关键字,它将一直循环下去。
深入探讨
哈希,数组,区间和集合都包含一个 Ruby 模块——Enumerable。一个模块就是一种代码
库(我将在第十二章详细介绍模块)。在第四章中,我使用了 Comparable 模块来给数组添加
一个比较方法,如<和>。你应该记起来了,我通过定义一个 Array 类的子类并包含
Comparable 模块来实现的:
class Array2 < Array
include Comparable
end
Enumerable 模块
enum.rb
Enumerable 模块已经包含在 Ruby 的 Array 类中,它为 Array 类提供了很多有用的方法如
include?方法,该方法当某指定值包含于该数组中返回 true,min 方法用于返回数组中最小
值,max 返回数组中最大值,collect 创建一个新数组,该数组由一个代码段返回值组成:
arr = [1,2,3,4,5]
y = arr.collect{ |i| i } #=> y = [1, 2, 3, 4, 5]
z = arr.collect{ |i| i * i } #=> z = [1, 4, 9, 16, 25]
arr.include?( 3 ) #=> true
arr.include?( 6 ) #=> false
arr.min #=> 1
arr.max #=> 5
enum2.rb
这些方法在其他的包含了 Enumerable 模块的类中同样有效。Hash 就是其一。记住,然而
Hash 中的项不是顺序索引的,所以当你使用 min 和 max 方法时,这些方法返回的最小和
最大项是按照它们的字面值大小来计算的——当项为字符串时,字面值是由 key 中字符的