第五章

953 views

Published on

0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
953
On SlideShare
0
From Embeds
0
Number of Embeds
30
Actions
Shares
0
Downloads
22
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

第五章

  1. 1. 第五章 循环和迭代 大部分的程序设计中都会涉及到重复。也许你想你的程序连续发出十次”哔”的声音,或者 从一个文件中读入多行文本并将其打印出来,除非用户输入按键打印一个警告。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. 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. 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. 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. 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. 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. 7. while tired do sleep 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. 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. 9. end until i == 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. 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 中字符的
  11. 11. ASCII 码决定的。 自定义比较 但是假设你更倾向于让 min 和 max 方法的返回值基于其他的准则(比方说字符串的长度)。 我们最早的实现方式就是在一个代码块中定义一个比较。这种方式和我在第四章中实现的排 序块方式很相似。你也许想起来了我们通过传递一个代码块给 sort 方法来给一个 Hash 排序 (变量 h),如下: h.sort{ |a,b| a.to_s <=> b.to_s } 两个参数 a 和 b,分别代表 Hash 中两个使用<=>进行比较的项。我们可以类似地给 max 和 min 方法传递代码块: h.min{ |a,b| a[0].length <=> b[0].length } h.max{|a,b| a[0].length <=> b[0].length } 一个 Hash 传递项给一个代码块和传递给数组相同,每一个项都是一个键值对。所以,如果 一个 Hash 包含这样的项: {'one'=>'for sorrow', 'two'=>'for joy'} 那么这两个块参数 a 和 b 将被初始化为两个数组: a = ['one','for sorrow'] b = ['two','for joy'] 这就解释了为什么在我定义的 max 和 min 方法中的自定义比较中这两个块只比较两个块参 数索引值 0 处的元素: a[0].length <=> b[0].length 这确保比较是基于 Hash 中键值进行的。 如果你想比较值而不是键,只需要将数组的索引值设为 1: enum3.rb p( h.min{|a,b| a[1].length <=> b[1].length } ) p( h.max{|a,b| a[1].length <=> b[1].length } ) 你当然也可以在你的代码块中定义其他的自定义比较类型。假设你想让'one','two','three'这 等,能按我们平时读的顺序来进行计算。一个方法就是创建一个有序的字符串数组: str_arr = ['one','two','three','four','five','six','seven']
  12. 12. 如果一个哈希 h 包含这些字符串作为键值,那么在代码块中可以使用 str_arr 作为一个参考 来判定最大值和最小值: h.min{|a,b| str_arr.index(a[0]) <=> str_arr.index(b[0])} #=> ["one", "for sorrow"] h.max{|a,b| str_arr.index(a[0]) <=> str_arr.index(b[0])} #=> ["seven", "for a secret never to be told"] 上面所有的示例,都使用了 Array 和 Hash 的 min 和 max 方法。记住,这些方法是由 Enumerable 模块为这些类提供的。 当我们在使用一些其他类并不是继承于那些已经实现了 max,min 和 collect 方法的类(如 Array)时,能将 Enumerable 中的 max,min 和 collect 方法应用于这些类之中将是非常有 用的。你可以通过在你的类中包含 Enumerable 模块,然后编写一个名为 each 的迭代器方 法,如下: inclue_enum1.rb class MyCollection include Enumerable def initialize( someItems ) @items = someItems end def each @items.each{ |i| yield( i ) } end end 你可以使用一个数组来初始化一个 MyCollection 对象,数组将保存在实例变量@items 中。 你可以调用由 Enumerable 模块秘密地提供的方法(如 min,max 或者 collect),调用 each 方 法每次获取 data 中的一项。 现在你可以通过你的 MyCollection 对象使用 Enumerable 的方法: things = MyCollection.new(['x','yz','defgh','ij','klmno'])
  13. 13. p( things.min ) #=> "defgh" p( things.max ) #=> "yz" p( things.collect{ |i| i.upcase } ) #=> ["X", "YZ", "DEFGH", "IJ", "KLMNO"] inclue_enum2.rb 你可以类似地使用你的 MyCollection 类来处理数组,继而处理 Hash 的键和值。当前 min 和 max 方法采用默认的行为来进行比较,那就是基于字面值大小,所以基于 ASCII 码 值'xy'将被视为比'abcd'要大。如果你想做一些其他类型的比较,比方说,通过字符串长度, 那么'abcd'就会被认为大于'xz'——你完全可以重写 min 和 max 方法: inclue_enum3.rb def min @items.to_a.min{|a,b| a.length <=> b.length } end def max @items.to_a.max{|a,b| a.length <=> b.length } end Each 和 Yield... 那么事情上当 Enumerable 模块中的一个方法使用你自己编写的 each 方法时将发生什么 呢?结果是 Enumerable 方法 (min,max,collect 等等 ) 传 递 一个 代 码 块 给 each 方法。这 代 个代码块将一次接受一个数据 ( 即某种集合中的每一项 ) 。你的 each 方法通过 一 个 块 参 数 一 的形式提供该 数 据 , 就 像 这 里 的 参 数 i: 据,就像 里的 def each{ @items.each{ |i| yield( i ) } end 关键字 yield 是 Ruby 中的一点小魔法,它告诉代码运行传递给 each 方法的代码块——也 就是说,运行由 Enumerable 模块提供的 min,max 和 collect 方法。这意味着这些方法的 代码可以被不同的集合使用。你所要做的就是, 1) 包含 Enumerable 模块到你的类中 ;2) 编写一个 each 方法,该方法决定 Enumerable 方法将使用哪个值。

×