数组(Array)

array0.rb

什么是数组(Array)?

数组是每个元素都可以被索引到的有序集合。在 Ruby 中,(与许多其它语言不同)一个 Array 可以包含不同类型的元素,例如字符串、整数和浮点数,甚至是方法的返回值。 a1 = [1, ‘two’, 3.0, array_length(a0)]数组中的第一个项目的索引为 0,这意味着数组中的最后一个元素的索引为数组中元素总数减 1。如上所示,给定一个数组 a1,然后访问其第一个和最后一个元素: a1[0] # returns 1st item (at index 0) a1[3] # returns 4th item (at index 3)

我们已经使用了数次数组,例如在第二章的 2adventure.rb 中我们使用了一个数组来存储房间的地图:

  1. mymap = Map.new([room1, room2, room3])

创建数组

与其他许多编程语言一样,Ruby 使用方括号来界定数组。你可以使用逗号分隔多个值很容易的创建一个数组,并将其赋给一个变量。

  1. arr = ['one','two','three','four']
array1.rb

与 Ruby 中其它的东西一样,数组也是对象。你可能会猜到,正如字符串一样,它由 Array 类定义,索引从 0 开始。你可以将索引放在方括号中得到相应元素,如果索引无效,将会返回 nil

  1. arr = ['a', 'b', 'c']
  2. puts(arr[0]) # shows "a"
  3. puts(arr[1]) # shows "b"
  4. puts(arr[2]) # shows "c"
  5. puts(arr[3]) # nil
array2.rb

在数组中混合数据类型是被允许的,甚至也可以包含一些产生值的表达式。假设你创建了这个方法:

  1. def hello
  2. return "hello world"
  3. end

你可以这样声明一个数组:

  1. x = [1+2, hello, `dir`]

这里,第一个元素是整数 3,第二个元素是字符串 “hello world”(由 hello 方法返回)。如果你在 Windows 上运行,第三个数组将是一个包含目录列表的字符串。这是因为 dir 反引号字符串是可以被操作系统执行的命令(见第三章)。因此,数组中的最后一个位置将被 dir 命令返回的文件名字符串填充。如果你是运行在不同的操作系统上,这时候应该替换一个合适的命令。

dir_array.rb

创建一个文件名的数组

许多 Ruby 类有返回值为数组的方法。例如,Dir 类用来执行在磁盘上目录操作,拥有entries方法。传递给该方法一个目录名称,将会返回一个包含文件名列表的数组。

Dir.entries(‘C:\‘) # returns an array of files in C:\

如果你要创建一个包含单引号字符串的数组,但是输入所有引号又很麻烦,一种简洁的方式就是使用 %w 和将不带引号的字符串以空格分隔放入圆括号中的形式表示(或者使用 %W 表示双引号字符串,如第三章所述):

array2.rb
  1. y = %w(this is an array of strings)

你也可以使用通常的构造器来(new)创建一个数组,你可以同时将一个整数传递给构造方法,来创建一个特定大小(每个元素值为 nil)的数组。当然,你也可以传递两个参数,第一个参数指定数组大小,第二个参数指定要放入数组中的元素:

  1. a = Array.new # an empty array
  2. a = Array.new(2) # [nil,nil]
  3. a = Array.new(2, "hello world") # ["hello world", "hello world"]

多维数组

要创建一个多维数组,你可以先创建一个数组,然后再将其它数组作为元素放入该数组中。例如,这将创建一个包含两个数组元素的数组。

  1. a = Array.new(2)
  2. a[0]= Array.new(2,'hello')
  3. a[1]= Array.new(2,'world')

你还可以将数组对象作为参数传递给数组的 new 方法来创建多维数组。不过要注意,虽然在传递数组参数时不适用圆括号是可以的,但你如果不在方法名和参数之间加入空格,Ruby 将认为这是一个语法错误,所以在传递参数时,请一定要使用圆括号。

也可以使用方括号将数组嵌套在一起。这是创建了一个包含四个数组元素的 2 维数组,每个数组元素包含四个整数元素:

  1. a = [ [1,2,3,4],
  2. [5,6,7,8],
  3. [9,10,11,12],
  4. [13,14,15,16] ]

在上面显示的代码中,我将四个子数组分别放在不同行中,这并不是强制性的,但这样的写法有助于构建多元化的数组结构,通过将每个子数组显示为一行,类似电子表格中的行。当谈到数组中的数组时,可以很方便的将每个子数组引用为外层数组的行。

multi_array.rb

有关更多的使用多维数组的示例,请加载 multi_array.rb 程序。首先创建了包含另外两个数组的多维数组 multiarr,而这两个数组中在多维数组中的索引分别为 0 和 1。

  1. multiarr = [['one','two','three','four'],[1,2,3,4]]

数组迭代

你可以使用 for 循环来遍历数组访问数组中的元素,循环将会遍历位于索引 0 和 1 处的子数组两个元素:

  1. for i in multiarr
  2. puts(i.inspect)
  3. end

将会输出:

  1. >["one", "two", "three", "four"]
  2. [1, 2, 3, 4]

那么,你如何子数组中的元素呢?如果元素数量是固定的,你可以指定多个不同迭代变量,这时将会匹配子数组中对应索引位置的元素。

这两个子数组有四个元素,所以你可以使用四个迭代变量:

  1. for (a,b,c,d) in multiarr
  2. print("a=#{a}, b=#{b}, c=#{c}, d=#{d}\n" )
  3. end

迭代器和 for 循环

for 循环中的代码对每一个迭代元素进行执行,语法可以总结如下: for in do end当提供多个变量时,会将这些变量传递给代码里面的 for...end 块,如同给方法传递参数一样。在这里,你可以将 (a,b,c,d) 作为四个参数进行初始化,每一次匹配 for 循环所遍历的多维数组 multiarr 的每一行: for (a,b,c,d) in multiarr print(“a=#{a}, b=#{b}, c=#{c}, d=#{d}\n” ) end我们将在下一章中更深入地研究 for 循环和其他迭代器。
multi_array2.rb

您还可以使用 for 循环来单独迭代每个子数组中的所有元素:

  1. for s in multiarr[0]
  2. puts(s)
  3. end
  4. for s in multiarr[1]
  5. puts(s)
  6. end

以上两种技术(多个迭代变量和多个 for 循环)都需要满足两个条件:a)你需要知道多维数组有几行或者几列;b)每个子数组都包含相同数量的元素。

为了更灵活的迭代多维数组,你可以使用嵌套的 for 循环。一个外部循环遍历每一行,内部循环则遍历当前行中的元素。这种技术在子数组有不同数量元素时都可以正常运行:

  1. for row in multiarr
  2. for item in row
  3. puts(item)
  4. end
  5. end

数组索引

与字符串一样(参见第三章),你可以使用负数从末尾开始索引元素,也可以使用范围来索引:

array_index.rb
  1. arr = ['h','e','l','l','o',' ','w','o','r','l','d']
  2. print( arr[0,5] ) #=> "hello"
  3. print( arr[-5,5 ] ) #=> "world"
  4. print( arr[0..4] ) #=> "hello"
  5. print( arr[-5..-1] ) #=> "world"

注意,与字符串一样,当提供两个整数以返回一个来自数组的连续几项的元素,第一个整数作为起始索引,第二个则是元素数目(并非终止索引):

  1. arr[0,5] # returns 5 chars - ["h", "e", "l", "l", "o"]
array_assign.rb

你也可以利用索引来进行数组中元素的赋值,例如,我们首先创建一个空的数组,然后对索引为 0,1 和 3 的位置进行赋值,而没有赋值的索引为 2 的位置将填充一个默认值 nil

  1. arr = []
  2. arr[0] = [0]
  3. arr[1] = ["one"]
  4. arr[3] = ["a", "b", "c"]
  5. # arr now contains:
  6. # [[0], ["one"], nil, ["a", "b", "c"]]

同样地,你也可以使用范围,负索引等:

  1. arr2 = ['h','e','l','l','o',' ','w','o','r','l','d']
  2. arr2[0] = 'H'
  3. arr2[2,2] = 'L', 'L'
  4. arr2[4..6] = 'O','-','W'
  5. arr2[-4,4] = 'a','l','d','o'
  6. # arr2 now contains:
  7. # ["H", "e", "L", "L", "O", "-", "W", "a", "l", "d", "o"]

数组拷贝

array_copy.rb

注意,如果你使用赋值运算符 = 将一个数组变量赋值给另一个变量时,你实际上只是将该数组的引用赋值给了另一个变量,并没有真正复制该数组。你可以使用 clone 方法来为该数组创建一个副本:

  1. arr1 = ['h','e','l','l','o',' ','w','o','r','l','d']
  2. arr2 = arr1
  3. # arr2 is now the same as arr1. Change arr1 and arr2 changes too!
  4. arr3 = arr1.clone
  5. # arr3 is a copy of arr1. Change arr1 and arr2 is unaffected

数组比较

array_compare.rb

关于比较运算符 <=> 需要额外说几句。这里我们比较两个数组,称之为 arr1arr2;如果 arr1 小于 arr2,则返回 -1; 如果 arr1arr2 相等,它返回 0; 如果 arr2 大于 arr1,则返回 1。但是,Ruby 是如何确定一个数组是“大于”还是“小于”另一个数组?事实证明,Ruby 会将两个数组中相同索引位置上的元素进行比较。当遇到两个元素值不相等时,将返回其比较结果。换句话说,如果进行了这种比较:

  1. [0, 10, 20] <=> [0, 20, 20]

将会返回 -1(第一个数组小于第二个数组),因为在索引为 1 时第一个数组中的值(10)小于第二个数组中的值(20)。

如果要比较字符串数组,则对字符串的 ASCII 值进行比较。如果一个数组比另一个数组长,并且两个数组中的元素都相等,那么较长的数组被认为“更大”。但是,如果短数组中的元素值有比长数组的元素值大的,则认为短数组更大。

数组排序

array_sort.rb

sort 方法使用比较运算符 <=> 来比较相邻的数组元素。该运算符在许多 Ruby 类中都有定义,包括数组(Array)、字符串(String)、浮点数(Float)、日期(Date)和 Fixnum。但是,sort 运算并没有为所有类定义(也就是说,派生出其它所有类的 Object 类中 sort 没有定义)。其中令人遗憾的是,它不能用于对包含 nil 值的数组进行排序。但是,这个可以通过定义你自己的排序例程来解决。通过给 sort 方法发生一个块(block)来实现。我们将在第 10 章详细介绍块(blocks)。现在,只需要知道这里的块(block)是一段决定了 sort 方法如何进行元素比较的代码就足够了。

这是我的 sort 例程:

  1. arr.sort {
  2. |a,b|
  3. a.to_s <=> b.to_s
  4. }

这里的 arr 代表一个数组,变量 ab 代表两个连续的元素。我已经使用 to_s 方法将每个变量转换成了字符串;这样就会将 nil 转换成一个排序时认为更小的空字符串。注意,虽然我的 block 定义了数组的排序顺序,但不会改变数组元素自身。所以,nil 依然为 nil,整数(integers)依然为整数。字符串的转换操作只用于实现元素比较,不会改变数组元素。

比较值

这个比较运算符 <=>(实际上是一个方法)在 Ruby 名为 Comparable 的模块中定义的。现在,你可以将模块(module)视为一种可重用的排序代码库。我们将在第 12 章中更详细地研究模块。

你可以在自己的类中包含(include)Comparable 模块。这样你就可以覆盖掉 <=> 方法,去实现特定类型对象之间比较的准确方式。例如,你可能想子类化 Array,以便仅基于两个数组的长度进行比较,而不是数组中的每个元素值(如前所述,这是默认的)。下面来看看如何做到这一点:

comparisons.rb
  1. class MyArray < Array
  2. include Comparable
  3. def <=> ( anotherArray )
  4. self.length <=> anotherArray.length
  5. end
  6. end

现在,你可以初始化两个 MyArray 对象:

  1. myarr1 = MyArray.new([0,1,2,3])
  2. myarr2 = MyArray.new([1,2,3,4])

你可以使用在 MyArray 类中定义的 <=> 方法来进行比较:

  1. # Two MyArray objects
  2. myarr1 <=> myarr2 # returns 0

返回 0 表示两个数组相等(因为我们的 <=> 方法仅根据长度来进行比较是否相等)。另一方面,我们也可以用相同的整数初始化两个标准数组(Arrays),用 Array 类自己的 <=> 方法来执行比较:

  1. # Two Array objects
  2. arr1 <=> arr2 # returns -1

这里的 -1 代表第一个数组小于第二个数组,因为 Array 类的 <=> 比较得出 arr1 中的元素数值小于 arr2 中相同索引位置上的元素数值。

但是,如果你想直接使用“小于”、“等于”、“大于”这些常规运算符进行比较:

  1. < # less than
  2. == # equal to
  3. > # greater than

在 MyArray 类中,我们可以在不编写任何额外代码的情况下进行比较。这是由于已包含在 MyArray 类中的 Comparable 模块自动提供了这三种比较方法; 每种方法都根据 <=> 方法的定义进行比较。因为我们的 <=> 方法基于元素数量进行判断,所以 ‘<’ 方法在第一个数组较短时返回 true,== 在两个数组长度相等时返回 true,> 方法在第二个数组较短时返回 true。

  1. p( myarr1 < myarr2 ) #=> false
  2. p( myarr1 == myarr2 ) #=> true

但是,标准 Array 类不包含 Comparable 模块。因此,如果您尝试使用 <==> 比较两个普通数组,Ruby 将显示一个错误消息,告诉您该方法未定义。

事实证明,很容易将这三种方法添加到 Array 的子类中。 所有你要做的就是包含(include)Comparable 模块,像这样:

  1. class Array2 < Array
  2. include Comparable
  3. end

现在 Array2 类将基于 Array 的 <=> 方法进行比较,也就是比较数组中的每一个元素,而不是数组的长度。假设有 Array2 对象,arr1arr2,用之前我们用于 myarr1myarr2 的同样的数组进行初始化,我们可以看到这些结果:

  1. p( arr1 < arr2 ) #=> true
  2. p( arr1 > arr2 ) #=> false

数组方法

array_methods.rb

一些标准数组方法会修改数组本身,而不是返回修改了的数组副本。这些不仅包括那些标有末尾感叹号的方法,例如 flatten!compact!,还有 << 方法通过添加右边的数组到左边的数组来改变原数组,clear 方法会移除数组中的所有元素,以及 deletedelete_at 方法将会移除所选元素。