第五章

循环和迭代
大部分的程序设计中都会涉及到重复。也许你想你的程序连续发出十次”哔”的声音,或者
从一个文件中读入多行文本并将其打印出来,除非用户输入按键打印一个警告。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
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
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 循环中使用了多个循环变量。我们通过这种方式来
迭代一个多维数组。在每一个 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,而使用花括号{..},如下:
# 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)之前。
这种语法成为”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
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)
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
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
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 中字符的
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']
如果一个哈希 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'])
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 方法将使用哪个值。

第五章

  • 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.
    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 中字符的
  • 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.
    如果一个哈希 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.
    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 方法将使用哪个值。