在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
“循环”会用在程序中的各种地方。而在循环的地方善用“迭代器”,则是熟练进行Ruby程序设计的重要关键。 不过,迭代器确实有比较抽象的地方,语法也有点怪异(尤其是yield的用法),光是依靠文字说明、看一两个示例,还是不太好懂。其实,当初笔者开始学习Ruby时就卡在这里,很久都搞不懂。 所以在本章中,将通过很多的示例,来慢慢探索Ruby的迭代器。 20.1 迭代器与区块调用首先来整理一下用词和术语。在Ruby中说到“迭代器”,通常可能有两种意义。 所谓的“迭代器”,本来是指在反复(iterate)的处理中,用来控制反复方式这个“功能”。而在Ruby语言里,迭代器也表示实现这个功能的方法,或者这种方法的接口。 然而Ruby的迭代器接口,有时候又被用在循环以外的地方,例如,让使用者自定义方法一部分的功能,这个用途已经很难称作是迭代器了。 名称与功能不一致,是导致容易搞混的原因。所以又有人称这些使用迭代器的语法但内容并没有进行反复处理的东西叫做“区块”或“代码区块”。但是这又很容易与while等语句的do~end部分那种程序语言规范上的区块搞混,因此这种用词还是不太好。 所以在这一章中,无论是不是迭代器,凡是“迭代器”的语法接口,都称为“区块调用”。在这里希望读者注意的是,说“迭代器”的时候,表示一定与反复处理有关;而说“区块调用”的时候,就可能不一定与反复处理有关系了。
20.2 迭代器的基础知识迭代器是抽象化的“反复处理”。但抽象思考毕竟不容易,所以让我们从比较具体的反复处理来思考吧。请看程序20.1。 程序20.1 print_times.rb 5.times { print "<br>/n" } 这与单纯调用5次print方法意思是一样的。也就是说,一样的程序可以写成程序20.2这样。 程序20.2 print_no_times.rb print "<br>/n" print "<br>/n" print "<br>/n" print "<br>/n" print "<br>/n" 当然,说是“反复处理”,也不尽然是一模一样的动作。 请看下面的示例(程序20.3),用来计算数值1到5的和。 程序20.3 sum_each.rb sum = 0 (1..5).each{|i| sum += i } print "合计: ",sum,"/n" > ruby sum_each.rb 合计: 15 这与下面的程序20.4的意思是一样的。 程序20.4 sum_no_each.rb sum = 0 sum += 1 sum += 2 sum += 3 sum += 4 sum += 5 print "合计: ",sum,"/n" 请注意:与程序20.3比较可知,每一圈中加数都在改变。 这种反复处理的动作也经常会用到数组。下面的示例能够依序显示数组的元素(程序20.5)。 程序20.5 print_fruit.rb fruits = ['苹果','香蕉','凤梨'] fruits.each{|elem| print elem,"/n" } > ruby print_fruit.rb 苹果 香蕉 凤梨 这也是反复调用print方法,但每一圈中print方法的实参都是不同的字符串。 上面举了3个例子,从这些例子中可以归纳出循环的语法具有下面这些特性。 — 具有想要“反复处理”的部分; — 每一次的处理中具有不同的值。 其中,“反复处理”的部分就像程序20.1、程序20.5的print方法,在程序20.3中则是加法。而“每圈中不同的值”在程序20.1这个示例里没有,在程序20.3里是“加数”,而在程序20.5里则是“要显示的元素”(变量elem)。 那么,下面就要介绍一下区块调用的典型写法。 obj.method(arg1, arg2, ..., argn) { |变量行| 想要反复处理的部分 } 或者 obj.method(arg1, arg2, ..., argn) do |变量行| 想要反复处理的部分 end 就像这样,区块调用算是一种特殊形式的方法调用方式,特殊的地方当然就是后面的“{~}”或“do ~ end”这个“区块”的部分。而迭代器就是执行这个区块的部分。 在区块的前面,提供了一个用来摆“变量行”的地方,这相当于前面所说的“每圈中不同的值”的部分。也就是说,从这些变量里,可以在循环的每一圈中获取不同的值。这个变量又称为区块变量。 在程序20.1所介绍的Integer#times方法中,省略了区块变量。区块变量是可以省略的。
20.3 各式各样的迭代器首先要介绍的是最基本的迭代器,也就是each方法。这个方法的功能是“依序获取元素,并使用这个元素进行一些处理”。Ruby的很多类都定义了这个方法。 20.3.1 用在数组上数组的每个元素都有索引,所以可以依照索引的顺序取出所有的数据。 alphabet = ["a", "b", "c", "d", "e"] alphabet.each{|i| print i, "/n" } 20.3.2 用在杂凑上杂凑也可以像数组一样逐项取出元素,但与数组不同的是,杂凑的特征是每一圈都会取出键与值两个变量。不过,杂凑的顺序并不固定(译者注),无法得知会以什么顺序取出数据。不过至少可以像程序20.6这样,先取出所有的键与值对。 杂凑的顺序并不是字母顺序或赋值的顺序。将新的值存入杂凑,并不一定会插入最后一个,但每次使用迭代器反复时都会以一定的顺序获取,只是这个顺序对我们而言没有意义。 程序20.6 hash_each.rb sum = 0 outcome = {"参加费"=>1000, "名牌费"=>1000,"联欢会会费"=>4000} outcome.each{|item, price| sum += price } print "合计: ",sum,"/n" 另外,杂凑的each方法,如果只写一个区块变量时,则区块变量会存入一个“[键, 值]”这样的数组中。例如,程序20.6可以改写成程序20.7这样。 程序20.7 hash_each2.rb sum = 0 outcome = {"参加费"=>1000, "名牌费"=>1000," 联欢会会费"=>4000} outcome.each{|pair| sum += pair[1] # 读取“值” } print "合计: ",sum,"/n" 程序20.7使用了pair[1]读取杂凑的值,要读取杂凑的键时则写成pair[0]。 20.3.3 用在文件上文件对象也可以像数组、杂凑一样,依序“取出”数据来使用。 不过,文件对象实际上是“读/写数据的出入口”,数据本身并不在这个对象里。 所以从文件对象读取的数据,看成“从文件读出的数据”比较正确。File类把文本文件的一行当作读取的基本单位。 现在就来看看File类each方法的示例。下面的示例可以从“sample.txt”里依序获取每一行的数据。 f = File.open("sample.txt") f.each{|line| print line } f.close 接下来介绍File.open方法这个没有进行反复处理的区块调用的典型方法。若对File.open方法传递区块,则会将建立的文件对象作为区块变量,调用一次区块。例如,上面的示例可以改写成: f = File.open("sample.txt") {|f| f.each{|line| print line } } 与前面的示例比起来,从文件f读取数据的部分虽然都一样,但不用去调用close。对File.open方法传递区块时,文件会自动在离开区块之后关闭,所以没有必要自己执行关闭的动作。 像文件使用后要关闭这类固定的动作,让使用者把相关的动作都写在区块里面,而方法自己负责后续的善后动作,是一个不错的方法。这样不只可以减少使用者需要输入的代码量(关闭文件的动作),更重要的是可以避免忘记善后处理的错误发生。
20.4 Enumerable模块读入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 运行出来的效果是一样的。 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论