读入Enumerable模块的类,可以使用很多种迭代器与区块调用。在这里介绍其中的each、collect、sort和sort_by 4个方法。
20.4.1 each方法
each方法是使用Enumerable模块时一定要定义的方法。这个方法里必须定义一个迭代器,用来像前面的数组、杂凑、文件那样,将对象里可以访问的数据完整地逐项读出。
20.4.2 collect方法
collect方法是使用each方法定义出来的。
传入collect方法的区块虽然类似于each方法,但差异在于区块的判断结果最后会存放在数组中返回。
20.4.3 sort方法
sort方法用来排序元素。
所谓的“排序”方法其实有很多种:
— 依照数值大小排序;
— 依照数据长度排序;
— 依照字母顺序排序;
— 依照字母顺序反向排序。
若要结合不同的条件定义不同的排序方法,方法的数量会太多而很难记忆。所以,只定义一个用来排序的方法,而在调用方法的时候可以自己指定排序的方式,似乎是比较好的做法。
这个“指定排序方式”的动作,也是靠区块调用做到的。
例如,假设现在想要排序字符串数组array。若要以字母顺序排列时,则可以写:
sorted = array.sort{|a, b|
a <=> b
}
如果要以字符串长度排序时,则可以写:
sorted = array.sort{|a, b|
a.length <=> b.length
}
前面只是单纯比较a与b这两个字符串,而这里则使用length方法,来比较字符串的长度。
像这样,sort方法会在进行比较时使用区块里的代码。
20.4.4 sort_by方法
sort方法会在每次进行比较时判断元素。让我们来计算一下前面比较字符串长度的示例中,length方法到底被调用了几次。
ary = %w(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20)
num = 0 # 调用的次数
ary.sort {|a, b|
num += 2 # 累加调用的次数
a.length <=> b.length
}
p num #=> 54
这个示例可以知道有20个元素的时候,length方法要调用54次。按理说对每个字符串调用一次length方法再排序就可以了,所以这里做了很多次多余的调用动作。当数组很长,或者判断元素的运算很耗时的时候,这些多余的调用动作会对整体执行时间造成很大的影响。
没有迭代器的区块调用
在Ruby中,存在本身不是循环,与循环也无关,内含被称为区块的方法。
代表的实例有诸如前面介绍的sort方法,在sort方法中,的确存在区块部分,它跟loop这样的循环结构完全没有任何关系。
至于区块,为什么要使用它呢?这是因为,区块传递处理作为方法的参数。
关于“处理传递”,这里有必要说明一下。
通常,方法中传递的东西为“对象”,比如字符串、数值、数组、杂凑,或者是自定义的类的对象,总之这些都是对象。
然而,对于方法来说,在有的场合下,我们希望能够传递“处理”(而不是对象)。例如“排序”这样的处理,基于进行什么样的比较这一前提,得出的排序的结果自然也不一样。因此,我们需要往排序里传递比较的方式。
在这样的问题背景下,对于一些程序设计语言来说,采用的方法是“制作函数或是跟函数操作类似的东西,然后作为参数传递”的做法,基于该种做法的步骤就写作:
— 定义函数(或是类似函数的东西)
— 将函数作为参数,进行方法调用
两个阶段。与上面相比,在
array.sort{|a,b| a ób}
的书写方式中,将函数中的操作部分作为方法参数进行传递。这样一来,方法的表述也就特别简单了。
含区块的方法的结构突显灵活的理由是:对于1个方法来讲,有多少个参数才算合适这样的问题。根据经验,我们认为在有区块的场合下,1个参数会比较好些。这样一来,区块和方法的关联关系看起来会比较容易理解些。
像这种要对所有元素使用相同的运算方式所运算出的结果进行排序时,使用sort_by方法可以节省不少时间。
ary = %w(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20)
ary.sort_by {|item|
item.length
}
sort_by方法会对每个元素先执行一次区块指定的动作,再以这个结果进行排序
20.5 实现迭代器
前面介绍了很多Ruby所提供的标准迭代器,当然自己要定义迭代器也是可以的。
在这里以一个图书列表的类为例,来加以介绍。
首先定义一个代表书籍的Book类(见程序20.8)。在Book类的对象里,存放书名、作者名、领域等数据,分别存放在@title、@author、@genre这些实例变量里,并将这些实例变量设定为可通过访问方法从外部读取与更改。
程序20.8 book.rb
class Book
attr_accessor :title, :author, :genre
def initialize(title, author, genre=nil)
@title = title
@author = author
@genre = genre
end
end
接着来定义BookList类作为图书列表(见程序20.9)。这个类提供了新增书籍、删除书籍、读取列表中的书籍数据等操作。
程序20.9 booklist.rb(尚未支持迭代器)
require 'book'
class BookList
## 初始化
def initialize()
@booklist = Array.new()
end
## 新增书籍
def add(book)
@booklist.push(book)
end
## 返回书籍数量
def length()
@booklist.length()
end
## 将第n本书改成其他书籍
def []=(n,book)
@booklist[n] = book
end
## 返回第n本书
def [](n)
@booklist[n]
end
## 从列表中删除书籍
def delete(book)
@booklist.delete(book)
end
end
程序20.10是一个使用Book类与BookList类的程序示例。
程序20.10 booklist_test.rb
require 'book'
require 'booklist'
# 建立新的图书列表(这时是空的)
booklist = BookList.new()
# 先插入几本书
b1 = Book.new("iPod 玩拆解","三浦一则")
b2 = Book.new("How Objects Work","平泽章")
# 新增书籍
booklist.add(b1)
booklist.add(b2)
# 输出列表里的书
print booklist[0].title, "\n"
print booklist[1].title, "\n"
接下来,依序获取存放在BookList类对象里的每一本书。做法虽然有很多,但这里想要使用“以迭代器获取”的做法。使用起来会是这样:
booklist.each{|book|
print book.title, "\n"
}
就在booklist.rb里定义这个方法吧:
def each
@booklist.each{|book|
yield(book)
}
end
在这里出现了“yield(book)”这行语句,这个yield就是定义迭代器时最重要的关键所在。写在方法定义里的yield,表示调用传递给这个方法的区块。这时,yield若有指定实参,就会分别存进区块变量里。
例如,下面这个print2方法的调用。
obj.print2{|x, y|
print x,"\n"
print y,"\n"
}
如果print2方法的定义如下:
def print2
yield(1,2)
end
则这时区块变量的x与y的值就分别是1与2。
当然,这段定义中print2方法的区块只调用了一次。要定义成迭代器时,应该使用循环之类的方式不断调用yield语句。
回到前面each方法的定义。在这个定义中,调用实例变量@booklist的each方法,获取@booklist内的每个对象,再以这个对象作为实参,调用yield语句。这样一来,就可以对实例变量@booklist里的每个元素,执行一次迭代器的区块了。
接下来要思考的是只获取书名的迭代器。这个迭代器的名称使用each_title似乎不错。使用方法如下:
booklist.each_title{|title|
print title,"\n"
}
前面的each方法是先建立Book对象,再以对象的title方法获取数据。这个each_title方法则可以直接从booklist对象里取出书名。
现在就试着在booklist.rb里定义each_title方法吧。
def each_title
@booklist.each{|book|
yield(book.title)
}
end
与前面的each方法示例不同的地方只有yield的部分。使用each方法时yield的实参是book,传递整个book对象,而这里则是以book.title为实参,所以区块得到的数据就只有书名的字符串了。
下面再设计一个迭代器,不只可以处理书名,而且还传入作者。方法名称就叫做each_title_author吧。用法如下:
booklist.each_title_author{|title, author|
print "「",title,"“" ,author,"\n"
end
与前面示例不同的地方在于它传递给区块两个实参。现在就赶快在booklist.rb里定义这个each_title_author方法吧。
def each_title_author
@booklist.each{|book|
yield(book.title, book.author)
}
end
差异也只在于yield的实参而已。前面只有book.title,这里则有book.title与book.author两个。
就像这个方法所定义的,当yield不只一个实参时,区块就会收到不只一个区块变量。
有实参的迭代器
这里再举一个除了区块以外,也有一般实参的迭代器示例。
例如,想要只获取某个特定作者的书籍列表时,使用each方法,可以这样写:
author_regexp = /高桥/
booklist.each{|book|
if author_regexp =~ book.author
print book.title, "\n"
end
}
当然这样做也不是不行,但若能直接以作者姓名获取适当的迭代器,感觉似乎更方便。例如像这样:
booklist.find_by_author(/高桥/){|book|
print book.title, "\n"
}
把这个find_by_author方法也定义在booklisk.rb里:
def find_by_author(author)
@booklist.each{|book|
if author =~ book.author
yield(book)
end
}
end
以实例变量@booklist的each方法逐项获取book的地方是一样的,差异仅在于在使用yield将book传递给区块的部分是写在if条件式里的。这里会将传递给方法的参数author与书籍的作者姓名,也就是book对象的author互相匹配,只在匹配成功的时候调用yield。
接下来再把这个方法改写成没有指定区块的时候,就返回匹配成功的项构成的数组。让这个方法也可以不需要指定区块。
books = booklist.find_by_author(/高桥/)
books.each{|book|
print book.title,"\n"
}
修改后的find_by_author方法如下所示:
def find_by_author(author)
if block_given?
@booklist.each{|book|
if author =~ book.author
yield(book)
end
}
else ## 区块不存在时
result = []
@booklist.each{|book|
if author =~ book.author
result << book
end
}
return result
end
end
这个方法使用到了block_given?这个方法。这个方法在有传入区块时会返回true;没有传入区块时会返回false。
这一章所定义的BookList类的最后完整版如程序20.11所示。
程序20.11 booklist.rb(完整版)
require 'book'
class BookList
## 初始化
def initialize()
@booklist = Array.new()
end
## 新增书籍
def add(book)
@booklist.push(book)
end
## 返回书籍数量
def length()
@booklist.length()
end
## 将第n本书改成其他书籍
def []=(n,book)
@booklist[n] = book
end
## 返回第n本书
def [](n)
@booklist[n]
end
## 从列表中删除书籍
def delete(book)
@booklist.delete(book)
end
def each
@booklist.each{|book|
yield(book)
}
end
def each_title
@booklist.each{|book|
yield(book.title)
}
end
def each_title_author
@booklist.each{|book|
yield(book.title, book.author)
}
end
def find_by_author(author)
if block_given?
@booklist.each{|book|
if author =~ book.author
yield(book)
end
}
else ## 区块不存在时
result = []
@booklist.each{|book|
if author =~ book.author
result << book
end
}
return result
end
end
end
区块的传递方法
到目前为止的例子中,区块是以方法结尾地方用{~}括起来这样的形式进行传递的。除此之外,区块还可以以Proc对象的方式进行传递。
例如,在方法向方法传递区块的场合,需要通过命名变量来表示传递的区块,该变量前面必须使用“&”方能进行区块的传递。
def each_some(a, b, &block)
#前面的处理
each_some2(&block)
#后面的处理
end
each_some方法调用了each_some2方法,这时传入each_some中区块&block,再次传给了each_some2方法。
对于使用了“&”的参数代表传递的区块的场合,在运行该区块的内容时,需要用call方法进行调用。
def def foo(a, b, &block)
block.call(a, b)
end
这与
def foo(a, b)
yield(a, b)
end
运行出来的效果是一样的。