Ruby-代码块.迭代器

来自站长百科
跳转至: 导航、​ 搜索

导航: 上一页 | ASP | PHP | JSP | HTML | CSS | XHTML | aJAX | Ruby | JAVA | XML | Python | ColdFusion

使用 Blocks 做循环抽象[ ]

Bill Venners:
Ruby 支持 blocks 和 Closure 结构. 什么是 blocks 和 Closure,他们如何使用?

Yukihiro Matsumoto:
Blocks 基本上就是匿名函数。你可能熟悉诸如Lisp 或 Python等其他语言中的Lambda 函数。 你可以向另外一个函数传递一个匿名函数,这个函数可以调用这个被传递过来的匿名函数。例如, 函数可以通过一次传递给匿名函数一个元素来执行循环迭代。在那些可以将函数当作第一类型的编程语言中,这是个通常的方式,称为高排序函数样式。 Lisp 可以这样,Python 也是如此,甚至就连C 也可以通过函数指针实现这点。很多其他语言也可以做这样的编程。在 Ruby中,不同之处只是在高排序函数语法风格上有所不同。在其他语言中,你必须显示的指出一个函数可以接受另外一个函数作为参数。但是在Ruby 中,任何方法都可以 Block 作为一个隐性参数被调用。在方法中,你可以使用 yield关键字和一个值来调用 block.

Bill Venners:
Block 的好处是什么?

Yukihiro Matsumoto:
基本上,Block 是被设计来做循环迭代抽象的。Block 最基本的使用就是让你以自己的方式定义如何循环迭代。

例如,如果你有一个列表,序列,矢量组或者数组,你可以通过使用标准库中提供的方法来实现向前循环迭代,但是如果你想从后往前实现循环迭代呢?如果使用 C 语言,你得先设置四件事情:一个索引,一个起始值,一个结束条件和一个递增变量。这种方式不好,因为它暴露了列表的内部实现方法,我们希望能够隐藏内部逻辑,通过使用 Block 我们可以将内部循环迭代的方式隐藏在一个方法或者函数中。比如,调用list.reverse_each,你可以对一个列表实现一个反向的循环迭代,而不需要知道列表内部是如何实现的。


Bill Venner

就是说,我传递一个 Block 结构,这个 Block 中的代码可对循环迭代中每个元素做任何事情,至于如何反向遍历就取决于List 本身了。换句话说,我就是把原本在 C 语言 Loop 循环中写的那些代码作为一个 Block 来传递。


Yukihiro Matsumoto

对,这意味着你可以定义许多迭代的方式。你可以提供一种向前循环迭代的方式,一种向后循环迭代的方式,等等。这全取决于你了。C#也有迭代器,但是它对于每个类只有一个迭代器。在 Ruby 中你可以拥有任意数量的迭代器。例如,如果你有一个 Tree 类,可以让人以深度优先或者广度优先的方式遍历,你可以通过提供两种不同的方法来提供两种遍历方式。


Bill Venners

让我想想是否我了解了这点,在 Java 中,它们是通过 Iterator 接口实现抽象迭代的,例如,调用程序可以让 Collection 来实现 Iterator。但是调用程序必须使用循环来遍历Iterator 返回的元素。在 For 循环中, 我的代码实现对每个循环迭代的元素的处理,这样循环语句将总是显示在调用程序中。 使用 Block , 我并不调用一个方法来获取一个迭代器,我只是调用一个方法,同时将我希望对循环迭代中每个要处理的元素的处理代码作为一个 Block 块结构传递给该函数。 Block 的好处是不是将一些代码从调用程序中的 for 循环中提取出来。


Yukihiro Matsumoto
实现循环迭代的具体细节应该属于提供这个功能的类。调用程序应该尽可能的少知道这些。这就是 Block 结构的本来目的。实际上,在早期版本的 Ruby 中,使用 Block 的方法被称为迭代器,因为它们就是被设计来实现循环迭代的。但是在 Ruby发展过程中,Block的用途在后来已经得到了很大的增强,从最初的循环抽象到任何事情。

Bill Venners:
例如。。。。

Yukihiro Matsumoto:
我们可以从Block 中创建一个 Closure 对象,一个 Closure 对象就是像 Lisp中实现的那种匿名函数。 你可以向任何方法传递一个匿名函数(即 Closure)来自定义方法的行为。另外举个例子,如果你有一个排序的方法用于排序数组或者列表,你可以定义一个 Block来定义如何在元素之间进行比较,这不是循环迭代。这不是个循环,但是它使用了 Block 。


使用Closures[ ]

Bill Venners:
什么使得 Block 成为了一个 Closure?

Yukihiro Matsumoto:
Closure 对象包含可以运行的代码,是可执行的,代码包含状态,执行范围。也就是说在Closure 中你捕捉到运行环境,即局部变量。因此,你可以在一个Closure中引用局部变量,即是在函数已经返回之后,他的执行范围已经销毁掉,局部变量依然作为一部分存在于Closure对象中,当没有任何对象引用它的时候,垃圾搜集器将处理它,局部变量将消失。

Bill Venners:
这么说,局部变量基本上是被方法和Closure 对象共享的?如果 Closure 对象更新了变量,方法可以看到,如果方法更新了变量,Cosure 对象也可以看到。

Yukihiro Matsumoto:
是的,局部变量在Closure 和方法之间共享,这是真正的 Closure,它不仅仅是复制。

Bill Venners:
一个真正的 Closure 有什么好处?一旦我将一个 Block 变为一个 Closure,我能用它做什么?

Yukihiro Matsumoto:
你可以将一个 Closure 转换为一个 Block,所以 Closure 可以被用在任何Block可以使用的地方。通常,Closure 用来将一个 Block的状态保存在一个实例变量中,因为一旦你将一个 Block 转换为一个 Closure, 它就是一个通过变量可以引用的对象了。当然Closure 也可以像其他语言中那样使用,例如传递给对象以实现对方法行为的定义。如果你希望传递一些代码来自定义一个方法,你当然可以传递给它一个Block. 但是如果你想将同样的代码传递给两个方法(当然这是非常少见的情况),但是如果你确实想这么做,你可以将一个 Block 转换为一个 Closure ,将同一个Closure传递给多个方法。

Bill Venners:
原来如此,但是获取上下文环境有什么好处呢?真正让Ruby 的 Closure不同的是它捕捉运行时间的上下文环境,局部变量等等。那么到底拥有上下文环境有什么好处是我们无法通过传递给对象一个代码块所获得的呢?

Yukihiro Matsumoto:
实际上,说实在的,最主要的原因是向 Lisp 语言表达敬意,Lisp提供了真正的Closure 结构,所以我希望继续提供这个功能。

Bill Venners:
我看到的一个不同之处是: 数据在Closure 对象和方法之间共享。我想我可以在一个常规的非 Closure 结构的 Block 中放入任何需要的环境数据作为参数来传递,但是 Block 仅仅是对环境数据的一份复制,并不是真正的 Closure.它并没有共享环境数据。共享是Closure 和普通的传统函数对象不同的地方。

Yukihiro Matsumoto:
是的,共享允许你做一些有趣的代码演示,但是我觉得它对于程序员的日常工作并没有想象的那么有用。这没什么太大的关系,例如像 Java 的内部类那样的普通复制,在许多场合都在使用。但是通过Ruby 的Clousure 结构,我希望表达我对 Lisp 文化的致意。



实现迭代器[ ]

一个Ruby迭代器不过是一个简单的方法,它可以调用代码块。初看Ruby的代码块很像是C、Java或者Perl中的代码块,不幸的是,不是这样----Ruby的代码块是一种组合语句的途径但不是一种方便的途径。

首先,一个代码块出现在一个方法调用的代码附近,代码块和方法的最后一个参数处在同一行;第二,代码块中的代码并不被执行,而是Ruby保存代码块出现时的上下文关系(局部变量、当前对象等等),然后进入到方法中。这正是魅力所在。
在方法中,代码块通过yield语句被调用,这使得代码块就好像是一个方法一样。当yield执行时,它调用代码块中的代码。代码块退出时,控制就马上被返回给yield后面的语句。[程序设计语言的粉丝会很高兴看到yield关键字被采用在这里,它模拟了Liskov的CLU语言中的yield功能,这是一个有着20年历史的语言,仍然保留着远没有被非CLU语言所广泛采用的许多特性。我们看一个简单的例子:

def threeTimes
yield
yield
yield
end
threeTimes { puts "Hello" }
结果:
Hello
Hello
Hello


(译者注:这里的代码块是puts "Hello"这条语句,而方法是threeTimes,当语句执行到threeTimes { puts "Hello"}这行时,puts "Hello"不是马上被执行,而是由Ruby先保存puts "Hello"这条语句和threeTimes的关系,然后进入到threeTimes中,遇到第一条yield语句的时候调用并执行puts "Hello"语句,执行完毕后返回到第一条yield语句的后面也就是第二条yield语句,直到三条yield语句都被执行了,才返回到threeTimes{puts "Hello"}这条语句后)

代码块(大括号括住的部分)和threeTimes方法的调用相关联,在这个方法中,yield被调用了三次,每次它都调用代码块中的代码,然后打出一个愉快的问候。不过,代码块最有趣的地方是你可以给它传递参数还可以取得它的返回值。比如我们写一个简单的函数来返回一个特定值的菲波纳奇数列。[菲波纳奇数列是一个整数序列,以两个1开始,随后的每一项都是它前面两项之和,这个序列常常用在排序算法和分析自然现象当中。]

def fibUpTo(max)
i1, i2 = 1, 1 # 并行赋值
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传递来的值,所以代码块就连续地显示出数列的数了。(这个例子也展示了并行赋值,75页还有更多介绍。)尽管经常给代码块传递一个参数,但不是必须的,代码块可以有很多参数。如果代码块的参数数目和yield传递来的不一样时该怎么办呢?这和并行赋值下的规则是一致的(些小的差别是:如果代码块只有一个参数那么yield传递来的多个参数会转换成一个数组)。

代码块的参数也可能是已存在的局部变量,如果是这样的话,代码块执行完毕后变量的新值会被保留下来。这可能会导致不可预测的结果,但是她也带来一个性能上的优势,变量已经存在了。(关于这方面的更多信息和其它的“gotchas”参看127页,更多的性能信息在128页。)

代码块也可以给方法返回值,在代码块中最后被计算的表达式的值被作为yield的值回传给方法。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集合,我们已经见识过find,另外两个是each和collect,each可能是最简单的迭代器----它所做的就是连续返回集合中的元素。

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


结果:
1
3
5


each迭代器在ruby中有着特殊的地位,在85页我们描述了它怎样用在最基本的for循环中,在102页还会看到怎样定义一个each方法以便在你的类中自由地添加更多的功能。

另一个通用的迭代器是collect,它从集合中取得每一个元素然后传递给代码块。代码块返回的结果用来生成一个新的数组,例如:

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


Ruby与C++和Java的比较[ ]

有必要用一个段落比较一下Ruby在迭代器方面和C++与Java的不同。迭代器就是一个简单的方法,和其他方法一样,当它产生一个新值的时候就调用yield。使用迭代器是很简单的,把一个代码块和方法相关联即可,不需要Java和C++那样生成帮助类来承载迭代器的状态,在这方面,也在其它的很多方面,ruby是一种透明的语言,你写一个ruby程序,只要集中精力于完成工作,而不必搭建用来支持语言本身的脚手架。

迭代器在访问已经存在数据的数组和哈希时没有限制,就像我们在菲波纳奇例子中看到的,一个迭代器能返回传来的值。这种能力被用在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程序员求数组元素的和,他们会像这样来使用inject函数:

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


inject是这样工作的,当关联的代码块第一次被调用时,sum被赋给inject的参数值(在这里是0),element取数组第一个元素。第二次和以后调用到代码块时,sum被赋给上次调用代码块时返回的值,这样sum就跑完了全程,inject最终的结果是代码块最后被调用的值。

ruby没有inject方法,但是很容易实现。这个例子中,我们把它增加到Array类中。在100页我们可以看到如何使它更通用。

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


尽管迭代器经常使用在代码块上,但也可以用在其它方面,我们来看一下。


事务代码块[ ]

代码块可以定义为在某种事务控制下运行的一系列代码,举例来说,你经常要打开一个文件,对文件的内容进行一些处理,然后确保在使用完毕后关闭了它。你可以使用常规的代码来完成这些,不过我们这里要表现的是如何让文件自己负责关闭它自己,我们要用代码块来做。看下面这个粗略的实现(忽略了错误处理):

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对象就可以被调用。我们希望它能像常规的File.open方法那样获得同样的参数,但我们又确实不想关心这些参数具体是什么,所以,我们指定参数为*args,意为“把实参放到数组中传递给方法”,然后我们调用File.open,把*args作为一个参数传递给它。数组被分成单独的参数。来来回回的结果是OpenAndProcess透明地传递它所接收的参数给File.open了。

一旦文件被打开,OpenAndProcess调用yield,把打开的文件对象传递给代码块。当代码块返回后,文件被关闭,这样,关闭的责任就从文件对象的使用者转移给文件本身了。

最后,这个例子使用do...end来定义代码块。使用这种形式和使用大括号的形式之间的区别在于,do...end定义要低级于{...},这在234页会有详细讨论。

这种文件管理自己的生命周期的技巧非常有用,所以Ruby的File类直接就支持了这种特性。如果File.open有一个关联的代码块,那么这个代码块将会随一个文件对象而被调用,到代码块终止时文件对象被关闭。这很有趣,意味着File.open有两种不同的行为。当调用它的时候有代码块,它执行代码块然后关闭文件;如果没有代码块,它返回文件对象。这种特性因为Kernel::block_given?的存在而成为可能,如果当前方法有关联的代码块,返回true。使用它,你可以像下面这样来实现File.open(又一次忽略了错误处理):

class File
def File.myOpen(*args)
aFile = File.new(*args)
# 如果有代码块,传递文件,然后等代码块返回时关闭文件
if block_given?
yield aFile
aFile.close
aFile = nil
end
return aFile
end
end


代码块可以转换为闭包[ ]

让我们回到我们的点唱机,还记得它吗。我们要用一些代码来完成用户界面,比如人们用来选择歌曲和控制点唱机的按钮。我们需要把这些按钮和某些行为联系起来:按下STOP按钮,歌曲停止。Ruby的代码块用来完成这个就最方便不过了。我们先假设人们已经用硬件实现了一个Ruby的扩展,它给了我们一个基本的按钮类(在169页我们讨论扩展Ruby)

bStart = Button.new("Start")
bPause = Button.new("Pause")

  1. ...


当用户按下其中一个按钮时发生了什么?在Button类中,硬件调用一个回调方法,buttonPressed。为这些按钮增加功能的显见的途径就是创建Button类的子类,在每一个子类中实现它们自己的buttonPressed方法。

class StartButton < Button
def initialize
super("Start") # 调用Button的initialize
end
def buttonPressed
# 开始播放...
end
end
bStart = StartButton.new


这里有两个问题,第一个,这会导致出现大量的子类。如果Button的界面改变了,我们需要非常多的维护。第二,按钮按下时执行的操作处在一个错误的级别,它们不应该是按钮的特性,而应该是点唱机的特性,我们用块来修改这些错误。

class JukeboxButton < Button
def initialize(label, &action)
super(label)
@action = action
end
def buttonPressed
@action.call(self)
end
end
bStart = JukeboxButton.new("Start") { songList.start }
bPause = JukeboxButton.new("Pause") { songList.pause }


这里关键是JukeboxButton#initialize的第二个参数。如果一个方法的最后一个参数有&前缀,Ruby就会在方法被调用时查找一个代码块,这个代码块被转变成一个Proc类的对象并且分配成参数。你可以把这个参数当作任意的变量。在我们的例子中,我们把它赋给实例变量@action。当回调方法buttonPressed被调用时,我们使用对象的proc#call方法来调用块。

(译者注:如果你感到不好理解,我来解释一下:看这一句bStart = JukeboxButton.new("Start") { songList.start },当bStart引用的对象实例化时,调用JukeboxButton#initialize,这时{ songList.start }被作为第二个参数传递给JukeboxButton#initialize,在JukeboxButton#initialize中,实例变量@action被赋值成{ songList.start },等到JukeboxButton#buttonPressed被执行时,@action通过proc#call来调用块{ songList.start })

创建一个Proc对象需要很多工作吗?有趣的是,它不过比一堆代码多一点点东西而已。和一个代码块关联的(因此就是一个Proc对象)就是这个代码块被定义时的上下文关系:self的值,方法,变量,常量等的视图,Ruby充满魔力的地方就是代码块可以一直使用所有的这些原始视图信息,即使定义它时的环境已经消失,在其它的语言中,这种能力被称为闭包。

让我们看一个例子,这个例子使用了proc方法,它把一个代码块转换成一个Proc对象。

def nTimes(aThing)  
return proc { |n| aThing * n }
end
p1 = nTimes(23)
p1.call(3) >> 69
p1.call(4) >> 92
p2 = nTimes("Hello ")
p2.call(3) >> "Hello Hello Hello "


nTimes方法返回一个Proc对象,它引用了方法的参数aThing,尽管参数已经不在代码块被调用的时间范围,但仍然保留了对代码块的访问能力。