集合(Scala 2.8 - 2.12)

数组

语言

Array 是 Scala 中一种特殊的集合。一方面,Scala 数组与 Java 数组一一对应。也就是说,Scala 数组 Array[Int] 表示为 Java int[]Array[Double] 表示为 Java double[]Array[String] 表示为 Java String[]。但同时,Scala 数组提供的功能远超其 Java 类似项。首先,Scala 数组可以是泛型。也就是说,你可以拥有 Array[T],其中 T 是类型参数或抽象类型。其次,Scala 数组与 Scala 序列兼容 - 你可以在需要 Seq[T] 的地方传递 Array[T]。最后,Scala 数组还支持所有序列操作。以下是一个示例

scala> val a1 = Array(1, 2, 3)
a1: Array[Int] = Array(1, 2, 3)
scala> val a2 = a1 map (_ * 3)
a2: Array[Int] = Array(3, 6, 9)
scala> val a3 = a2 filter (_ % 2 != 0)
a3: Array[Int] = Array(3, 9)
scala> a3.reverse
res0: Array[Int] = Array(9, 3)

鉴于 Scala 数组的表示方式与 Java 数组相同,如何在 Scala 中支持这些附加特性?事实上,这个问题的答案在 Scala 2.8 和更早版本之间有所不同。以前,Scala 编译器在需要时通过一个称为装箱和拆箱的过程对数组和 Seq 对象进行“神奇”的包装和解包。其细节相当复杂,尤其是在创建泛型类型 Array[T] 的新数组时。有一些令人费解的特殊情况,并且数组操作的性能并不完全可预测。

Scala 2.8 的设计简单得多。几乎所有编译器魔法都消失了。相反,Scala 2.8 数组实现系统地使用了隐式转换。在 Scala 2.8 中,数组不会假装序列。它实际上不可能那样,因为本机数组的数据类型表示不是 Seq 的子类型。相反,数组与 scala.collection.mutable.WrappedArray 类实例之间存在隐式的“包装”转换,它是 Seq 的子类。您可以在此看到它的实际应用

scala> val seq: Seq[Int] = a1
seq: Seq[Int] = WrappedArray(1, 2, 3)
scala> val a4: Array[Int] = seq.toArray
a4: Array[Int] = Array(1, 2, 3)
scala> a1 eq a4
res1: Boolean = true

上面的交互演示了数组与序列兼容,因为存在从数组到 WrappedArray 的隐式转换。要反向进行,从 WrappedArrayArray,您可以使用在 Traversable 中定义的 toArray 方法。上面 REPL 的最后一行显示,使用 toArray 进行包装然后解包会得到您最初开始使用的相同数组。

还有另一个隐式转换应用于数组。此转换只是简单地“添加”所有序列方法到数组,但不会将数组本身变成序列。“添加”意味着数组被包装在另一个 ArrayOps 类型对象中,该对象支持所有序列方法。通常,此 ArrayOps 对象的生存期很短;在调用序列方法后通常无法访问它,并且可以回收其存储。现代 VM 通常完全避免创建此对象。

数组上两个隐式转换之间的差异在下一个 REPL 对话中显示

scala> val seq: Seq[Int] = a1
seq: Seq[Int] = WrappedArray(1, 2, 3)
scala> seq.reverse
res2: Seq[Int] = WrappedArray(3, 2, 1)
scala> val ops: collection.mutable.ArrayOps[Int] = a1
ops: scala.collection.mutable.ArrayOps[Int] = [I(1, 2, 3)
scala> ops.reverse
res3: Array[Int] = Array(3, 2, 1)

您会看到对 seq(它是 WrappedArray)调用 reverse 会再次得到 WrappedArray。这是合乎逻辑的,因为包装数组是 Seq,并且对任何 Seq 调用 reverse 都会再次得到 Seq。另一方面,对 ArrayOps 类的 ops 值调用 reverse 会得到 Array,而不是 Seq

上面的 ArrayOps 示例非常人为,只是为了显示与 WrappedArray 的差异。通常,您永远不会定义 ArrayOps 类的值。您只需在数组上调用 Seq 方法

scala> a1.reverse
res4: Array[Int] = Array(3, 2, 1)

通过隐式转换自动插入 ArrayOps 对象。因此,上面的行等效于

scala> intArrayOps(a1).reverse
res5: Array[Int] = Array(3, 2, 1)

其中 intArrayOps 是之前插入的隐式转换。这引发了一个问题,即编译器如何选择 intArrayOps 而非上面一行中 WrappedArray 的另一个隐式转换。毕竟,这两个转换都将数组映射到支持反向方法的类型,而这是输入指定的。该问题的答案是这两个隐式转换具有优先级。ArrayOps 转换的优先级高于 WrappedArray 转换。第一个在 Predef 对象中定义,而第二个在类 scala.LowPriorityImplicits 中定义,后者由 Predef 继承。子类和子对象中的隐式转换优先于基类中的隐式转换。因此,如果两个转换都适用,则选择 Predef 中的转换。字符串也有非常类似的方案。

现在您知道数组如何与序列兼容以及如何支持所有序列操作。泛型性如何?在 Java 中,您无法编写 T[],其中 T 是类型参数。那么 Scala 的 Array[T] 如何表示?实际上,像 Array[T] 这样的泛型数组在运行时可以是 Java 的八种基本数组类型 byte[]short[]char[]int[]long[]float[]double[]boolean[] 中的任何一种,或者可以是对象数组。唯一涵盖所有这些类型的通用运行时类型是 AnyRef(或等效的 java.lang.Object),因此这是 Scala 编译器将 Array[T] 映射到的类型。在运行时,当访问或更新类型为 Array[T] 的数组元素时,会有一系列类型测试来确定实际数组类型,然后在 Java 数组上执行正确的数组操作。这些类型测试会稍微减慢数组操作的速度。您可以预期对泛型数组的访问速度比对基本数组或对象数组的访问速度慢三到四倍。这意味着如果您需要最大性能,则应优先使用具体数组而不是泛型数组。但是,表示泛型数组类型还不够,还必须有一种方法来创建泛型数组。这是一个更难的问题,需要您提供一些帮助。为了说明这个问题,请考虑以下尝试编写创建数组的泛型方法:

// this is wrong!
def evenElems[T](xs: Vector[T]): Array[T] = {
  val arr = new Array[T]((xs.length + 1) / 2)
  for (i <- 0 until xs.length by 2)
    arr(i / 2) = xs(i)
  arr
}

evenElems 方法返回一个新数组,该数组包含参数向量 xs 中所有位于偶数位置的元素。evenElems 主体的第一行创建结果数组,该数组具有与参数相同的元素类型。因此,根据 T 的实际类型参数,这可以是 Array[Int],或 Array[Boolean],或 Java 中其他一些基本类型的数组,或某个引用类型的数组。但这些类型都有不同的运行时表示,那么 Scala 运行时将如何选择正确的类型呢?事实上,它无法根据给定的信息进行选择,因为与类型参数 T 对应的实际类型在运行时被擦除了。这就是为什么如果你编译上面的代码,你会收到以下错误消息

error: cannot find class manifest for element type T
  val arr = new Array[T]((arr.length + 1) / 2)
            ^

这里需要你通过提供 evenElems 的实际类型参数是什么的运行时提示来帮助编译器。此运行时提示采用类型为 scala.reflect.ClassTag 的类清单的形式。类清单是一个类型描述符对象,它描述了类型的顶级类是什么。作为类清单的替代,还有类型为 scala.reflect.Manifest 的完整清单,它描述了类型的各个方面。但对于数组创建,只需要类清单。

如果你指示 Scala 编译器这样做,它会自动构建类清单。“指示”意味着你要求类清单作为隐式参数,如下所示

def evenElems[T](xs: Vector[T])(implicit m: ClassTag[T]): Array[T] = ...

使用替代的和更短的语法,你还可以通过使用上下文绑定来要求类型带有类清单。这意味着在类型后面加上冒号和类名 ClassTag,如下所示

import scala.reflect.ClassTag
// this works
def evenElems[T: ClassTag](xs: Vector[T]): Array[T] = {
  val arr = new Array[T]((xs.length + 1) / 2)
  for (i <- 0 until xs.length by 2)
    arr(i / 2) = xs(i)
  arr
}

evenElems 的两个修订版本完全相同。在任何情况下发生的事情是,当构建 Array[T] 时,编译器将查找类型参数 T 的类清单,即它将查找类型为 ClassTag[T] 的隐式值。如果找到这样的值,则使用该清单构建正确类型的数组。否则,你会看到类似于上面内容的错误消息。

以下是使用 evenElems 方法的一些 REPL 交互。

scala> evenElems(Vector(1, 2, 3, 4, 5))
res6: Array[Int] = Array(1, 3, 5)
scala> evenElems(Vector("this", "is", "a", "test", "run"))
res7: Array[java.lang.String] = Array(this, a, run)

在这两种情况下,Scala 编译器都会自动为元素类型(首先是 Int,然后是 String)构建一个类清单,并将其传递给 evenElems 方法的隐式参数。编译器可以对所有具体类型执行此操作,但如果参数本身是另一个没有其类清单的类型参数,则无法执行此操作。例如,以下操作将失败

scala> def wrap[U](xs: Vector[U]) = evenElems(xs)
<console>:6: error: No ClassTag available for U.
     def wrap[U](xs: Vector[U]) = evenElems(xs)
                                           ^

这里发生的情况是 evenElems 要求类型参数 U 的类清单,但未找到任何类清单。在这种情况下,解决方案当然是要求 U 的另一个隐式类清单。因此,以下操作有效

scala> def wrap[U: ClassTag](xs: Vector[U]) = evenElems(xs)
wrap: [U](xs: Vector[U])(implicit evidence$1: scala.reflect.ClassTag[U])Array[U]

此示例还显示 U 定义中的上下文绑定只是此处名为 evidence$1 的类型为 ClassTag[U] 的隐式参数的速记。

总之,泛型数组创建需要类清单。因此,每当创建类型参数 T 的数组时,还需要提供 T 的隐式类清单。最简单的方法是用 ClassTag 上下文绑定声明类型参数,如 [T: ClassTag]

此页面的贡献者