ruby 各式各样的迭代器

首先要介绍的是最基本的迭代器,也就是each方法。这个方法的功能是“依序获取元素,并使用这个元素进行一些处理”。Ruby的很多类都定义了这个方法。

用在数组上

数组的每个元素都有索引,所以可以依照索引的顺序取出所有的数据。

用在杂凑上

杂凑也可以像数组一样逐项取出元素,但与数组不同的是,杂凑的特征是每一圈都会取出键与值两个变量。不过,杂凑的顺序并不固定(译者注),无法得知会以什么顺序取出数据。不过至少可以像下面程序一样,先取出所有的键与值对。

杂凑的顺序并不是字母顺序或赋值的顺序。将新的值存入杂凑,并不一定会插入最后一个,但每次使用迭代器反复时都会以一定的顺序获取,只是这个顺序对我们而言没有意义。

另外,杂凑的each方法,如果只写一个区块变量时,则区块变量会存入一个“[键, 值]”这样的数组中。例如,上面程序可以改写成下面程序这样。

使用了pair[1]读取杂凑的值,要读取杂凑的键时则写成pair[0]。

用在文件上

文件对象也可以像数组、杂凑一样,依序“取出”数据来使用。

不过,文件对象实际上是“读/写数据的出入口”,数据本身并不在这个对象里。

所以从文件对象读取的数据,看成“从文件读出的数据”比较正确。File类把文本文件的一行当作读取的基本单位。

现在就来看看File类each方法的示例。下面的示例可以从“sample.txt”里依序获取每一行的数据。

接下来介绍File.open方法这个没有进行反复处理的区块调用的典型方法。若对File.open方法传递区块,则会将建立的文件对象作为区块变量,调用一次区块。例如,上面的示例可以改写成:

与前面的示例比起来,从文件f读取数据的部分虽然都一样,但不用去调用close。对File.open方法传递区块时,文件会自动在离开区块之后关闭,所以没有必要自己执行关闭的动作。

像文件使用后要关闭这类固定的动作,让使用者把相关的动作都写在区块里面,而方法自己负责后续的善后动作,是一个不错的方法。这样不只可以减少使用者需要输入的代码量(关闭文件的动作),更重要的是可以避免忘记善后处理的错误发生。

 

Enumerable模块

读入Enumerable模块的类,可以使用很多种迭代器与区块调用。在这里介绍其中的each、collect、sort和sort_by 4个方法。

each方法

each方法是使用Enumerable模块时一定要定义的方法。这个方法里必须定义一个迭代器,用来像前面的数组、杂凑、文件那样,将对象里可以访问的数据完整地逐项读出。

collect方法

collect方法是使用each方法定义出来的。

传入collect方法的区块虽然类似于each方法,但差异在于区块的判断结果最后会存放在数组中返回。

sort方法

sort方法用来排序元素。

所谓的“排序”方法其实有很多种:

—   依照数值大小排序;

—   依照数据长度排序;

—   依照字母顺序排序;

—   依照字母顺序反向排序。

若要结合不同的条件定义不同的排序方法,方法的数量会太多而很难记忆。所以,只定义一个用来排序的方法,而在调用方法的时候可以自己指定排序的方式,似乎是比较好的做法。

这个“指定排序方式”的动作,也是靠区块调用做到的。

例如,假设现在想要排序字符串数组array。若要以字母顺序排列时,则可以写:

如果要以字符串长度排序时,则可以写:

前面只是单纯比较a与b这两个字符串,而这里则使用length方法,来比较字符串的长度。

像这样,sort方法会在进行比较时使用区块里的代码。

sort_by方法

sort方法会在每次进行比较时判断元素。让我们来计算一下前面比较字符串长度的示例中,length方法到底被调用了几次。

这个示例可以知道有20个元素的时候,length方法要调用54次。按理说对每个字符串调用一次length方法再排序就可以了,所以这里做了很多次多余的调用动作。当数组很长,或者判断元素的运算很耗时的时候,这些多余的调用动作会对整体执行时间造成很大的影响。

 

没有迭代器的区块调用

在Ruby中,存在本身不是循环,与循环也无关,内含被称为区块的方法。

代表的实例有诸如前面介绍的sort方法,在sort方法中,的确存在区块部分,它跟loop这样的循环结构完全没有任何关系。

至于区块,为什么要使用它呢?这是因为,区块传递处理作为方法的参数。

关于“处理传递”,这里有必要说明一下。

通常,方法中传递的东西为“对象”,比如字符串、数值、数组、杂凑,或者是自定义的类的对象,总之这些都是对象。

然而,对于方法来说,在有的场合下,我们希望能够传递“处理”(而不是对象)。例如“排序”这样的处理,基于进行什么样的比较这一前提,得出的排序的结果自然也不一样。因此,我们需要往排序里传递比较的方式。

在这样的问题背景下,对于一些程序设计语言来说,采用的方法是“制作函数或是跟函数操作类似的东西,然后作为参数传递”的做法,基于该种做法的步骤就写作:

—   定义函数(或是类似函数的东西)

—   将函数作为参数,进行方法调用

两个阶段。与上面相比,在

array.sort{|a,b| a ób}

的书写方式中,将函数中的操作部分作为方法参数进行传递。这样一来,方法的表述也就特别简单了。

含区块的方法的结构突显灵活的理由是:对于1个方法来讲,有多少个参数才算合适这样的问题。根据经验,我们认为在有区块的场合下,1个参数会比较好些。这样一来,区块和方法的关联关系看起来会比较容易理解些。

像这种要对所有元素使用相同的运算方式所运算出的结果进行排序时,使用sort_by方法可以节省不少时间。

 

sort_by方法会对每个元素先执行一次区块指定的动作,再以这个结果进行排序。

实现迭代器

前面介绍了很多Ruby所提供的标准迭代器,当然自己要定义迭代器也是可以的。

在这里以一个图书列表的类为例,来加以介绍。

首先定义一个代表书籍的Book类. 在Book类的对象里,存放书名、作者名、领域等数据,分别存放在@title、@author、@genre这些实例变量里,并将这些实例变量设定为可通过访问方法从外部读取与更改。

接着来定义BookList类作为图书列表。这个类提供了新增书籍、删除书籍、读取列表中的书籍数据等操作。

 

下面是一个使用Book类与BookList类的程序示例。

 

 

接下来,依序获取存放在BookList类对象里的每一本书。做法虽然有很多,但这里想要使用“以迭代器获取”的做法。使用起来会是这样:

 

在这里出现了“yield(book)”这行语句,这个yield就是定义迭代器时最重要的关键所在。写在方法定义里的yield,表示调用传递给这个方法的区块。这时,yield若有指定实参,就会分别存进区块变量里。

例如,下面这个print2方法的调用。

则这时区块变量的x与y的值就分别是1与2。

当然,这段定义中print2方法的区块只调用了一次。要定义成迭代器时,应该使用循环之类的方式不断调用yield语句。

回到前面each方法的定义。在这个定义中,调用实例变量@booklist的each方法,获取@booklist内的每个对象,再以这个对象作为实参,调用yield语句。这样一来,就可以对实例变量@booklist里的每个元素,执行一次迭代器的区块了。

接下来要思考的是只获取书名的迭代器。这个迭代器的名称使用each_title似乎不错。使用方法如下:

 

前面的each方法是先建立Book对象,再以对象的title方法获取数据。这个each_title方法则可以直接从booklist对象里取出书名。

现在就试着在booklist.rb里定义each_title方法吧。

与前面的each方法示例不同的地方只有yield的部分。使用each方法时yield的实参是book,传递整个book对象,而这里则是以book.title为实参,所以区块得到的数据就只有书名的字符串了。

下面再设计一个迭代器,不只可以处理书名,而且还传入作者。方法名称就叫做each_title_author吧。用法如下:

与前面示例不同的地方在于它传递给区块两个实参。现在就赶快在booklist.rb里定义这个each_title_author方法吧。

差异也只在于yield的实参而已。前面只有book.title,这里则有book.title与book.author两个。

就像这个方法所定义的,当yield不只一个实参时,区块就会收到不只一个区块变量。

有实参的迭代器

这里再举一个除了区块以外,也有一般实参的迭代器示例。

例如,想要只获取某个特定作者的书籍列表时,使用each方法,可以这样写:

当然这样做也不是不行,但若能直接以作者姓名获取适当的迭代器,感觉似乎更方便。例如像这样:

以实例变量@booklist的each方法逐项获取 book的地方是一样的,差异仅在于在使用yield将book传递给区块的部分是写在if条件式里的。这里会将传递给方法的参数author与书籍的作者姓名,也就是book对象的author互相匹配,只在匹配成功的时候调用yield。

接下来再把这个方法改写成没有指定区块的时候,就返回匹配成功的项构成的数组。让这个方法也可以不需要指定区块。

 

这个方法使用到了block_given?这个方法。这个方法在有传入区块时会返回true;没有传入区块时会返回false。

最后完整版如下所示

区块的传递方法

到目前为止的例子中,区块是以方法结尾地方用{~}括起来这样的形式进行传递的。除此之外,区块还可以以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

运行出来的效果是一样的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值