块
块
https://www.kancloud.cn/imxieke/ruby-base/107298
块是什么
块就是在调用方法时,能与参数一起传递的多个处理的集合。之前在介绍 each
方法、time
方法等与循环有关的部分时,我们就已经接触过块。接收块的方法会执行必要次数的块。块的执行次数由方法本身决定,因此不需事前指定,甚至有可能一次都不执行。
在下面的例子中,我们使用 each
方法,把保存在 Array
对象中的各个整数依次取 2 次幂后输出。do
和 end
之间的部分就是所谓的块。在本例中,块总共被执行了 5 次。
我们把这样的方法调用称为“调用带块的方法”或者“调用块”。块的调用方法一般采用以下形式。
或者
块的开头是块变量。块变量就是在执行块的时候,从方法传进来的参数。不同方法的块变量个数也不相同。例如,在 Array#each
方法中,数组的元素会作为块变量被逐个传递到块中。而在 Array#each_with_index
方法中,则是 [ 元素 , 索引 ] 两个值被传递到块中。
块的使用方法
循环
在 Ruby 中,我们常常使用块来实现循环。在接收块的方法中,实现了循环处理的方法称为迭代器(iterator)。each
方法就是一个典型的迭代器。
在下面的例子中,我们把数组的各个元素转换为大写后输出。
和数组一样,散列也能将元素一个个拿出来,但与数组不同的是,散列会将 [key, value]
的组合作为数组来提取元素。可以成对地提取散列的全部键、值。本例中使用 pair[1]
提取并合计了散列的值,提取散列的键时则可以使用 pair[0]
。
在接收块变量时,多重赋值规则也是同样适用的。我们稍微把代码修改一下,这样一来,键、值就可以被分别赋值给不同的变量了。
File
对象被用于读写文件的内容。使用 File
对象可将文件数据从头到尾读取出来。
根据文件内容的不同,我们需要考虑是以字符为单位,还是以行为单位来做读取处理。代码里使用了 File
类的 each
方法的一个程序示例,它会把 sample.txt
文件的内容按顺序逐行读取出来并输出。
除了 each_line
方法外,File
对象中还有以字符为单位来循环读取数据的 each_char
方法、以及以字节为单位进行循环读取的 each_byte
方法等等。而其他对象也有很多以 each_XX
命名的循环读取数据的方法。
隐藏常规处理
上文中我们介绍了将块用于循环的迭代器的例子。但正如本章开头所介绍的那样,除了迭代器以外,块还被广泛使用在其他地方。其中一个用法就是确保后处理被执行。下面我们来看一个典型的例子——File.open
方法。File.open
方法在接收块后,会将 File
对象作为块变量,并执行一次块。
与改写之前的程序相比,File
对象读取数据的部分一样,不同点在于没有了最后的 close
方法的调用。如果使用完打开的文件后没有将文件关闭的话,有可能会产生其他程序无法打开该文件,或者到达一次性可打开的文件数的上限时无法再打开新文件等问题。而在上面程序中,即使遇到无法打开文件等错误也可以正常关闭文件,因为块内部进行了类似下面的异常处理。
File.open
方法使用块时,块内部的处理完毕并跳出方法前,文件会被自动关闭,因此就不需要那样使用 File.close
方法。
文件使用完毕后,由方法执行关闭操作,而我们只需将必要的处理记述在块中即可。这样一来可以减少程序的代码量,二来可以防止忘记关闭文件等错误的发生。
替换部分算法
下面我们再来介绍一个块的常见用法。这一次我们以数组排序为例,来了解一下指定处理顺序时块的使用方法。
自定义排列顺序
Array
类的sort
方法是对数组内元素进行排序的方法。对数组元素进行排序,可以采取多种方法。按数字的大小顺序
按字母顺序
按字符串的长度顺序
按数组元素的合计值的大小顺序
如果按照这样的条件分别定义相应的排序方法,就会使方法的数量过多,不便于记忆。因此,在
Array.sort
方法中,元素的排序步骤由方法决定,用户只能指定元素间关系的比较逻辑。Array.sort
方法没有指定块时,会使用<=>
运算符对各个元素进行比较,并根据比较后的结果进行排序。<=>
运算符的返回值为 -1、0、1 中的一个。状态 | 结果
| - a <> 时 | -1(比 0 小) a == b 时 | 0 a > b 时 | 1(比 0 大)
使用
<=>
运算符比较字符串时,会按照字符编码的顺序进行比较。比较字母时,会按先大写字母后小写字母的顺序排列。我们可以通过调用块来指定排列顺序。下面的例子与不使用块时的执行结果是一样的。
在
sort
方法的末尾添加了块{ |a, b| a <=> b }
,sort
方法会根据块的执行结果判断元素的大小关系。当需要比较元素的大小关系时,块中需要比较的两个对象就会被作为块变量调用。对块变量a
和b
进行比较后,数组整体就会按该顺序排列。在这里,我们需要注意块中最后一个表达式的值就是块的执行结果,因此
<=>
运算符必须在最后一行使用。备注 块的最后一个表达式不是指块的最后一行表达式,而是指在块中最后执行的表达式。
按字符串的长度排序时,可以采用如下方法。
在之前的例子中,我们只是单纯地比较了字符串
a
、b
,这里我们使用String.length
方法,来比较字符串的长度。用<=>
运算符比较数值时,得到的是由小到大的排列顺序,因此,比较字符串长度时,结果就是按照由短到长的顺序进行排列。像这样,块经常被用来在
sort
方法中实现自定义排列顺序。预先取出排序所需的信息
我们再来详细看看
sort
方法的块。每次比较元素时,sort
方法都会调用一次将两个元素作为块变量的块。这里,我们仍以刚才介绍的按字符串长度排序的程序为例,来看看程序调用了length
方法多少次。执行示例
可以看出,在这个例子中,我们对 28 个元素进行了排序,块总共被调用了 91 次。由于每调用 1 次块,
length
方法就会被调用 2 次,因此最终就会被调用 182 次。而实际上,我们只需对所有的字符串都调用 1 次length
方法,然后再用得出的结果进行排序就可以了。像这样,在能够通过< = >
运算符对转换后的结果进行比较的情况下,使用sort_by
方法会使排序更加有效率。sort_by 方法会将每个元素在块中各调用一次,然后再根据这些结果做排序处理。这种情况下,虽然比较的次数不变,但获取排序所需要的信息的次数(本例中为 28 次)只需与元素个数一样就可以了。
总结一下,元素排序算法中公共的部分由方法本身提供,我们则可以用块来替换方法中元素排列的顺序(或者取得用于比较的信息),或者根据不同的目的来替换需要更改的部分。
定义带块的方法
执行块
首先让我们重温一下 myloop
方法
myloop
方法在执行 while
循环的同时执行了 yield
关键字,yield
关键字的作用就是执行方法的块。因为这个 while
循环的条件固定为 true
,所以会无限循环地执行下去,但只要在块里调用 break
,就可以随时中断 myloop
方法,来执行后面的处理。
传递块参数,获取块的值
在刚才的例子中,块参数以及块的执行结果都没有被使用。接下来,我们会定义一个方法,该方法接收两个整数参数,并对这两个整数之间的整数做某种处理后进行合计处理,而“某种处理”则由块指定。
total
方法会先使用 Integer.upto
方法把 from
到 to
之间的整数值按照从小到大的顺序取出来,然后交给块处理,最后再将块处理后的值累加到变量 result
。程序第 5 行中,对 yield
传递参数后,参数值就会作为块变量传递到块中。同时,块的运行结果也会作为 yield
的结果返回。
程序第 4 行的 block_given?
方法被用来判断调用该方法时是否有块被传递给方法,如果有则返回 true
,反之返回 false
。如果方法没有块,则在程序第 7 行中直接把 num
相加。
在本例中,对 yield
传递 1 个参数,就有 1 个块变量接收。下面我们来看看对 yield
传递 0 个、1 个、3 个等多个参数时,对应的块变量是如何进行接收的。
执行示例
首先我们注意到,yield
参数的个数与块变量的个数是不一样的。从 |a|
和 |a, b, c|
的例子中可以看出,块变量比较多时,多出来的块变量值为 nil
,而块变量不足时,则不能接收参数值。
最后的通过 |*a|
接收的情况是将所有块变量整合为一个数组来接收。这与定义方法时接收可变参数的情况非常相似。
抽取嵌套数组的元素的规则,同样也适用于块变量。例如,Hash.each_with_index
方法的块变量有 2 个,并以 yield([ 键 , 值 ], 索引 )
的形式传递。在接收块变量后,我们就可以把 [ 键 , 值 ]
部分分别赋值给不同的变量。
执行示例
控制块的执行
在调用代码清单 total
方法时,如果像下面那样在中途使用 break
,total
方法的结果会变成什么样子呢?
答案是 nil
。在块中使用 break
,程序会马上返回到调用块的地方,因此 total
方法中返回计算结果的处理等都会被忽略掉。但作为方法的结果,当我们希望返回某个值的时候,就可以像 break 0
这样指定 break
方法的参数,这样该值就会成为方法的返回值。
此外,如果在块中使用 next
,程序就会中断当前处理,并继续执行下面的处理。使用 next
后,执行块的 yield
会返回,如果 next
没有指定任何参数则返回 nil
,而如果像 next 0
这样指定了参数,那么该参数值就是返回值。
最后,如果在块中使用 redo
,程序就会返回到块的开头,并按照相同的块变量再次执行处理。这种情况下,块的处理结果不会返回给外部,因此需要十分小心 redo
的用法,注意不要使程序陷入死循环。
最后,如果在块中使用 redo
,程序就会返回到块的开头,并按照相同的块变量再次执行处理。这种情况下,块的处理结果不会返回给外部,因此需要十分小心 redo
的用法,注意不要使程序陷入死循环。
将块封装为对象
如前所述,在接收块的方法中执行块时,可以使用 yield
关键字。
而 Ruby 还能把块当作对象处理。把块当作对象处理后,就可以在接收块的方法之外的其他地方执行块,或者把块交给其他方法执行。
这种情况下需要用到 Proc
对象。Proc
对象是能让块作为对象在程序中使用的类。定义 Proc
对象的典型的方法是,调用 Proc.new
方法这个带块的方法。在调用 Proc
对象的 call
方法之前,块中定义的程序不会被执行。
在下面的代码例子中,定义一个输出信息的 Proc
对象,并调用两次。这时,程序就会把 call
方法的参数作为块参数来执行块。
执行示例
把块从一个方法传给另一个方法时,首先会通过变量将块作为 Proc
对象接收,然后再传给另一个方法。在方法定义时,如果末尾的参数使用“& 参数名”的形式,Ruby 就会自动把调用方法时传进来的块封装为 Proc
对象。
下面,我们将代码中块的接收方法加以改写,如下所示:
我们在首行的方法定义中定义了 &block
参数。像这样,在变量名前添加 &
的参数被称为 Proc
参数。如果在调用方法时没有传递块,Proc
参数的值就为 nil
,因此通过这个值就可以判断出是否有块被传入方法中。另外,执行块的语句不是 yield
,而是 block.call(num)
,这一点与之前的例子也不一样。
方法可以有多个参数,而且定义参数的默认值等时都需要按照一定的顺序。而 Proc
参数则一定要在所有参数之后,也就是方法中最后一个参数。
将块封装为 Proc
对象后,我们就可以根据需要随时调用块。甚至还可以将其赋值给实例变量,让别的实例方法去任意调用。
此外,我们也能将 Proc
对象作为块传给其他方法处理。这时,只需在调用方法时,用“&Proc
对象”的形式定义参数就可以了。例如,向 Array.each
方法传递块时,可以像下面那样定义。
这样一来,我们就可以非常方便地把调用 call_each
方法时接收到的块,原封不动地传给 ary.each
方法。
执行示例
局部变量与块变量
块内部的命名空间与块外部是共享的。在块外部定义的局部变量,在块中也可以继续使用。而被作为块变量使用的变量,即使与块外部的变量同名,Ruby 也会认为它们是两个不同的变量。
执行示例
在 ary.each
方法的块中,x
的值被赋值给了局部变量 y
。因此,y
保留了最后一次调用块时块变量 x
的值 3。而变量 x 的值在调用 ary.each
前后并没有发生改变。
相反,在块内部定义的变量不能被外部访问。在刚才的例子中,如果把第 2 行的代码删掉,程序就会出错。
块中变量的作用域之所以这么设计,是为了通过与块外部共享局部变量,从而扩展变量的有效范围。在块内部给局部变量赋值的时候,要时刻注意它与块外部的同名变量的关系。大家一定要小心 Ruby 中的这个小陷阱。
块变量是只能在块内部使用的变量(块局部变量),它不能覆盖外部的局部变量,但 Ruby 为我们提供了定义块变量以外的块局部变量的语法。使用在块变量后使用 ;
加以区分的方式,来定义块局部变量。这里我们再稍微修改一下刚才的例子,如下所示。可以看出,块执行后 x
和 y
的值并没有变化。
执行示例