# **第 13 章 数组类**
本章将详细介绍数组(`Array`)类。
-
**数组的创建方法**
介绍如何新创建数组、以及如何通过其他对象创建数组。
-
**索引的使用方法**
数组的基本用法是利用索引(下标)访问数组内各元素。在这一部分中我们会介绍索引的相关用法。
-
**作为集合的数组与作为列的数组**
Ruby 中可以把数组作为集合操作,也可以把数组作为列操作。在这一部分中我们会介绍这两种数组的用法。
-
**主要的数组方法**
通过数组方法,不仅可以将执行结果以新对象的形式返回,还可以对元素进行互换、删除等操作,进而变更已存在的对象。在这一部分中我们会详细介绍如何使用这些数组方法。
-
**数组与迭代器**
迭代器经常被用来逐个处理数组中的元素。在这一部分中我们会介绍迭代器的基本用法、以及数组的迭代方法。
-
**处理数组中的元素**
除了迭代器之外,使用其他方法也可以处理数组内的各元素。在这一部分中我们会介绍几种具有代表性的方法。
-
**同时访问多个数组**
任何对象都可以作为数组的元素。在这一部分中我们会介绍一些数组使用过程中需要注意的事项。
### **13.1 复习数组**
在第 2 章中我们已经介绍过数组了,现在我们先来复习一下。
数组是带索引的对象的集合。
数组有以下特征:
-
**可以从数组中获取某个索引的元素(对象)**
例:`print name[2]`
-
**可以将任意的值(对象)保存到数组的某个索引的元素中**
例:`name[0] = " 野尻 "`
-
**使用迭代器可以逐个取出数组中的元素**
例:`names.each{|name| puts name}`
![{%}](https://box.kancloud.cn/2015-10-26_562e01f31c8e0.png)
### **13.2 数组的创建方法**
在第 2 章中,我们介绍了使用 `[]` 来创建数组的方法。
~~~
nums = [1, 2, 3, 4, 5]
strs = ["a", "b", "c", "d"]
~~~
除此以外还有其他的创建方法,接下来就简要地介绍一下。
### **13.2.1 使用 Array.new**
创建类的实例时使用的 `new` 方法,创建数组时也同样可以使用。
~~~
a = Array.new
p a #=> []
a = Array.new(5)
p a #=> [nil, nil, nil, nil, nil]
a = Array.new(5, 0)
p a #=> [0, 0, 0, 0, 0]
~~~
`Array` 类的情况下,若 `new` 方法没有参数,则会创建元素个数为 0 的数组;若 `new` 方法只有 1 个参数,则会创建元素个数为该参数个数,且各元素初始值都为 `nil` 的数组;若 `new` 方法有两个参数,则第 1 个参数代表元素的个数,第 2 个参数代表元素值的初始值。
当希望创建元素值相同的数组时,建议使用这个方法。
### **13.2.2 使用 %w 与 %i**
创建不包含空白的字符串数组时,可以使用 `%w`。
~~~
lang = %w(Ruby Perl Python Scheme Pike REBOL)
p lang #=> ["Ruby", "Perl", "Python", "Scheme", "Pike",
# "REBOL"]
~~~
虽然给人的感觉只是节省了书写 `" "` 和 `,` 的时间,但是如果能掌握这种字符串数组的创建方法,就会使程序更加简洁。此外,Ruby2.0 还提供了创建符号(Symbol)数组的 `%i`。
~~~
lang = %i(Ruby Perl Python Scheme Pike REBOL)
p lang #=> [:Ruby, :Perl, :Python, :Scheme, :Pike, :REBOL]
~~~
在本例中,创建数组时使用了 `()` 将数组元素括了起来,但实际上还可以使用如 `<>`、`||`、`!!`、`@@`、`AA` 这样的任意字符。
虽然 Ruby 允许我们使用任意字符,但若用一些不常用的字符来创建数组的话,可能就会使程序不便于阅读。在选择表示字符串数组元素的字符时,还要注意该字符不能在要创建的字符串中出现,因此建议使用 `()`、`<>`、`||`。
### **13.2.3 使用 to_a 方法**
到现在为止,我们已经介绍了三种传统的创建数组的方法,下面我们就来看看如何将其他对象转换为数组。
很多类都定义了 `to_a` 方法,该方法能把该类的对象转换为数组。
~~~
color_table = {black: "#000000", white: "#FFFFFF"}
p color_table.to_a #=> [[:black, "#000000"],
# [:white, "#FFFFFF"]]
~~~
对散列对象使用 `to_a` 方法,结果就会得到相应的数组的数组。具体来说就是,将散列中的各键、值作为一个数组,然后再把这样的数组放到一个大数组中。
### **13.2.4 使用字符串的 split 方法**
我们再来介绍一个将对象转换为数组的方法。对用逗号或者空白间隔的字符串使用 `split` 方法,也可以创建数组。
~~~
column = "2013/05/30 22:33 foo.html proxy.example.jp".split()
p column
#=> ["2013/05/30", "22:33", "foo.html", "proxy.example.jp"]
~~~
关于 `split` 方法我们会在第 14.6 节中详细说明。
### **13.3 索引的使用方法**
在了解了如何创建数组之后,下面我们就来看看如何操作数组。
首先,我们将介绍如何用索引操作数组。用索引操作数组的方法是操作数组的基础方法。这部分内容也许会和第 2 章的内容有些重复,不过这里我们会重点介绍一些新的内容,以使大家能够对索引有一个系统的了解。
### **13.3.1 获取元素**
对数组指定索引值,就可以获取相应的元素。我们可以逐个获取元素,也可以一次获取多个元素。
通过 `[]` 指定索引,获取元素。`[]` 有以下 3 种用法:
(a)*a* [*n*]
(b)*a* [*n..m*] 或者 *a* [*n...m*]
(c)*a* [*n, len*]
用法(a)是我们在第 2 章中使用过的获取索引值为 *n* 的元素的方法。例如,通过 `alpha[0]` 获取数组 `alpha` 的首个元素。这里要注意,数组的索引值是从 0 开始的(图 13.1)。
![{%}](https://box.kancloud.cn/2015-10-26_562e01f32bcdc.png)
**图 13.1 数组与索引的关系**
索引值为负数时,不是从数组的开头,而是从数组的末尾开始获取元素(图 13.2)。如果指定的索引值大于元素个数,则返回 `nil`。
![{%}](https://box.kancloud.cn/2015-10-26_562e01f340f7a.png)
**图 13.2 索引值为负数**
用法(b)的 *a* [*n..m*] 表示获取从 *a* [*n*] 到 *a* [*m*] 的元素,然后用它们创建新数组并返回(图 13.3)。*a* [*n..m*] 表示获取从 *a* [*n*] 到 *a* [*m*-1] 的元素,并用它们创建新数组返回。虽然下面的例子中只讨论 [*n..m*] 的形式,但能用 [*n..m*] 的地方同样能用 [*n...m*]。
![{%}](https://box.kancloud.cn/2015-10-26_562e01f36d586.png)
**图 13.3 指定索引范围**
如果 `m` 的值比数组长度大,则返回的结果与指定数组最后一个元素时是一样的(图 13.4)。
![{%}](https://box.kancloud.cn/2015-10-26_562e01f38ba51.png)
**图 13.4 索引值比数组长度大时**
用法(c)[*n, len*] 表示从 *a* [*n*] 开始,获取之后的 *len* 个元素,用它们创建新数组并返回(图 13.5)。
![{%}](https://box.kancloud.cn/2015-10-26_562e01f3a221f.png)
**图 13.5 从某个元素开始,获取多个元素**
另外,我们还可以用普通的方法代替 `[]`。
-
***a*.`at`(*n*) ……与 *a*[*n*] 等价**
-
***a*.`slice`(*n*) ……与 *a*[*n*] 等价**
-
***a*.`slice`(*n..m*) ……与 *a*[*n..m*] 等价**
-
***a*.`slice`(*n, len*) ……与 *a*[*n, len*] 等价**
不过一般情况下我们很少会使用上述方法。
### **13.3.2 元素赋值**
使用 `[]`、`at`、`slice` 方法除了可以获取元素外,还可以对元素赋值。
-
***a*[*n*] = *item***
这是将 *a* [*n*] 的元素值变更为 *item*。在下面的例子中,我们来尝试把 `B` 赋值给第 2 个元素,把 `E` 赋值给第 5 个元素(图 13.6)。
![{%}](https://box.kancloud.cn/2015-10-26_562e01f3c5611.png)
**图 13.6 元素赋值**
上面的例子介绍的是对一个元素赋值,实际上 Ruby 还可以一次对多个元素赋值。指定多个元素的方法与在 13.3.1 节中介绍的获取多个元素的方法是一样的。
在下面的例子中,我们来尝试对数组的第 3 个元素到第 5 个元素赋值。图 13.7 表示的是使用 [*n..m*] 的形式进行赋值时的情况。下面是使用 [*n, len*] 的形式进行赋值的例子。
~~~
alpha = ["a", "b", "c", "d", "e", "f"]
alpha[2, 3] = ["C", "D", "E"]
p alpha #=> ["a", "b", "C", "D", "E", "f"]
~~~
![{%}](https://box.kancloud.cn/2015-10-26_562e01f3e4c52.png)
**图 13.7 对多个元素赋值**
### **13.3.3 插入元素**
我们还可以在保持当前元素不变的情况下,对数组插入新的元素。
插入元素其实也可以被认为是对 0 个元素进行赋值。因此,指定 [*n*, 0] 后,就会在索引值为 *n* 的元素前插入新元素(图 13.8)。
![{%}](https://box.kancloud.cn/2015-10-26_562e01f4161e8.png)
**图 13.8 插入元素**
### **13.3.4 通过多个索引创建数组**
通过使用我们在 13.3.1 节中介绍的方法,虽然可以获取多个连续的元素,但是却不能获取分散的元素。而使用 `values_at` 方法,就可以利用多个索引来分散获取多个元素,并用它们创建新数组。
-
***a*.`values_at` (*n1*, *n2*, …)**
用这个方法,我们就可以每隔一个元素获取一次(图 13.9)。
![{%}](http://www.ituring.com.cn/figures/2015/Ruby4/20.d13z.010.png)
**图 13.9 获取分散的元素并创建数组**
### **13.4 作为集合的数组**
到目前为止的数组操作都是通过索引完成的。也就是说,从哪里获取元素、给哪个元素赋值、在哪里插入元素这些操作都是直接指定数组索引后进行的。
的确,数组、`Array` 类本来就是带有索引的对象,使用索引也是理所当然。不过有些时候我们会希望不通过索引而直接操作数组元素。
例如,我们可以把数组当成集合,这样一来,`Array` 类中的各元素就变了集合里的元素。
然而,由于集合没有顺序的概念,因此 `["a", "b", "c"]`、`["b", "c", "a"]`、`["c", "b", "a"]` 就都可以被认为是同一个集合。
这样操作数组时,如果我们还关心“这个对象是数组的第几个元素”之类的问题,就可能会造成混乱。这是因为,索引操作实际上只是数组封装的一个功能而已。
接下来,我们就来看看如何把数组当作集合使用。而在下一节中,我们会再讨论把数组当作列使用时的方法。
集合的基本运算分为交集和并集。
-
**取出同时属于两个集合的元素,并创建新的集合**
-
**取出两个集合中的所有元素,并创建新的集合**
我们把第 1 种集合称为交集,第 2 种集合成为并集。
-
**交集**……*ary* = *ary1* & *ary2*
-
**并集**……*ary* = *ary1* | *ary2*
图 13.10 描述的是 Ruby 数组中的交集和并集。
![{%}](http://www.ituring.com.cn/figures/2015/Ruby4/20.d13z.011.png)
**图 13.10 交集与并集**
集合还有另外一种运算——补集,即获取某个集合中不属于另外一个集合的元素。但是 `Array` 类的情况下,由于没有全集的概念,因此也就没有补集。不过 `Array` 类有把某个集合中属于另外一个集合的元素删除的差运算(图 13.11)。
- **集合的差**……*ary* = *ary1* - *ary2*
![{%}](http://www.ituring.com.cn/figures/2015/Ruby4/20.d13z.012.png)
**图 13.11 集合的差**
由于图 13.11 的数组 `ary2` 中包含的字符串 `"d"` 在数组 `ary1` 中并没有,因此不会被保留在执行结果中。
### **“|”与“+”的不同点**
连接数组除了可以使用 `|` 外还可以使用 `+`。这两种方法看起来比较相似,但是有相同元素时它们的效果就不一样了。
~~~
num = [1, 2,3]
even = [2, 4, 6]
p (num + even) #=> [1, 2, 3, 2, 4, 6]
p (num | even) #=> [1, 2, 3, 4, 6]
~~~
数组 `num` 与数组 `even` 都有元素 `2`。使用 `+` 时元素 `2` 会有两个,使用 `|` 时相同的元素只会有一个。
### **13.5 作为列的数组**
下面,我们来看看把数组对象当作列来看待时的情况。
数据结构的队列(queue)和栈(stack)都是典型的列结构。这两个相对的数据结构都有以下两种操作数据的方式。
-
**追加元素**
-
**获取元素**
队列是一种按元素被追加时的顺序来获取元素的数据结构(图 13.12(a))。这样的做法称为 FIFO(First-in First-out),也就是“先进先出”的意思。这与人们为等待某件事而排成一列时的情况一样,因此有时候也称为等待队列。
而栈则是一种按照与元素被追加时的顺序相反的顺序来获取元素的数据结构。这样的做法称为 LIFO(Last-in First-out),是一种“先进后出”的数据结构(图 13.12(b))。也就是说,在末尾追加元素,并从末尾开始获取元素。
![{%}](http://www.ituring.com.cn/figures/2015/Ruby4/20.d13z.013.png)
**图 13.12 队列与栈**
简单地说就是,按 A、B、C 的顺序保存数据时,按照 A、B、C 的顺序取得数据的数据结构就是队列,按照 C、B、A 的顺序取得数据的数据结构就是栈。
队列与栈都是比较复杂的数据结构,同时也是提高程序运行效率所不可欠缺的工具。
在数组的开头或末尾插入元素,或者从数组的开头或末尾获取元素等操作,是实现队列、栈等数据结构所必须的前提条件。Ruby 的数组封装了如表 13.1 所示的方法,因此可以很轻松地实现这些前提条件。
**表 13.1 操作数组开头与末尾的元素的方法**
<table border="1" data-line-num="288 289 290 291 292 293" width="90%"><thead><tr><th> <p class="表头单元格"> </p> </th> <th> <p class="表头单元格">对数组开头的元素的操作</p> </th> <th> <p class="表头单元格">对数组末尾的元素的操作</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格">追加元素</p> </td> <td> <p class="表格单元格"><code>unshift</code></p> </td> <td> <p class="表格单元格"><code>push</code></p> </td> </tr><tr><td> <p class="表格单元格">删除元素</p> </td> <td> <p class="表格单元格"><code>shift</code></p> </td> <td> <p class="表格单元格"><code>pop</code></p> </td> </tr><tr><td> <p class="表格单元格">引用元素</p> </td> <td> <p class="表格单元格"><code>first</code></p> </td> <td> <p class="表格单元格"><code>last</code></p> </td> </tr></tbody></table>
利用图 13.13 所示的 `push` 方法和 `shift` 方法可以实现队列,利用图 13.14 所示的 `push` 方法和 `pop` 方法可以实现栈。
![{%}](http://www.ituring.com.cn/figures/2015/Ruby4/20.d13z.014.png)
**图 13.13 队列**
![{%}](http://www.ituring.com.cn/figures/2015/Ruby4/20.d13z.015.png)
**图 13.14 栈**
要注意的是,`shift` 方法和 `pop` 方法不只是获取数组元素,而且还会把该元素从数组中删除。如果只是单纯地希望引用元素,则应该使用 `first` 方法和 `last` 方法。
~~~
a = [1, 2, 3, 4, 5]
p a.first #=> 1
p a.last #=> 5
p a #=> [1, 2, 3, 4, 5]
~~~
### **13.6 主要的数组方法**
数组方法有很多,下面我们将选取最常用的几种方法,并把具有相同功能的方法归纳在一起来分别加以介绍。
### **13.6.1 为数组添加元素**
-
***a*.`unshift` (*item*)**
将 *item* 元素添加到数组的开头。
~~~
a = [1, 2, 3, 4, 5]
a.unshift(0)
p a #=> [0, 1, 2, 3, 4, 5]
~~~
-
***a* << *item*
*a*.`push` (*item*)**
`<<` 与 `push` 是等价的方法,在数组 a 的末尾添加新元素 *item*。
~~~
a = [1, 2, 3, 4, 5]
a << 6
p a #=> [1, 2, 3, 4, 5, 6]
~~~
-
***a*.`concat` (*b*)
*a* + *b***
连接数组 *a* 和数组 *b*。`concat` 是具有破坏性的方法,而 `+` 则会根据原来的数组元素创建新的数组。
~~~
a = [1, 2, 3, 4, 5]
a.concat([8, 9])
p a #=> [1, 2, 3, 4, 5, 8, 9]
~~~
-
***a* [*n*] = *item*
*a* [*n..m*] = *item*
*a* [*n, len*] = *item***
把数组 *a* 指定的部分的元素替换为 *item*。
~~~
a = [1, 2, 3, 4, 5, 6, 7, 8]
a[2..4] = 0
p a #=> [1, 2, 0, 6, 7, 8]
a[1, 3] = 9
p a #=> [1, 9, 7, 8]
~~~
> **专栏**
> **具有破坏性的方法**
> 像 `pop` 方法、`shift` 方法那样,会改变接收者对象值的方法称为具有破坏性的方法。在使用具有破坏性的方法时需要特别小心,因为当有变量也引用了接收者对象时,如果接受者对象值发生了改变,变量值也会随之发生变化。我们来看看下面的例子。
~~~
a = [1, 2, 3, 4]
b = a
p b.pop #=> 4
p b #=> [1, 2, 3]
p a #=> [1, 2, 3]
~~~
> 执行 `pop` 方法删除元素后,变量 `a` 引用的数组的元素也被删除,从 `[1, 2, 3, 4]` 变为了 `[1, 2, 3]`,同时变量 `b` 引用的数组元素也被删除了。这是由于执行 `b = a` 后,并不是将变量 `a` 的内容复制给了变量 `b`,而是让变量 `b` 和变量 `a` 同时引用了一个对象。
> ![{%}](http://www.ituring.com.cn/figures/2015/Ruby4/20.d13z.016.png)
> 在 Ruby 的方法中,有像 `sort` 和 `sort!` 这样,在相同方法名后加上 `!` 的方法。为了区分方法是否具有破坏性,在具有破坏性的方法末尾添加 ! 这一做法目前已经成为了通用的规则。
### **13.6.2 从数组中删除元素**
根据某些条件从数组中删除元素。
-
***a*.`compact`
*a*.`compact!`**
从数组 *a* 中删除所有 `nil` 元素。`compact` 方法会返回新的数组,`compact!` 则直接替换原来的数组。`compact!` 方法返回的是删除 `nil` 元素后的 `a`,但是如果什么都没有删除的话就会返回 `nil`。
~~~
a = [1, nil, 3, nil, nil]
a.compact!
p a #=> [1, 3]
~~~
-
***a*.`delete`(*x*)**
从数组 *a* 中删除 *x* 元素。
~~~
a = [1, 2, 3, 2, 1]
a.delete(2)
p a #=> [1, 3, 1]
~~~
-
***a*.`delete_at`(*n*)**
从数组中删除 *a*[*n*] 元素。
~~~
a = [1, 2, 3, 4, 5]
a.delete_at(2)
p a #=> [1, 2, 4, 5]
~~~
-
***a*.`delete_if`{|*item*| … }
*a*.`reject`{|*item*| … }
*a*.`reject!`{|*item*| … }**
判断数组 *a* 中的各元素 *item*,如果块的执行结果为真,则从数组 *a* 中删除 *item*。`delete_if` 和 `reject!` 方法都是具有破坏性的方法。
~~~
a = [1, 2, 3, 4, 5]
a.delete_if{|i| i > 3}
p a #=> [1, 2, 3]
~~~
-
***a*.`slice!`(*n*)
*a*.`slice!`(*n..m*)
*a*.`slice!`(*n, len*)**
删除数组 *a* 中指定的部分,并返回删除部分的值。`slice!` 是具有破坏性的方法。
~~~
a = [1, 2, 3, 4, 5]
p a.slice!(1, 2) #=> [2, 3]
p a #=> [1, 4, 5]
~~~
-
***a*.`uniq`
*a*.`uniq!`**
删除数组 *a* 中重复的元素。`uniq!` 是具有破坏性的方法。
~~~
a = [1, 2, 3, 4, 3, 2, 1]
a.uniq!
p a #=> [1, 2, 3, 4]
~~~
-
***a*.`shift`**
删除数组 *a* 开头的元素,并返回删除的值。
~~~
a = [1, 2, 3, 4, 5]
a.shift #=> 1
p a #=> [2, 3, 4, 5]
~~~
-
***a*.`pop`**
删除数组 *a* 末尾的元素,并返回删除的值。
~~~
a = [1, 2, 3, 4, 5]
a.pop #=> 5
p a #=> [1, 2, 3, 4]
~~~
### **13.6.3 替换数组元素**
将数组元素替换为别的元素的方法中,也分为带 `!` 的和不带 `!` 的方法,前者是具有破坏性的会改变接收者对象值的方法,后者则是直接返回新数组的方法。
-
***a*.`collect`{|*item*| … }
*a*.`collect!`{|*item*| … }
*a*.`map`{|*item*| … }
*a*.`map!`{|*item*| … }**
将数组 *a* 的各元素 *item* 传给块,并用块处理过的结果创建新的数组。从结果来看,数组的元素个数虽然不变,但由于经过了块处理,因此数组的元素和之前会不一样。
~~~
a = [1, 2, 3, 4, 5]
a.collect!{|item| item * 2}
p a #=> [2, 4, 6, 8, 10]
~~~
-
***a*.`fill`(*value*)
*a*.`fill`(*value, begin*)
*a*.`fill`(*value, begin, len*)
*a*.`fill`(*value, n..m*)**
将数组 *a* 的元素替换为 *value*。参数为一个时,数组 *a* 的所有元素值都会变为 *value*。参数为两个时,从 *begin* 到数组末尾的元素值都会变为 *value*。参数为三个时,从 *begin* 开始 *len* 个元素的值会变为 *value*。另外,当第 2 个参数指定为 [*n..m*] 时,则指定范围内的元素值都会变为 *value*。
~~~
p [1, 2, 3, 4, 5].fill(0) #=> [0, 0, 0, 0 ,0]
p [1, 2, 3, 4, 5].fill(0, 2) #=> [1, 2, 0, 0, 0]
p [1, 2, 3, 4, 5].fill(0, 2, 2) #=> [1, 2, 0, 0, 5]
p [1, 2, 3, 4, 5].fill(0, 2..3) #=> [1, 2, 0, 0, 5]
~~~
-
***a*.`flatten`
*a*.`flatten!`**
平坦化数组 *a*。所谓平坦化是指展开嵌套数组,使嵌套数组变为一个大数组。
~~~
a = [1, [2, [3]], [4], 5]
a.flatten!
p a #=> [1, 2, 3, 4, 5]
~~~
-
***a*.`reverse`
*a*.`reverse!`**
反转数组 *a* 的元素顺序。
~~~
a = [1, 2, 3, 4, 5]
a.reverse!
p a #=> [5, 4, 3, 2, 1]
~~~
-
***a*.`sort`
*a*.`sort!`
*a*.`sort`{|*i, j*| … }
*a*.`sort!`{|*i, j*| … }**
对数组 *a* 进行排序。排序方法可以由块指定。没有块时,使用 `<=>` 运算符比较。
~~~
a = [2, 4, 3, 5, 1]
a.sort!
p a #=> [1, 2, 3, 4, 5]
~~~
关于如何使用块指定排序方法,在 11.2.3 节中我们已经介绍过了。
-
***a*.`sort_by`{|*i*| … }**
对数组 *a* 进行排序。根据块的运行结果对数组的所有元素进行排序。
~~~
a = [2, 4, 3, 5, 1]
p a.sort_by{|i| -i } #=> [5, 4, 3, 2, 1]
~~~
详情请参考 11.2.3 节。
### **13.7 数组与迭代器**
迭代器是实现循环处理的方法,而数组则是多个对象的集合。在对这些对象进行某种处理,或者取出某几个对象时,都需要大量用到迭代器。
例如,对数组的各元素进行相同的操作时使用的 `each` 方法就是典型的迭代器。该方法会遍历数组的所有元素,并对其进行特定的处理。
此外,接收者不是数组的情况下,为了让迭代器的执行结果能作为某个对象返回,也会用到数组。其中 `collect` 方法就是一个具有代表性的方法。`collect` 方法会收集某种处理的结果,并将其合并为一个数组后返回。
~~~
a = 1..5
b = a.collect{|i| i += 2}
p b #=> [3, 4, 5, 6, 7]
~~~
在上面的例子中,接收者为范围对象,而结果则是数组对象。像这样,迭代器和数组就被紧密地结合在一起了。
### **13.8 处理数组中的元素**
对数组中的元素进行处理时可以采取多种方法。
### **13.8.1 使用循环与索引**
传统的方法是使用循环,也就是在遍历数组的同时,利用索引逐个访问数组元素。
例如,在代码清单 13.1 中,我们把数组的元素逐个取出来并输出。
**代码清单 13.1 list.rb**
~~~
list = ["a", "b", "c", "d"]
for i in 0..3
print "第", i+1,"个元素是",list[i],"。\n"
end
~~~
代码清单 13.2 的程序是对数值数组的元素进行合计的例子。
**代码清单 13.2 sum_list.rb**
~~~
list = [1, 3, 5, 7, 9]
sum = 0
for i in 0..4
sum += list[i]
end
print "合计:",sum,"\n"
~~~
### **13.8.2 使用 each 方法逐个获取元素**
在数组中,通过 `each` 方法可以实现循环操作。下面,我们尝试使用 `each` 方法来改写代码清单 13.2(代码清单 13.3)。
**代码清单 13.3 sum_list2.rb**
~~~
list = [1, 3, 5, 7, 9]
sum = 0
list.each do |elem|
sum += elem
end
print "合计:",sum,"\n"
~~~
但是,使用 `each` 方法时,我们并不知道元素的索引值。因此,当需要指定元素的索引值时,可以使用 `each_with_index` 方法。
**代码清单 13.4 list2.rb**
~~~
list = ["a", "b", "c", "d"]
list.each_with_index do |elem, i|
print "第", i+1,"个元素是",elem,"。\n"
end
~~~
### **13.8.3 使用具有破坏性的方法实现循环**
如果数组内各元素全部处理完毕后该数组就不需要了,那么我们就可以通过逐个删除数组元素使数组变空这样的手段来实现循环。
~~~
while item = a.pop
## 对item 进行处理
end
~~~
假设在循环开始前已经有元素在数组 `a` 中。如果逐个删除数组 `a` 中的元素,就会对删除的元素进行处理。最后,当数组为空时,循环结束。
### **13.8.4 使用其他迭代器**
Ruby 中实现了不少像 `collect`、`map` 方法这样一眼就能看出其作用的基本操作。当希望创建某种迭代器时,请翻阅 Ruby 参考手册,一般情况下在里面都能找到我们需要的迭代器。这样就不会因为花精力创建了一个 Ruby 本来就有的迭代器而感到失望了。
### **13.8.5 创建专用的迭代器**
不过有时也可能会找不到自己想要的迭代器,这时就只能根据需要创建属于自己的迭代器了。
关于迭代器的创建,请参考 11.3 节。
### **13.9 数组的元素**
数组中可以存放各种各样的对象。除了数值、字符串外,我们还可以在数组对象中存放别的数组对象或散列对象等等。
### **13.9.1 使用简单的矩阵**
下面我们来看看用数组来表示矩阵的例子。
数组的各个元素也可以是数组,也就是所谓的数组的数组,这样的形式经常被用于表示矩阵。
例如,我们试试用数组的数组这种形式来表示图 13.15 那样的矩阵。
![{%}](http://www.ituring.com.cn/figures/2015/Ruby4/20.d13z.017.png)
**图 13.15 3 行 3 列的矩阵**
第 1 行为 `[1, 2, 3]`,第 2 行为 `[4, 5, 6]`,第 3 行为 `[7, ,8 , 9]`,把它们归纳为数组,如下所示。
~~~
a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
~~~
如果想取出元素 `6`,我们可以像下面这样做。
~~~
a[1][2]
~~~
首先用 `a[1]` 表示 `[4, 5, 6]` 这个数组,然后再指定第 3 位的元素,这样就能达到我们的目的了。
### **13.9.2 初始化时的注意事项**
把数组对象或者散列对象作为数组元素时,需要注意该对象初始化时的问题。
~~~
a = Array.new(3, [0, 0, 0])
~~~
在上面的例子中,我们可能会以为 `a` 为 `[[0, 0, 0], [0, 0, 0], [0, 0, 0]]`,但实际却是另外一个结果(图 13.16)。
![{%}](http://www.ituring.com.cn/figures/2015/Ruby4/20.d13z.018.png)
**图 13.16 数组的初始化**
像下面那样,原本只是打算变更第 1 行的第 2 个元素,结果所有行的第 2 个元素都发生了改变。
~~~
a = Array.new(3, [0, 0, 0])
a[0][1] = 2
p a #=> [[0, 2, 0], [0, 2, 0], [0, 2, 0]]
~~~
为了解决这个问题,我们可以指定 `new` 方法的块和元素个数。程序调用与元素个数一样次数的块,然后再将块的返回值赋值给元素。每次调用块都会生成新的对象,这样一来,各个元素引用同一个对象的问题就不会发生了。
~~~
a = Array.new(3) do
[0, 0, 0]
end
p a #=> [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
a[0][1] = 2
p a #=> [[0, 2, 0], [0, 0, 0], [0, 0, 0]]
~~~
进行下述操作后,对应的元素的索引值就会被赋值给 `i`,这样就可以根据索引值初始化出不同的值了。
~~~
a = Array.new(5){|i| i + 1 }
p a #=> [1, 2, 3, 4, 5]
~~~
### **13.10 同时访问多个数组**
接下来我们来看看用相同的索引值同时访问对多个数组时的情况。在代码清单 13.5 中,合计三个数组中索引值相同的元素,并将结果保存在新数组(result)中。
**代码清单 13.5 sum_with_each.rb**
~~~
ary1 = [1, 2, 3, 4, 5]
ary2 = [10, 20, 30, 40, 50]
ary3 = [100, 200, 300, 400, 500]
i = 0
result = []
while i < ary1.length
result << ary1[i] + ary2[i] + ary3[i]
i += 1
end
p result #=> [111, 222, 333, 444, 555]
~~~
使用 `zip` 方法可以程序变得更简单(代码清单 13.6)。
**代码清单 13.6 sum_with_zip.rb**
~~~
ary1 = [1, 2, 3, 4, 5]
ary2 = [10, 20, 30, 40, 50]
ary3 = [100, 200, 300, 400, 500]
result = []
ary1.zip(ary2, ary3) do |a, b, c|
result << a + b + c
end
p result #=> [111, 222, 333, 444, 555]
~~~
`zip` 方法会将接收器和参数传来的数组元素逐一取出,而且每次都会启动块。参数可以是一个也可以是多个。
> **专栏**
> **Enumerable 模块**
> 介绍完 `Comparable` 模块后,我们再来介绍一下常被用于 Mix-in 的 `Enumerable` 模块。Enumerable 的意思是“可以被计数的”、“可以被列举的”。在本书介绍过的类中,`Array`、`Dir`、`File`、`Hash`、`IO`、`Range`、`Enumerator` 等类中都包含了 `Enumerable` 模块。
> **表 Enumerable 模块定义的方法**
| `all?` | `any?` | `chunk` | `collect` |
|-----|-----|-----|-----|
| `collect_concat` | `count` | `cycle` | `detect` |
| `drop` | `drop_while` | `each_cons` | `each_entry` |
| `each_slice` | `each_with_index` | `each_with_object` | `entries` |
| `find` | `find_all` | `find_index` | `first` |
| `flat_map` | `grep` | `group_by` | `include?` |
| `inject` | `lazy` | `map` | `max` |
| `max_by` | `member?` | `min` | `min_by` |
| `minmax` | `minmax_by` | `none?` | `one?` |
| `partition` | `reduce` | `reject` | `reverse_each` |
| `select` | `slice_before` | `sort` | `sort_by` |
| `take` | `take_while` | `to_a` | `zip` |
> 本章中介绍的 `Array` 类的方法中,实际上有些是在 `Enumerable` 模块中定义的。关于上文中没有介绍的方法的用法,请参考 Ruby 参考手册。
> 就像 `Comparable` 模块需要 `<=>` 运算符那样,`Enumerable` 模块则需要 `each` 方法。例如,如果用 Ruby 来实现 `each_with_index` 方法的话,大概就是下面这样(实际的程序会更加复杂,例如会有没有块时返回 `Enumerator` 对象等处理)。
~~~
module Enumerable
def each_with_index
index = 0 # 初始化索引
each do |item|
yield(item, index) # 将元素与index 作为参数
# 执行块
index += 1 # 累加索引值
end
end
end
~~~
> 我们在创建提供循环处理的类的时候,可以首先创建迭代器 `each` 方法,然后再包含 `Enumerable` 模块,这样一来,上表的方法就都可以使用了。
# **练习题**
1. 创建一个数组 `a`,使 1 到 100 的整数按升序排列(即 `a[0]` 为 1,`a[99]` 为 100)。
2. 将 1 数组中的各元素扩大 100 倍,创建新数组 `a2`(即 `a2[0]` 为 100)。另外,不创建新数组,将原数组中的各元素都扩大 100 倍。
3. 获取 1 数组中值是 3 的倍数的元素,创建新数组 `a3`(即 `a3[0]` 为 3,`a3[2]` 为 9)。另外,不创建新数组,把 3 的倍数以外的元素全部删除。
4. 将 1 的数组按倒序排列。
5. 求 1 数组中的整数的和。
6. 从 1 到 100 的整数数组中,取出 10 个分别包含 10 个元素的数组,如 1 ~ 10,11 ~ 20,21 ~ 30。再将取出的全部数组按顺序保存到数组 `result` 中,请考虑以下代码中 `???` 的部分应该怎么写。
~~~
ary = [ 包含1~100 的整数数组 ]
result = Array.new
10.times do |i|
result << ary[???]
end
~~~
7. 定义方法 `sum_array`,合计数组 `nums1` 和 `nums2` 中相对应的各个元素的值,并将合计结果作为数组返回。
~~~
p sum_array([1, 2, 3], [4, 6, 8]) #=> [5, 8, 11]
~~~
> 参考答案:请到图灵社区本书的“随书下载”处下载([http://www.ituring.com.cn/book/1237](http://www.ituring.com.cn/book/1237))。
- 推荐序
- 译者序
- 前言
- 本书的读者对象
- 第 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 参考集
- 后记
- 谢辞