块和迭代器,事务处理

下面,我们要在SongList中修改[ ] 方法,使它能接受一个字符串参数,返回以此为标题的歌曲的。看起来我们很容易可以实现:我们有一个包含了很多Song对象的对象的数组,我们只需循环遍历整个数组,找到匹配的那个就可以了。

class SongList
  def [](key)
    if key.kind_of?(Integer)
      return @songs[key]
    else
      for i in [email protected]
        return @songs[i] if key == @songs[i].name
      end
    end
    return nil
  end
end

这样已经可以工作了,而且看上去很符合常规:用一个for循环来遍历数组。

有没有更自然的方法呢?

当然有,我们可以用Array的find方法。

class SongList
  def [](key)
    if key.kind_of?(Integer)
      result = @songs[key]
    else
      result = @songs.find { |aSong| key == aSong.name }
    end
    return result
  end
end

我们可以用if修饰符来使代码更简短一些。

class SongList
  def [](key)
    return @songs[key] if key.kind_of?(Integer)
    return @songs.find { |aSong| aSong.name == key }
  end
end

方法find就是一个迭代器,这个方法重复不断地执行一个给定的block。块和迭代器都是Ruby中比较有趣的特点。我们后面会进一步来讨论这些特点。

实现迭代器

一个Ruby迭代器就是一个简单的能接收代码块的方法。第一眼看上去,Ruby中的block像C,Java,Perl中的一样,但是实际上是有不同的。

首先,块在源代码中紧挨着方法调用,并且和这个方法的最后一个参数写在同一行上。其次,这个块不会立即被执行,Ruby首先会记住这个块出现的上下文(局部变量,当前对象等),然后进入方法,这里也是魔术开始的地方。

在方法里面,这个块才会用yield来调用执行,就像这个块是方法本身一样,每当yield在方法中被执行,这个块就会被调用。当这个块执行完退出后,控制将交给yield后面的语句(yield来自一个有20多年历史的语言:CLU)。我们来看一个小例子。

def threeTimes
  yield
  yield
  yield
end
threeTimes { puts "Hello" }
produces:
Hello
Hello
Hello

这个块(用两个大括号定义)赋给了一个方法threeTimes,在这个方法里面,yield执行了3次,每次执行它都会调用给定的block,即打印一个欢迎语句。使块变得有趣的是你可以给块传递参数,并且从块中得到结果。下面例子,我们将会得到小于一个指定值得Fibonacci 数列。

def fibUpTo(max)
  i1, i2 = 1, 1        # parallel assignment
  while i1 <= max
    yield i1
    i1, i2 = i2, i1+i2
  end
end
fibUpTo(1000) { |f| print f, " " }
produces:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

在这个例子中,yield接收一个参数,这个参数将会在执行的时候传递给指定的块。在块的定义中,参数用两个竖线括起来,放在最前面。在这个例子中f用来接收yield传递的参数,所以,这个块才能打印这个序列。一个块可以接受任意个参数。如果一个块的参数和yield中传递的参数个数不一样,将会怎样呢?很巧合,这和我们在并行赋值(parallel assignment)中谈到的原则一样(如果一个block只接收一个参数,而yield提供的参数多于1个,那么这些参数将被转化为一个数组。)

传递给一个块的参数可以是存在地局部变量,如果是这样的话,那么这个局部变量的新值(如果在块中被修改了)在块退出后将会保留,这可能会有一定的副作用,但是这样做有一个性能方面的考率。

一个块也可以返回一个结果给调用它的方法。这个块中的最后一个表达式的值将会返回给方法,Array中的find方法就是这样工作的。(find在Enumerable中定义,被插入到了类Array

class Array
  def find
    for i in 0...size
      value = self[i]
      return value if yield(value)
    end
    return nil
  end
end
[1, 3, 5, 7, 9].find {|v| v*v > 30 } » 7

这个用法中数组将连续的元素传递给指定的块,如果这个块返回true,则这个方法返回当前对应的元素值,如果没有符合的值,则返回nil。这个方法显示了迭代器的好处,Array类只作自己应该做的,访问数组元素,而应用代码只关注于特殊的需求。

Ruby中的集合对象中也包含其它一些常用迭代器,其中之二是each和collect。each可以认为是最简单的迭代器,它们都会对集合的每个元素来调用块。

[ 1, 3, 5 ].each { |i| puts i }
produces:
1
3
5

另一个是collect,它跟each类似,它将集合中的元素传递给一个块,在块中处理后返回一个包含处理结果的新数组。

["H", "A", "L"].collect { |x| x.succ } » ["I", "B", "M"]

Ruby 和 C++ 、 Java的比较

这值得我们再花一些时间来比较一下Ruby,C++,JAVA中的迭代器。在Ruby中,迭代器是一个简单的方法,每当它产生一个新值,都会调用yield方法。使用迭代器只需要给这个迭代器传递一个块,不需要像C++,Java中那样创建辅助类来处理迭代器的状态。从这一点和其它一些特点来说,Ruby是一种透明语言,当你写程序的时候,你只需关注于让功能能够实现,而不必编写脚手架来支持语言的一些功能。

迭代器不仅仅用在数组和哈希等集合结构上,它也能返回上面Fibonacci 例子中那样的序列值,Ruby中的输入输出类也用到了迭代器,这些类实现了迭代器接口,每次返回一个I/O流的下一行。

f = File.open("testfile")
f.each do |line|
  print line
end
f.close
produces:
This is line one
This is line two
This is line three
And so on...

让我们再看看另外一个迭代器实现。Smalltalk也支持迭代器,如果你用smalltalk语言来计算一个数组中元素的和,可以这样:

sumOfValues              "Smalltalk method"
    ^self values
          inject: 0
          into: [ :sum :element | sum + element value]

inject 这样工作:第一次指定的block被执行的时候,sum被设成inject的参数(本例为0),element被设为数组的第一个元素。以后block被执行的时候,sum的值设为上次block执行后返回的值。这样,sum就可以记录总数了最后的inject的值是block最后一次执行后返回的值。

Ruby 没有inject 方法,但我们可以很容易的写一个,在这个例子中我们把它加入Array类。

class Array
  def inject(n)
     each { |value| n = yield(n, value) }
     n
  end
  def sum
    inject(0) { |n, value| n + value }
  end
  def product
    inject(1) { |n, value| n * value }
  end
end
[ 1, 2, 3, 4, 5 ].sum » 15
[ 1, 2, 3, 4, 5 ].product » 120

尽管block经常是用在迭代器中,但是,块也有其它一些有用的用处。

在事务处理中使用块

block也可以用作定义一块代码,这些代码必须在一定的事务控制下运行。比如,你经常会打开一个文件,对内容作一些处理,然后需要确保文件在最后会被关闭。尽管我们可以用常规的方法实现,但是,我们可以让文件对象自己负责关闭它。一个简单例子如下(忽略了错误处理等):

class File
  def File.openAndProcess(*args)
    f = File.open(*args)
    yield f
    f.close()
  end
end


File.openAndProcess("testfile", "r") do |aFile|  
  print while aFile.gets  
end
   
produces:
This is line one
This is line two
This is line three
And so on...

这个小例子阐述了几个技术点。方法openAndProcess 是一个类方法,这个方法可以独立于任何File对象来单独调用,即不需要生成类的实例。我们在这个方法的参数列表中使用了 *args,这表示所有调用时候的参数将作为数组传递到这个方法。而这个参数是传递给File.open方法的。

一旦这个文件被打开,openAndProcess 将调用yield,然后将打开的文件对象传递给这个block。当block返回后,这个文件将被关闭。这种情况下,关闭文件的任务就从使用文件对象的用户转变为文件本身了。

最后,这个例子用do..end来定义一个块,这和用两个打括号定义一个块只是有优先级别的区别,将在后面讨论。

让文件自己管理自己的生命周期十分有用,Ruby自带的File类就提供了这样的支持。如果File.open调用时指定了一个block,那么这个块就会用传递过来文件对象作为参数执行,然后,当块结束后,这个文件会被文件对象关闭。这很有趣,也就是说File.open方法有两个版本,一个接受block,一个不接受block。当没有指定block调用这个方法的时候,这个方法只会返回一个打开的文件对象。方法Kernel::block_given?提供了实现这种功能的可能,如果调用的时候给了一个block,这个方法将返回true,这样,你也可以实现自己open方法:

class File
  def File.myOpen(*args)
    aFile = File.new(*args)
    # If there's a block, pass in the file and close
    # the file when it returns
    if block_given?
      yield aFile
      aFile.close
      aFile = nil
    end
    return aFile
  end
end

本文摘自:http://www.ruby-cn.org/book/ProgrammingRuby/tut_containers.html

你可能感兴趣的:(java,Integer,Ruby,语言,each,smalltalk)