# **第 11 章 块**
Ruby 中大量使用了块(block)。块原本只是为了循环而产生的语法结构,但现在程序中许多地方也都使用了块。因此,如何灵活地使用块,也是 Ruby 的重点之一。
下面就让我们来讨论一下块的作用及其用途。
### **11.1 块是什么**
块 1 就是在调用方法时,能与参数一起传递的多个处理的集合。之前在介绍 `each` 方法、`time` 方法等与循环有关的部分时,我们就已经接触过块。接收块的方法会执行必要次数的块。块的执行次数由方法本身决定,因此不需事前指定,甚至有可能一次都不执行。
1有时也称代码块。——译者注
在下面的例子中,我们使用 `each` 方法,把保存在 `Array` 对象中的各个整数依次取 2 次幂后输出。`do` 和 `end` 之间的部分就是所谓的块。在本例中,块总共被执行了 5 次。
~~~
[1, 2, 3, 4, 5].each do |i|
puts i ** 2
end
~~~
正如在第 7 章中所介绍的那样,我们把这样的方法调用称为“调用带块的方法”或者“调用块”。块的调用方法一般采用以下形式。
**对象. 方法名( 参数列表) `do` | 块变量 |
希望循环的处理
`end`**
或者
**对象. 方法名( 参数列表) { | 块变量 |
希望循环的处理
}**
块的开头是块变量。块变量就是在执行块的时候,从方法传进来的参数。不同方法的块变量个数也不相同。例如,在 `Array#each` 方法中,数组的元素会作为块变量被逐个传递到块中。而在 `Array#each_with_index` 方法中,则是 [ 元素 , 索引 ] 两个值被传递到块中。
> **执行示例**
![{%}](https://box.kancloud.cn/2015-10-26_562e01ec04111.png)
而在第 6 章中介绍的 `loop` 方法则不需要传递块变量。
### **11.2 块的使用方法**
### **11.2.1 循环**
在 Ruby 中,我们常常使用块来实现循环。在接收块的方法中,实现了循环处理的方法称为迭代器(iterator)。`each` 方法就是一个典型的迭代器。
在下面的例子中,我们把数组的各个元素转换为大写后输出。
~~~
alphabet = ["a", "b", "c", "d", "e"]
alphabet.each do |i|
puts i.upcase
end
~~~
和数组一样,散列也能将元素一个个拿出来,但与数组不同的是,散列会将 `[key, value]` 的组合作为数组来提取元素。如代码清单 11.1 所示,可以成对地提取散列的全部键、值。本例中使用 `pair[1]` 提取并合计了散列的值,提取散列的键时则可以使用 `pair[0]`。
**代码清单 11.1 hash_each.rb**
~~~
sum = 0
outcome = {"参加费"=>1000, "挂件费用"=>1000, "联欢会费用"=>4000}
outcome.each do |pair|
sum += pair[1] # 指定值
end
puts "合计:#{sum}"
~~~
在接收块变量时,多重赋值规则也是同样适用的。我们稍微把代码清单 11.1 的程序修改一下,使之变成代码清单 11.2 那样,这样一来,键、值就可以被分别赋值给不同的变量了。
**代码清单 11.2 hash_each2.rb**
~~~
sum = 0
outcome = {"参加费"=>1000, "挂件费用"=>1000, "联欢会费用"=>4000}
outcome.each do |item, price|
sum += price
end
puts "合计:#{sum}"
~~~
`File` 对象被用于读写文件的内容。使用 `File` 对象可将文件数据从头到尾读取出来。
根据文件内容的不同,我们需要考虑是以字符为单位,还是以行为单位来做读取处理。代码清单 11.3 是使用了 `File` 类的 `each` 方法的一个程序示例,它会把 sample.txt 文件的内容按顺序逐行读取出来并输出。
**代码清单 11.3 file_each.rb**
~~~
file = File.open("sample.txt")
file.each_line do |line|
print line
end
file.close
~~~
除了 `each_line` 方法外,`File` 对象中还有以字符为单位来循环读取数据的 `each_char` 方法、以及以字节为单位进行循环读取的 `each_byte` 方法等等。而其他对象也有很多以 `each_XX` 命名的循环读取数据的方法。
### **11.2.2 隐藏常规处理**
上文中我们介绍了将块用于循环的迭代器的例子。但正如本章开头所介绍的那样,除了迭代器以外,块还被广泛使用在其他地方。其中一个用法就是确保后处理被执行。下面我们来看一个典型的例子——`File.open` 方法。`File.open` 方法在接收块后,会将 `File` 对象作为块变量,并执行一次块。这里,我们可以使用块把代码清单 11.3 改写为代码清单 11.4 那样。
**代码清单 11.4 file_open.rb**
~~~
File.open("sample.txt") do |file|
file.each_line do |line|
print line
end
end
~~~
与改写之前的程序相比,`File` 对象读取数据的部分一样,不同点在于没有了最后的 `close` 方法的调用。如果使用完打开的文件后没有将文件关闭的话,有可能会产生其他程序无法打开该文件,或者到达一次性可打开的文件数的上限时无法再打开新文件等问题。而在代码清单 11.4 的程序中,即使遇到无法打开文件等错误也可以正常关闭文件,因为块内部进行了程序清单 11.5 那样的处理。
**代码清单 11.5 file_open_no_block.rb**
~~~
file = File.open("sample.txt")
begin
file.each_line do |line|
print line
end
ensure
file.close
end
~~~
`File.open` 方法使用块时,块内部的处理完毕并跳出方法前,文件会被自动关闭,因此就不需要像代码清单 11.3 那样使用 `File#close` 方法。
文件使用完毕后,由方法执行关闭操作,而我们只需将必要的处理记述在块中即可。这样一来可以减少程序的代码量,二来可以防止忘记关闭文件等错误的发生。
### **11.2.3 替换部分算法**
下面我们再来介绍一个块的常见用法。这一次我们以数组排序为例,来了解一下指定处理顺序时块的使用方法。
-
**自定义排列顺序**
`Array` 类的 `sort` 方法是对数组内元素进行排序的方法。对数组元素进行排序,可以采取多种方法。
-
**按数字的大小顺序**
-
**按字母顺序**
-
**按字符串的长度顺序**
-
**按数组元素的合计值的大小顺序**
如果按照这样的条件分别定义相应的排序方法,就会使方法的数量过多,不便于记忆。因此,在 `Array#sort` 方法中,元素的排序步骤由方法决定,用户只能指定元素间关系的比较逻辑。
`Array#sort` 方法没有指定块时,会使用 `<=>` 运算符对各个元素进行比较,并根据比较后的结果进行排序。`<=>` 运算符的返回值为`-1`、`0`、`1` 中的一个。
**表 11.1 a <=> b 的结果**
| `a <>` 时 | -1(比 0 小) |
|-----|-----|
| `a == b` 时 | 0 |
| `a > b` 时 | 1(比 0 大) |
使用 `<=>` 运算符比较字符串时,会按照字符编码的顺序进行比较。比较字母时,会按先大写字母后小写字母的顺序排列。
~~~
array = ["ruby", "Perl", "PHP", "Python"]
sorted = array.sort
p sorted #=> ["PHP", "Perl", "Python", "ruby"]
~~~
我们可以通过调用块来指定排列顺序。下面的例子与不使用块时的执行结果是一样的。
~~~
array = ["ruby", "Perl", "PHP", "Python"]
sorted = array.sort{ |a, b| a <=> b }
p sorted #=> ["PHP", "Perl", "Python", "ruby"]
~~~
在 `sort` 方法的末尾添加了块 `{ |a, b| a <=> b }`,sort 方法会根据块的执行结果判断元素的大小关系。当需要比较元素的大小关系时,块中需要比较的两个对象就会被作为块变量调用。对块变量 `a` 和 `b` 进行比较后,数组整体就会按该顺序排列。
在这里,我们需要注意块中最后一个表达式的值就是块的执行结果,因此 `<=>` 运算符必须在最后一行使用。
> **备注** 块的最后一个表达式不是指块的最后一行表达式,而是指在块中最后执行的表达式。
按字符串的长度排序时,可以采用如下方法。
~~~
array = ["ruby", "Perl", "PHP", "Python"]
sorted = array.sort{ |a, b| a.length <=> b.length }
p sorted #=> ["PHP", "ruby", "Perl", "Python"]
~~~
在之前的例子中,我们只是单纯地比较了字符串 `a`、`b`,这里我们使用 `String#length` 方法,来比较字符串的长度。用 `<=>` 运算符比较数值时,得到的是由小到大的排列顺序,因此,比较字符串长度时,结果就是按照由短到长的顺序进行排列。
像这样,块经常被用来在 `sort` 方法中实现自定义排列顺序。
-
**预先取出排序所需的信息**
我们再来详细看看 `sort` 方法的块。每次比较元素时,`sort` 方法都会调用一次将两个元素作为块变量的块。这里,我们仍以刚才介绍的按字符串长度排序的程序为例,来看看程序调用了 `length` 方法多少次。
**代码清单 11.6 sort_comp_count.rb**
~~~
ary = %w(
Ruby is a open source programming language with a focus
on simplicity and productivity. It has an elegant syntax
that is natural to read and easy to write
)
call_num = 0 # 块的调用次数
sorted = ary.sort do |a, b|
call_num += 1 # 累加块的调用次数
a.length <=> b.length
end
puts "排序结果 #{sorted}"
puts "数组的元素数量 #{ary.length}"
puts "调用块的次数 #{call_num}"
~~~
> **执行示例**
~~~
> ruby sort_comp_count.rb
排序结果 ["a", "a", "on", "to", "It", "to", "is", "an", ......]
数组的元素数量 28
调用块的次数 91
~~~
可以看出,在这个例子中,我们对 28 个元素进行了排序,块总共被调用了 91 次。由于每调用 1 次块,`length` 方法就会被调用 2 次,因此最终就会被调用 182 次。而实际上,我们只需对所有的字符串都调用 1 次 `length` 方法,然后再用得出的结果进行排序就可以了。像这样,在能够通过 `< = >` 运算符对转换后的结果进行比较的情况下,使用 `sort_by` 方法会使排序更加有效率。
~~~
ary = %w(
Ruby is a open source programming language with a focus
on simplicity and productivity. It has an elegant syntax
that is natural to read and easy to write
)
sorted = ary.sort_by{ |item| item.length }
p sorted
~~~
`sort_by` 方法会将每个元素在块中各调用一次,然后再根据这些结果做排序处理。这种情况下,虽然比较的次数不变,但获取排序所需要的信息的次数(本例中为 28 次)只需与元素个数一样就可以了。
总结一下,元素排序算法中公共的部分由方法本身提供,我们则可以用块来替换方法中元素排列的顺序(或者取得用于比较的信息),或者根据不同的目的来替换需要更改的部分。
### **11.3 定义带块的方法**
在第 7 章中我们简单地介绍了如何定义带块的方法,接下来我们就来详细地讨论一下。
### **11.3.1 执行块**
首先让我们重温一下第 7 章中的 `myloop` 方法(代码清单 11.7)。
**代码清单 11.7 myloop.rb**
~~~
def myloop
while true
yield # 执行块
end
end
num = 1 # 初始化num
myloop do
puts "num is #{num}" # 输出num
break if num > 100 # num 超过100 后跳出循环
num *= 2 # num 乘2
end
~~~
`myloop` 方法在执行 `while` 循环的同时执行了 `yield` 关键字,`yield` 关键字的作用就是执行方法的块。因为这个 `while` 循环的条件固定为 `true`,所以会无限循环地执行下去,但只要在块里调用 `break`,就可以随时中断 `myloop` 方法,来执行后面的处理。
### **11.3.2 传递块参数,获取块的值**
在刚才的例子中,块参数以及块的执行结果都没有被使用。接下来,我们会定义一个方法,该方法接收两个整数参数,并对这两个整数之间的整数做某种处理后进行合计处理,而“某种处理”则由块指定(代码清单 11.8)。
**代码清单 11.8 total.rb**
~~~
1: def total(from, to)
2: result = 0 # 合计值
3: from.upto(to) do |num| # 处理从from 到to 的值
4: if block_given? # 如果有块的话
5: result += yield(num) # 累加经过块处理的值
6: else # 如果没有块的话
7: result += num # 直接累加
8: end
9: end
10: return result # 返回方法的结果
11: end
12:
13: p total(1, 10) # 从1 到10 的和 => 55
14: p total(1, 10){|num| num ** 2 } # 从1 到10 的2 次幂的和 => 385
~~~
`total` 方法会先使用 `Integer#upto` 方法把 `from` 到 `to` 之间的整数值按照从小到大的顺序取出来,然后交给块处理,最后再将块处理后的值累加到变量 `result`。程序第 5 行中,对 `yield` 传递参数后,参数值就会作为块变量传递到块中。同时,块的运行结果也会作为 `yield` 的结果返回。
程序第 4 行的 `block_given?` 方法被用来判断调用该方法时是否有块被传递给方法,如果有则返回 `true`,反之返回 `false`。如果方法没有块,则在程序第 7 行中直接把 `num` 相加。
在本例中,对 `yield` 传递 1 个参数,就有 1 个块变量接收。下面我们来看看对 `yield` 传递 0 个、1 个、3 个等多个参数时,对应的块变量是如何进行接收的(代码清单 11.9)。
**代码清单 11.9 block_args_test.rb**
~~~
def block_args_test
yield() # 0 个块变量
yield(1) # 1 个块变量
yield(1, 2, 3) # 3 个块变量
end
puts "通过|a| 接收块变量"
block_args_test do |a|
p [a]
end
puts
puts "通过|a, b, c| 接收块变量"
block_args_test do |a, b, c|
p [a, b, c]
end
puts
puts "通过|*a| 接收块变量"
block_args_test do |*a|
p [a]
end
puts
~~~
> **执行示例**
~~~
> ruby block_args_test.rb
通过|a| 接收块变量
[nil]
[1]
[1]
通过|a, b, c| 接收块变量
[nil, nil, nil]
[1, nil, nil]
[1, 2, 3]
通过|*a| 接收块变量
[[]]
[[1]]
[[1, 2, 3]]
~~~
首先我们注意到,`yield` 参数的个数与块变量的个数是不一样的。从 `|a|` 和 `|a, b, c|` 的例子中可以看出,块变量比较多时,多出来的块变量值为 `nil`,而块变量不足时,则不能接收参数值。
最后的通过 `|*a|` 接收的情况是将所有块变量整合为一个数组来接收。这与定义方法时接收可变参数的情况非常相似。
另外,在第 4 章中介绍的抽取嵌套数组的元素的规则,同样也适用于块变量。例如,`Hash#each_with_index` 方法的块变量有 2 个,并以 `yield([ 键 , 值 ], 索引 )` 的形式传递。像代码清单 11.10 那样,在接收块变量后,我们就可以把 `[ 键 , 值 ]` 部分分别赋值给不同的变量。
**代码清单 11.10 param_grouping.rb**
~~~
hash = {a: 100, b: 200, c: 300}
hash.each_with_index do |(key, value), index|
p [key, value, index]
end
~~~
> **执行示例**
~~~
> ruby param_grouping.rb
[:a, 100, 0]
[:b, 200, 1]
[:c, 300, 2]
~~~
### **11.3.3 控制块的执行**
在调用代码清单 11.8 的 `total` 方法时,如果像下面那样在中途使用 `break`,`total` 方法的结果会变成什么样子呢?
~~~
n = total(1, 10) do |num|
if num == 5
break
end
num
end
p n #=> ??
~~~
答案是 `nil`。在块中使用 `break`,程序会马上返回到调用块的地方,因此 `total` 方法中返回计算结果的处理等都会被忽略掉。但作为方法的结果,当我们希望返回某个值的时候,就可以像 `break 0` 这样指定 `break` 方法的参数,这样该值就会成为方法的返回值。
此外,如果在块中使用 `next`,程序就会中断当前处理,并继续执行下面的处理。使用 `next` 后,执行块的 `yield` 会返回,如果 `next` 没有指定任何参数则返回 `nil`,而如果像 `next 0` 这样指定了参数,那么该参数值就是返回值。
~~~
n = total(1, 10) do |num|
if num % 2 != 0
next 0
end
num
end
p n #=> 30
~~~
最后,如果在块中使用 `redo`,程序就会返回到块的开头,并按照相同的块变量再次执行处理。这种情况下,块的处理结果不会返回给外部,因此需要十分小心 `redo` 的用法,注意不要使程序陷入死循环。
### **11.3.4 将块封装为对象**
如前所述,在接收块的方法中执行块时,可以使用 `yield` 关键字。
而 Ruby 还能把块当作对象处理。把块当作对象处理后,就可以在接收块的方法之外的其他地方执行块,或者把块交给其他方法执行。
这种情况下需要用到 `Proc` 对象。`Proc` 对象是能让块作为对象在程序中使用的类。定义 `Proc` 对象的典型的方法是,调用 `Proc.new` 方法这个带块的方法。在调用 `Proc` 对象的 `call` 方法之前,块中定义的程序不会被执行。
在代码清单 11.11 的例子中,定义一个输出信息的 `Proc` 对象,并调用两次。这时,程序就会把 `call` 方法的参数作为块参数来执行块。
**代码清单 11.11 proc1.rb**
~~~
hello = Proc.new do |name|
puts "Hello, #{name}."
end
hello.call("World")
hello.call("Ruby")
~~~
> **执行示例**
~~~
> ruby proc1.rb
Hello, World.
Hello, Ruby.
~~~
把块从一个方法传给另一个方法时,首先会通过变量将块作为 `Proc` 对象接收,然后再传给另一个方法。在方法定义时,如果末尾的参数使用“& 参数名”的形式,Ruby 就会自动把调用方法时传进来的块封装为 `Proc` 对象。
下面,我们将代码清单 11.8 中块的接收方法加以改写,如下所示:
**代码清单 11.12 total2.rb**
~~~
1: def total2(from, to, &block)
2: result = 0 # 合计值
3: from.upto(to) do |num| # 处理从from 到to 的值
4: if block # 如果有块的话
5: result += # 累加经过块处理的值
6: block.call(num)
7: else # 如果没有块的话
8: result += num # 直接累加
9: end
10: end
11: return result # 返回方法的结果
12: end
13:
14: p total2(1, 10) # 从1 到10 的和 => 55
15: p total2(1, 10){|num| num ** 2 } # 从1 到10 的2 次幂的和 => 385
~~~
我们在首行的方法定义中定义了 `&block` 参数。像这样,在变量名前添加 `&` 的参数被称为 Proc 参数。如果在调用方法时没有传递块,`Proc` 参数的值就为 `nil`,因此通过这个值就可以判断出是否有块被传入方法中。另外,执行块的语句不是 `yield`,而是 `block.call(num)`,这一点与之前的例子也不一样。
在第 7 章中我们提到过方法可以有多个参数,而且定义参数的默认值等时都需要按照一定的顺序。而 `Proc` 参数则一定要在所有参数之后,也就是方法中最后一个参数。
将块封装为 `Proc` 对象后,我们就可以根据需要随时调用块。甚至还可以将其赋值给实例变量,让别的实例方法去任意调用。
此外,我们也能将 `Proc` 对象作为块传给其他方法处理。这时,只需在调用方法时,用“`&Proc` 对象”的形式定义参数就可以了。例如,向 `Array#each` 方法传递块时,可以像代码清单 11.13 那样定义。
**代码清单 11.13 call_each.rb**
~~~
def call_each(ary, &block)
ary.each(&block)
end
call_each [1, 2, 3] do |item|
p item
end
~~~
这样一来,我们就可以非常方便地把调用 `call_each` 方法时接收到的块,原封不动地传给 `ary.each` 方法。
> **执行示例**
~~~
> ruby call_each.rb
1
2
3
~~~
### **11.4 局部变量与块变量**
块内部的命名空间与块外部是共享的。在块外部定义的局部变量,在块中也可以继续使用。而被作为块变量使用的变量,即使与块外部的变量同名,Ruby 也会认为它们是两个不同的变量。请看代码清单 11.14。
**代码清单 11.14 local_and_block.rb**
~~~
x = 1 # 初始化x
y = 1 # 初始化y
ary = [1, 2, 3]
ary.each do |x| # 将x 作为块变量使用
y = x # 将x 赋值给y
end
p [x, y] # 确认x 与y 的值
~~~
> **执行示例**
~~~
> ruby local_and_block.rb
[1, 3]
~~~
在 `ary.each` 方法的块中,`x` 的值被赋值给了局部变量 `y`。因此,`y` 保留了最后一次调用块时块变量 `x` 的值 3。而变量 `x` 的值在调用 `ary.each` 前后并没有发生改变。
相反,在块内部定义的变量不能被外部访问。在刚才的例子中,如果把第 2 行的代码删掉,程序就会出错。
~~~
x = 1 # 初始化 x
#y = 1 # 初始化 y
ary = [1, 2, 3]
ary.each do |x| # 将 x 作为块变量使用
y = x # 将 x 赋值给y
end
p [x, y] # 引用 y 时会出错误(NameError)
~~~
块中变量的作用域之所以这么设计,是为了通过与块外部共享局部变量,从而扩展变量的有效范围。在块内部给局部变量赋值的时候,要时刻注意它与块外部的同名变量的关系。大家一定要小心 Ruby 中的这个小陷阱。
块变量是只能在块内部使用的变量(块局部变量),它不能覆盖外部的局部变量,但 Ruby 为我们提供了定义块变量以外的块局部变量的语法。使用在块变量后使用 `;` 加yiqufen以区分的方式,来定义块局部变量。这里我们再稍微修改一下刚才的例子,如下所示。可以看出,块执行后 `x` 和 `y` 的值并没有变化。
**代码清单 11.15 local_and_block2.rb**
~~~
x = y = z = 0 # 初始化x、y、z
ary = [1, 2, 3]
ary.each do |x; y| # 使用块变量x,块局部变量y
y = x # 代入块局部变量y
z = x # 代入不是块局部变量的变量z
p [x, y, z] # 确认块内的 x、y、z 的值
end
puts
p [x, y, z] # 确认x、y、z 的值
~~~
> **执行示例**
~~~
> ruby local_and_block2.rb
[1, 1, 1]
[2, 2, 2]
[3, 3, 3]
[0, 0, 3]
~~~
- 推荐序
- 译者序
- 前言
- 本书的读者对象
- 第 1 部分 Ruby 初体验
- 第 1 章 Ruby 初探
- 第 2 章 便利的对象
- 第 3 章 创建命令
- 第 2 部分 Ruby 的基础
- 第 4 章 对象、变量和常量
- 第 5 章 条件判断
- 第 6 章 循环
- 第 7 章 方法
- 第 8 章 类和模块
- 第 9 章 运算符
- 第 10 章 错误处理与异常
- 第 11 章 块
- 第 3 部分 Ruby 的类
- 第 12 章 数值类
- 第 13 章 数组类
- 第 14 章 字符串类
- 第 15 章 散列类
- 第 16 章 正则表达式类
- 第 17 章 IO 类
- 第 18 章 File 类与 Dir 类
- 第 19 章 Encoding 类
- 第 20 章 Time 类与 Date 类
- 第 21 章 Proc 类
- 第 4 部分 动手制作工具
- 第 22 章 文本处理
- 第 23 章 检索邮政编码
- 附录
- 附录 A Ruby 运行环境的构建
- 附录 B Ruby 参考集
- 后记
- 谢辞