Ruby的迭代器只不过是可以调用block的方法而已。Ruby的block不是传统意义上的、将语句组织在一起的一种方式。
首先,block在代码中只和方法调用一起出现:block和方法调用的最后一个参数处于同一行,并紧跟在其后(或者参数列表的右括号的后面)。其次,在遇到block的时候并不立刻执行其中的代码。Ruby会记住block出现时的上下文(局部变量、当前对象等)然后执行方法调用。
在方法内部,block可以像方法一样被yield语句调用。每执行一次yield,就会调用block中的代码。当block执行结束时,控制返回到紧随yield之后的那条语句。我们来看个简单的例子.
def three_times yield yield yield end three_times {puts "Hello"}
输出结果:
Hello Hello Hello
block(花括号内的代码)和对方法three_times的调用联合在一起。该方法内部,连续3次调用了yield。每次调用时,都会执行block中的代码,并且打印出一条欢迎信息。更有趣的是,你可以传递参数给block,并获得其返回值。例如,我们可以写个简单的函数返回低于某个值得所有Fibonacci数列项。
def fib_up_to(max) i1,i2=1,1 while i1<=max yield i1 i1,i2 = i2,i1+i2 end end fib_up_to(1000) {|f| print f," "}
输出结果:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
在这个例子中,yield语句带有一个参数。参数值将传递给相关联的block。在block定义中,参数列表位于两个竖线(管道符)之间。在这个例子中,变量f收到yield的参数的值,所以block能够输出数列中的下一个项(这个例子也展示了并行赋值的用法)。尽管通常block只有一个参数,但这不是必然的;block可以有任意数量的参数。
如果传递给block的参数是已存在的局部变量,那么这些变量即为block的参数,它们的值可能会因block的执行而改变。同样的规则适用于block内的变量;如果它们第一次出现在block内,那么它们就是block的局部变量。相反,如果它们先出现在Block外,那么Block就与其外部环境共享这些变量。
下面的例子中,我们看到block从外围环境中继承了变量a和b,而c是block的局部变量(defined?方法在其参数没有定义时返回nil)。
a = [1,2] b ='cat' a.each{|b| c=b*a[1]} a -> [1,2] b -> 2 defined?(c) -> nil
block也可以返回值给方法。block内执行的最后一条表达式的值被作为yield的值返回给方法。这也是Array类的find方法的工作方式。它的实现类似于下面的代码。
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
上面的代码把数组中的元素依次传递给关联的Block。如果block返回真,那么方法返回相应的元素。如果没有元素匹配,方法则返回nil.这个例子展示了这种实现迭代器的方式的优点。数组类处理它擅长的事情。例如访问数组元素,而让应用程序代码集中精力处理特殊需求。
一些迭代器是Ruby的许多收集(collections)类型所共有的。我们已经看了find方法。另外两个是each和collect。each可能是最简单的迭代器,它所做的就是连续访问收集的所有元素。
[1,3,5,7,9].each {|i| puts i}
输出结果:
1 3 5 7 9
另一个常用的迭代器是collect,它从收集中获得各个元素并传递给block。block返回的结果被用来生成一个新的数组,例如:
["H","A","L"].collect {|x| x.succ} ->["I","B","M"]
(备注:.succ方法:返回str的继承者,继承者由最右边的字母或数字递增一个计算出(如果最右边不是字母或数字,那么用最右边字母或字符),递增一个数字通常得到另一数字,同样递增一个字符得到另外一个字符)
让我们再来看一个有用的迭代器。inject方法(定义在Enumerable模块中)让你可以便利收集的所有成员以累计出一个值。例如,使用下面的代码你可以将数组中的所有元素加起来,并获得它们的累加和。
[1,3,5,7].inject(0){|sum,element| sum+element} ->16 [1,3,5,7].inject(1){|product,element| product*element} ->105
inject是这样工作的:block第一次被执行时,sum被置为inject的参数,而element被置为收集的第一个元素。接下来每次执行block时,sum被置为上次block被调用时的返回值。inject的最后结果时最后一次调用block返回的值。还有一个技巧:如果inject没有参数,那么它使用收集的第一个元素作为初始值,并从第二个元素开始迭代。