9.3 数组的构造

关于可以构造数组值的那些函数,首当其冲的肯定是Array类型附带的构造函数。

我们先说Array{T}(undef, dims)Array{T,N}(undef, dims)。这两个函数都是用来构造未初始化的 N 维数组的。其中的T依然代表元素类型,N依然代表维数。

undef是一个常量,它代表着单例类型UndefInitializer的唯一值。所谓的单例类型,是指有且仅有一个实例的类型。无论我们实例化这种类型多少次,都只会得到同一个值,即该类型的唯一值。UndefInitializer类型专用于数组的初始化,其值表达的含义是创建一个未初始化的数组。或者说它表达的是,上述构造函数的调用者不想向这个数组填充任何的元素值。这时,Julia 会在该数组的所有元素位置上填充随机值。

我们在前面讲了,数组类型的字面量上不会体现出数组在各个维度上的元素数量。然而,这些数量却是构造一个多维数组时必须要确定的信息。注意,对于多维数组,我们所说的在某个维度上的元素指的是可能一个个元素值,也可能是一个个低维数组。这在后面会有更详细的解释。

这里的参数dims的作用正是表示数组在各个维度上的元素数量。更确切地说,它表示的是各个维度的长度。dims是 dimensions 的缩写。它的值可以是一个包含了若干个整数的元组值,也可以是若干个由英文逗号分隔的整数值。不过后者只在三维及以下的数组构造中才有效。下面是一个例子:

  1. julia> Array{Int64}(undef, 4, 3, 2)
  2. 4×3×2 Array{Int64,3}:
  3. [:, :, 1] =
  4. 4683772848 4667574256 4667574256
  5. 4490317616 4667575152 4667574256
  6. 4490317616 4667574256 4667575152
  7. 4667574256 4490317616 4667574256
  8. [:, :, 2] =
  9. 4490317616 4667572800 0
  10. 4490317472 0 0
  11. 4667574256 0 0
  12. 4488855536 0 4680843264
  13. julia>

请注意,回显内容中表示的是一个4×3×2的三维数组。还记得吗?我们可以把三维数组比喻成一座停车楼。那么上面这个三维数组就相当于一个有 2 层的停车楼。现在,你要带着这个想象跟我一起理解它的展示格式。

图 9-2 4×3×2的三维数组的示意 图 9-2 4×3×2的三维数组的示意

回显内容的第一行反映了我们构造数组时给予的信息。第二行中的[:, :, 1]指的是在第三个维度上的第 1 个低维数组(即二维数组),相当于停车楼的上一层。你也可以把[:, :, 1]看成一个特殊的数组,其中的每一个元素的值都用于代表上述三维数组在对应维度上的某个低维数组。这个特殊的数组中的前两个元素都由英文冒号:占位,相当于选择了对应维度上的所有低维数组。而其中的最后一个元素值是1,代表的正是上述三维数组中的第 1 个二维数组。由此,在它下面才展示了对应的二维数组中的所有元素值,相当于俯瞰停车楼的上一层。

我们已经知道,只要 N 大于 1,那么 N 维数组就都可以被看做是由一个个尺寸相同的 N-1 维的数组拼接而成的结构,就像停车楼的每一层都是一个停车场那样。因此,在上述数组的第三个维度上的第 1 个低维数组就应该是一个4×3的二维数组。在[:, :, 1]下面的那 4 行内容展示的正是这个二维数组。其中的所有元素值都是由 Julia 自行填充的随机值。

又由于上述三维数组在第三个维度上的长度是 2,所以才有了再下面的[:, :, 2],以及与它对应的又一个4×3的二维数组,相当于停车楼的下一层。

让我们再来构造一个四维数组:

  1. julia> Array{Int64, 4}(undef, (4, 3, 2, 2))
  2. 4×3×2×2 Array{Int64,4}:
  3. [:, :, 1, 1] =
  4. 4688801328 4688801456 4688801680
  5. 4688801360 4688801488 4688801712
  6. 4688801392 4688801616 4688801744
  7. 4688801424 4688801648 4688801776
  8. [:, :, 2, 1] =
  9. 4688801808 4620636144 4688805040
  10. 4688801840 4688935952 4688805072
  11. 4688854576 4688991056 4688805104
  12. 4688935312 4688991088 4688986896
  13. [:, :, 1, 2] =
  14. 4688805264 4620632072 4688805456
  15. 4688987472 4688988016 4679072384
  16. 4688805328 4688988176 4679072480
  17. 4679071744 4688805424 4688989008
  18. [:, :, 2, 2] =
  19. 4688989104 4679073120 4679073520
  20. 4688989200 4679073216 4679073680
  21. 4688805584 4679073312 4679073728
  22. 4688989712 4688990032 4688796304
  23. julia>

四维数组可能会挑战到你的空间想象力。但有了前面的解释,这个四维数组的展示格式就应该容易理解一些了。这个四维数组由 2 个4×3×2的三维数组拼接而成,而这 2 个三维数组又分别由 2 个4×3的二维数组拼接而成。所以,[:, :, 1, 1]指的就是,这个四维数组中的第 1 个三维数组中的第 1 个二维数组。而[:, :, 2, 1]指的则是,这个四维数组中的第 1 个三维数组中的第 2 个二维数组。以此类推。紧挨在它们下面的那几行内容展示的就是对应的二维数组。你明白了吗?你可以再花一些时间思考一下。

为什么 Julia 会这样展示多维数组呢?这主要是因为,我们在平面(如屏幕、纸张等)之上最多只能铺开二维的数组。虽然我们也可以在纸上画出三维的物体(如六面体、球体等),但那终归只是一种视觉上的效果。而且,那些物体只能被当作图形来看待,很难完全用普通的文本直观地展示出来。即使我们生活在三维的世界里,可所用的文字和语言都只是二维的。这也是我们不容易理解四维以及更多维数的原因。总之,Julia 在用二维的方式展示多维数组。它把多维数组拆分成了一个个二维数组,并以普通文本的形式摆在我们面前。

言归正传。上例调用的是Array{T,N}(undef, dims)。这时我们需要注意,替代N的那个整数值一定要等同于替换掉dims的那个元组值的长度(或者替换掉dims的那些整数值的数量),否则 Julia 就会立即报错。因为两边给定的数组维数不一致。

在前面,我们传给数组构造函数的第一个参数值一直是undef。但这只是初始化元素值的一种选项而已。我们还可以选择nothingmissing作为这个参数的值。但前提是,该数组的元素类型必须是nothingmissing的类型的超类型。

nothingmissing也都是常量,其含义同样比较特殊。我们在前面的章节中对它们都做过解释。nothing代表着单例类型Nothing的唯一值,它的含义是“此处没有值”。而missing则代表单例类型Missing的唯一值,它的含义是“此处的值是缺失的”。

那怎样设定数组的元素类型才能让它成为NothingMissing的超类型呢?这个时候,Union类型就派上用场了。不要忘了,它的字面量可以表达多个类型的联合。因此,我们把元素类型设定为Union{Nothing, String}就意味着该数组的元素值既可以是一个字符串值,也可以是nothing。对于Missing来说也是类似的。下面是一些使用示例:

  1. julia> Array{Union{Nothing, String}}(nothing, 2, 2)
  2. 2×2 Array{Union{Nothing, String},2}:
  3. nothing nothing
  4. nothing nothing
  5. julia> Array{Union{Missing, Int64}}(missing, 2, 3)
  6. 2×3 Array{Union{Missing, Int64},2}:
  7. missing missing missing
  8. missing missing missing
  9. julia>

可以看到,如果我们传给数组构造函数的第一个参数值是nothing,那么此次被创建出的数组的所有元素值就都会是nothing。若传入missing的话也是类似的。

除了上面讲的构造函数,Julia 还提供了另外的一些可以创建多维数组的函数。比如,函数zeros可以创建元素值全为零值的数组。示例如下:

  1. julia> zeros(Int32, 4, 3)
  2. 4×3 Array{Int32,2}:
  3. 0 0 0
  4. 0 0 0
  5. 0 0 0
  6. 0 0 0
  7. julia> zeros(Float32, 4, 3)
  8. 4×3 Array{Float32,2}:
  9. 0.0 0.0 0.0
  10. 0.0 0.0 0.0
  11. 0.0 0.0 0.0
  12. 0.0 0.0 0.0
  13. julia>

zeros函数的第一个参数的名称是T,代表元素类型。这个参数是可选的,如果我们选择不为它传入值,那么其值就是缺省的Float64。该函数的第二个参数的名称是dims,与前述的构造函数中的dims含义相同。

注意,这个函数的第一个参数值通常只能是一个数值类型。更具体地说,它可以是任意的布尔类型、整数类型、浮点数类型、复数类型,以及有理数类型。另外,对于不同的数值类型,其零值也是不同的。所谓的零值,就是用来表示0的值。比如,UInt8类型的零值是0x00Complex类型的零值是0+0imRational类型的零值是0//1,等等。

与之类似,ones函数可以创建元素值全为1的数组。其参数的定义与zeros函数的参数定义相同。仍要注意,不同的数值类型表示1的方式也不同。

还有一个名叫fill的函数,它有两个参数:x‌和dims。参数x代表的值将会被填充到新数组的所有元素位置上。显然,新数组的元素类型由x决定。与前面一样,新数组的维数和大小仍由dims决定。下面是一个示例:

  1. julia> fill(1.0f-3, 2, 3)
  2. 2×3 Array{Float32,2}:
  3. 0.001 0.001 0.001
  4. 0.001 0.001 0.001
  5. julia>

另外,函数truesfalses也很常用。它们都只有一个名为dims的参数。trues函数用于创建元素值全为true的数组,而falses函数则用于创建元素值全为false的数组。注意,它们创建的数组的类型并不是Array,而是BitArray

BitArray类型也被称为位数组类型。它是元素类型为BoolArray类型的优化版本。它仅使用 1 个比特来存储一个元素值。要知道,在通常情况下,Bool类型的每一个值都需要占用 8 个比特。这就意味着,位数组在存储空间的利用率方面有着 8 倍的提升。为了与标准的存储方式保持兼容,从位数组取出的元素值会被还原成(新的)常规的布尔值。

以上就是我们构造数组值的时候经常会用到的函数。当然,还有一些函数也可以被用来构造数组值,如函数randrandncollectsimilarreinterpret等。不过,这些函数在功能上就没有那么的纯粹了。