下面,我们要在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" } |
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, " " } |
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 } |
1 3 5 |
另一个是collect,它跟each类似,它将集合中的元素传递给一个块,在块中处理后返回一个包含处理结果的新数组。
["H", "A", "L"].collect { |x| x.succ } |
» | ["I", "B", "M"] |
这值得我们再花一些时间来比较一下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 |
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 |
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 |