| |
|
一个只有一首歌曲的点唱机很少见(除非在非常非常特殊的场合),所以不久我们就开始考虑制作一个歌单和播放列表。它们两个都是容器:用来保存对一个或者多个对象的引用的对象。
歌单和播放列表有一些相似的方法:添加一首歌、删除一首歌,返回一个歌曲列表等等。播放列表还应该增加一些其它的功能,比如每次播放时插入广告,记录累计播放时间等。不过我们先不去考虑这些问题,现在最好先创建一个通用的SongList类供歌单和播放列表使用。
容器
在实现以前,我们需要决定怎样在SongList对象中储存歌曲列表,有三种方法,使用Ruby的Array类型或者Hash类型,要不然就自己做一个列表结构。偷懒的结果就是先来看看数组和哈希,从中选择一个用在我们的类中。
数组
Array类保存着对象引用的集合。每一个对象的引用在数组中都有一个位置,通过一个非负整数来索引。
你可以使用字面值或者直接生成一个Array对象,一个字面上的数组是被方括号括住的一个对象序列。
a = [ 3.14159, "pie", 99 ]
a.type >> Array
a.length >> 3
a[0] >> 3.14159
a[1] >> "pie"
a[2] >> 99
a[3] >> nil
b = Array.new
b.type >> Array
b.length >> 0
b[0] = "second"
b[1] = "array"
b >> ["second", "array"] |
数组通过[]操作符来索引,就像大多数的Ruby操作符,这实际上也是一个方法(在Array类中)所以也可以在子类中重载,如例中所示数组索引从0开始。使用一个single整数来索引数组,返回该位置的对象,如果在那个位置没有对象则返回nil。如果使用一个负整数索引数组,那么它从数组尾端返回,参看35页的表4.1。
a = [ 1, 3, 5, 7, 9 ]
a[-1] >> 9
a[-2] >> 7
a[-99] >> nil |
也可以使用一对数字来索引数组,[start,count]。这会返回一个新数组,它由从start开始的count个对象的引用组成。
a = [ 1, 3, 5, 7, 9 ]
a[1, 3] >> [3, 5, 7]
a[3, 1] >> [7]
a[-3, 2] >> [5, 7] |
(译者注:注意负整数索引的方向依然是从前向后)
最后你也可以使用区间来索引数组,开始和结束位置之间插入两个或者三个点,两个点的形式表示包含结束位置,三个点不包含。
a = [ 1, 3, 5, 7, 9 ]
a[1..3] >> [3, 5, 7]
a[1...3] >> [3, 5]
a[3..3] >> [7]
a[-3..-1] >> [5, 7, 9] |
[]操作符对应的有[]=操作符,通过它可以设置数组元素的值。用一个single整数索引,把操作符右边的值赋给该位置的元素。中间所产生的空隙用nil来填补。
a = [ 1, 3, 5, 7, 9 ] >> [1, 3, 5, 7, 9]
a[1] = 'bat' >> [1, "bat", 5, 7, 9]
a[-3] = 'cat' >> [1, "bat", "cat", 7, 9]
a[3] = [ 9, 8 ] >> [1, "bat", "cat", [9, 8], 9]
a[6] = 99 >> [1, "bat", "cat", [9, 8], 9, nil, 99] |
如果[]=的索引有两个数(开始和长度)或者是一个区间,那么在原始数组中的对应元素就会被操作符右边的值代替;如果索引的长度为0,那么右边的值就插入到开始位置前面,不删除元素;如果右边也是一个数组,它的元素代替原始数组的元素;如果索引所选择的元素数目和右边的不一样,那么就自动调整数组的大小来适应。
(译者注:值得一提的是如果出现了空隙,依旧用nil来填补)
a = [ 1, 3, 5, 7, 9 ] >> [1, 3, 5, 7, 9]
a[2, 2] = 'cat' >> [1, 3, "cat", 9]
a[2, 0] = 'dog' >> [1, 3, "dog", "cat", 9]
a[1, 1] = [ 9, 8, 7 ] >> [1, 9, 8, 7, "dog", "cat", 9]
a[0..3] = [] >> ["dog", "cat", 9]
a[5] = 99 >> ["dog", "cat", 9, nil, nil, 99] |
数组有大量有用的方法,通过它们你可以把数阻当成堆、栈、集、队列、双列、先入先出列等。278页有完整的数组方法列表。
哈希
哈希(有时被认为是数组和字典的结合)和数组一样是用来储存对象引用的集合。
不过,区别于通过整数来索引数组,你可以通过任意类型的对象来索引哈希:字符、正则表达式等。在哈希中保存元素实际上是保存了两个对象----键和值。用键可以索引到对应的值。哈希中的值可以是任意类型的对象,下面的例子使用了哈希字面值:括号括起来的键值对。
h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
h.length >> 3
h['dog'] >> "canine"
h['cow'] = 'bovine'
h[12] = 'dodecine'
h['cat'] = 99
h >> {"donkey"=>"asinine", "cow"=>"bovine", "dog"=>"canine", 12=>"dodecine", "cat"=>99} |
对比于数组,哈希有一个显见的好处就是可以使用任意对象做索引,但同时一个显见的不好处就是它的元素是无序的,所以你不能简单地把哈希用作堆栈或者队列。
你很快会发现哈希是ruby中使用最广泛的一类数据结构。317页有完整的哈希类中实现的方法列表。
实现一个SongList容器
上面简单讨论了一下数组和哈希,下面我们来实现点唱机的SongList类。我们先列出在SongList中所需的基本的方法的一个列表,我们希望随着我们的进度不断丰富它,现在先把它做出来。
append( aSong ) >> list
向列表中添加指定的歌曲
deleteFirst() >> aSong
从列表中删除第一首歌曲并返回该歌曲
deleteLast() >> aSong
从列表中删除最后一首歌并返回该歌曲
[anIndex] >> aSong
从列表中返回anIndex所索引的歌曲,可以是整数索引或者歌曲的标题。
(译者注:这里要实现一个操作符方法即[]方法)
这个列表给我们一些实现方法的提示。在列表尾端添加歌曲的功能,在最前和最后位置删除歌曲的功能。建议使用双列----一个两端队列----这样我们可以使用Array来实现,同样,数组也支持用一个整数来索引歌曲。
但是我们也需要使用歌曲标题来索引歌曲,可能会想到使用哈希,那样用标题做键歌曲做值。那么可以使用哈希吗?也许可以,不过这样有问题。首先哈希是无序的,所以我们不得不使用一个辅助的数组来跟踪列表。一个更大的麻烦是哈希不支持多个键对应一个值,这会给我们的播放列表带来麻烦,因为一首歌可能会被播放许多次。我们可以在一个歌曲的数组中搜索需要的歌曲标题,如果这会成为执行上的瓶颈,那么我们会在后面加入一些基于哈希的查找特性。
我们从一个基本的initialize方法开始我们的类,创建一个数组用来存放歌曲和一个引用它的实例变量@songs。
class SongList
def initialize
@songs=Array.new
end
end |
SongList#append方法在@songs数组末尾添加歌曲,返回它自己也就是当前的SongList对象。这是一个有用的特性,可以让我们把多个append调用联接在一起,后面会看到这个例子。
class SongList
def append(aSong)
@songs.push(aSong)
self
end
end |
然后添加deleteFirst和deleteLast方法,简单地用Array#shift和Array#pop来分别实现。
class SongList
def deleteFirst
@songs.shift
end
def deleteLast
@songs.pop
end
end |
让我们来快速地测试一下,在列表中添加四首歌曲。炫耀一下,我们用append返回的SongList对象来联接这些方法调用。
list = SongList.new
list.
append(Song.new('title1', 'artist1', 1)).
append(Song.new('title2', 'artist2', 2)).
append(Song.new('title3', 'artist3', 3)).
append(Song.new('title4', 'artist4', 4)) |
然后检查一下列表的开始和结束位置是否正确,当列表空的时候返回nil。
list.deleteFirst >> Song: title1--artist1 (1)
list.deleteFirst >> Song: title2--artist2 (2)
list.deleteLast >> Song: title4--artist4 (4)
list.deleteLast >> Song: title3--artist3 (3)
list.deleteLast >> nil |
很好,下一个方法是[],通过索引来访问元素。如果索引是整数(在这里我们用Object#kind of?来检查),那么返回该位置的元素。
class SongList
def [](key)
if key.kind_of?(Integer)
@songs[key]
else
# ...
end
end
end |
再来测试一下
list[0] >> Song: title1--artist1 (1)
list[2] >> Song: title3--artist3 (3)
list[9] >> nil |
现在需要添加通过歌曲标题来索引的功能,这要求扫描整个歌曲列表,检查每一首歌曲标题。在这之前,我们需要先来熟悉一下Ruby最简洁的一个特性:迭代器。
代码块和迭代器
(译者注:关于代码块,Ruby的作者在2003年9月的访谈中提及到,参看注3)
下一个问题是实现SongList的[]方法,它用一个字符串来搜索一首歌曲的标题,看起来很简单:我们有一个歌曲的列表,遍历整个列表依次匹配每一首歌曲的标题即可。
class SongList
def [](key)
if key.kind_of?(Integer)
return @songs[key]
else
for i in 0...@songs.length
return @songs[i] if key == @songs[i].name
end
end
return nil
end
end |
它能工作,并且看上去也很熟悉,一个for循环遍历整个数组,能不能做的更自然些呢?
事实上有更自然的方法。这里我们的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方法是一个迭代器,一个重复调用代码块的方法。迭代器和代码块是Ruby最有趣的特性中的两个,所以我们花些时间来研究一下它们(这个过程中我们也可以看到我们的[]方法是如何真正工作的)。
实现迭代器
一个Ruby迭代器不过是一个简单的方法,它可以调用代码块。初看Ruby的代码块很像是C、Java或者Perl中的代码块,不幸的是,不是这样----Ruby的代码块是一种组合语句的途径但不是一种方便的途径。
首先,一个代码块出现在一个方法调用的代码附近,代码块和方法的最后一个参数处在同一行;第二,代码块中的代码并不被执行,而是Ruby保存代码块出现时的上下文关系(局部变量、当前对象等等),然后进入到方法中。这正是魅力所在。
在方法中,代码块通过yield语句被调用,这使得代码块就好像是一个方法一样。当yield执行时,它调用代码块中的代码。代码块退出时,控制就马上被返回给yield后面的语句。[程序设计语言的粉丝会很高兴看到yield关键字被采用在这里,它模拟了Liskov的CLU语言中的yield功能,这是一个有着20年历史的语言,仍然保留着远没有被非CLU语言所广泛采用的许多特性。(注2)]我们看一个简单的例子:
def threeTimes
yield
yield
yield
end
threeTimes { puts "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 } |
结果:
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")
# ... |
当用户按下其中一个按钮时发生了什么?在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,尽管参数已经不在代码块被调用的时间范围,但仍然保留了对代码块的访问能力。
[注2]:
谈到Liskov,想要简单介绍一下OOP的历史:
面向对象技术鼻祖是挪威人克里斯坦.尼加德(Kristen Nygaard),他在1962年发明了颇具传奇色彩的Simula语言,并在该语言中创造出了许多沿用至今的面向对象概念。1970年前后,阿兰.凯(Alan Kay)与他的同事们在施乐(Xerox)公司发明了优雅的、纯粹的Smalltalk语言。Smalltalk提出了许多新概念,如消息和继承机制等。同样在1970年代,芭芭拉.莉丝柯夫(Barbara Liskov)使抽象数据结构的理论和实现获得了重大进展。她在LISP语言的基础上,通过增加面向对象机制,发明了著名的CLU语言,该语言支持隐藏内部数据的设计方法。此外,1980年代初期诞生的Ada语言也为面向对象技术贡献了泛型和包等重要概念。在Ada语言的基础上,格雷迪.布彻(Grady Booch)还首次提出了"面向对象设计"这一现代软件工程术语。
Ruby汲取了许多OOP语言的精髓,所以在这个学习的旅程中,如果碰到了你的熟人,大可开怀一笑,万不可学那小气之人,以为某种语言是自己的独家兵器,别人使不得的样子。
[注2由夏克补充]
[注3]:
在这里我们把Ruby作者关于代码块和闭包的谈话全文粘贴:
使用 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 Venners:
就是说,我传递一个 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 文化的致意。
[注3由夏克补充] |
|
| 版权声明:RUBY文档中心的所有文章标明[原创]的均为本站作品,版权属RUBY中文化计划,若转载请注明;标明[翻译]的其外文版权归原作者,译文版权属RUBY中文化计划;标明[转贴]的,若原作者感到侵犯了他的著作权,那么请及时跟主持人联系,我们会尽快更正。 |
|
|