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 序列兼容 - 您可以传递 Array[T]
,其中需要 Seq[T]
。最后,Scala 数组还支持所有序列操作。以下是它的一个操作示例
scala> val a1 = Array(1, 2, 3)
val a1: Array[Int] = Array(1, 2, 3)
scala> val a2 = a1.map(_ * 3)
val a2: Array[Int] = Array(3, 6, 9)
scala> val a3 = a2.filter(_ % 2 != 0)
val a3: Array[Int] = Array(3, 9)
scala> a3.reverse
val res0: Array[Int] = Array(9, 3)
鉴于 Scala 数组的表示方式与 Java 数组完全相同,Scala 中如何支持这些附加功能?Scala 数组实现系统地使用了隐式转换。在 Scala 中,数组并不假装是序列。它实际上不可能那样,因为本机数组的数据类型表示不是 Seq
的子类型。相反,数组和类 scala.collection.mutable.ArraySeq
(Seq
的子类)之间存在一个隐式的“包装”转换。您可以在此看到它的操作
scala> val seq: collection.Seq[Int] = a1
val seq: scala.collection.Seq[Int] = ArraySeq(1, 2, 3)
scala> val a4: Array[Int] = seq.toArray
val a4: Array[Int] = Array(1, 2, 3)
scala> a1 eq a4
val res1: Boolean = false
上面的交互演示了数组与序列兼容,因为数组到 ArraySeq
的隐式转换。从 ArraySeq
到 Array
,可以使用 Iterable
中定义的 toArray
方法。上面 REPL 的最后一行显示了使用 toArray
进行包装和解包会生成原始数组的副本。
还有另一个隐式转换应用于数组。此转换只是“添加”所有序列方法到数组,但不会将数组本身变成序列。“添加”意味着数组被包装在另一个类型为 ArrayOps
的对象中,该对象支持所有序列方法。通常,此 ArrayOps
对象是短命的;在调用序列方法后通常无法访问它,并且可以回收其存储。现代 VM 通常完全避免创建此对象。
数组上的两个隐式转换之间的差异在下一个 REPL 对话中显示
scala> val seq: collection.Seq[Int] = a1
val seq: scala.collection.Seq[Int] = ArraySeq(1, 2, 3)
scala> seq.reverse
val res2: scala.collection.Seq[Int] = ArraySeq(3, 2, 1)
scala> val ops: collection.ArrayOps[Int] = a1
val ops: scala.collection.ArrayOps[Int] = scala.collection.ArrayOps@2d7df55
scala> ops.reverse
val res3: Array[Int] = Array(3, 2, 1)
您会看到对 seq
(一个 ArraySeq
)调用 reverse,将再次给出一个 ArraySeq
。这很合乎逻辑,因为 arrayseq 是 Seqs
,并且对任何 Seq
调用 reverse 将再次给出一个 Seq
。另一方面,对类 ArrayOps
的 ops 值调用 reverse 将给出一个 Array
,而不是 Seq
。
上面的 ArrayOps
示例是相当人为的,仅用于显示与 ArraySeq
的差异。通常,您永远不会定义类 ArrayOps
的值。您只需对数组调用 Seq
方法
scala> a1.reverse
val res4: Array[Int] = Array(3, 2, 1)
ArrayOps
对象由隐式转换自动插入。因此,上面的行等效于
scala> intArrayOps(a1).reverse
val res5: Array[Int] = Array(3, 2, 1)
其中 intArrayOps
是之前插入的隐式转换。这引发了一个问题,即编译器如何选择 intArrayOps
而不是上面一行中到 ArraySeq
的另一个隐式转换。毕竟,这两个转换都将数组映射到支持 reverse 方法的类型,而这是输入指定的。这个问题的答案是这两个隐式转换是优先的。 ArrayOps
转换的优先级高于 ArraySeq
转换。第一个在 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
}
// 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 do
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)
^
-- Error: ----------------------------------------------------------------------
3 | val arr = new Array[T]((xs.length + 1) / 2)
| ^
| No ClassTag available for T
这里需要您通过提供 evenElems
的实际类型参数是什么来帮助编译器。此运行时提示采用类型为 scala.reflect.ClassTag
的类清单的形式。类清单是一个类型描述符对象,它描述类型的顶级类是什么。除了类清单之外,还有类型为 scala.reflect.Manifest
的完整清单,它描述类型的各个方面。但对于数组创建,只需要类清单。
如果您指示 Scala 编译器这样做,它将自动构建类清单。“指示”意味着您要求类清单作为隐式参数,如下所示
def evenElems[T](xs: Vector[T])(implicit m: ClassTag[T]): Array[T] = ...
def evenElems[T](xs: Vector[T])(using 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
}
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 do
arr(i / 2) = xs(i)
arr
evenElems
的两个修订版本完全相同。在任何情况下都会发生的是,当构建 Array[T]
时,编译器将查找类型参数 T 的类清单,即它将查找类型 ClassTag[T]
的隐式值。如果找到这样的值,则清单用于构建正确类型的数组。否则,您将看到如上所示的错误消息。
以下是使用 evenElems
方法的一些 REPL 交互。
scala> evenElems(Vector(1, 2, 3, 4, 5))
val res6: Array[Int] = Array(1, 3, 5)
scala> evenElems(Vector("this", "is", "a", "test", "run"))
val 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)
^
-- Error: ----------------------------------------------------------------------
6 |def wrap[U](xs: Vector[U]) = evenElems(xs)
| ^
| No ClassTag available for U
这里发生的情况是,evenElems
要求类型参数 U
的类清单,但未找到任何类清单。在这种情况下,解决方案当然是要要求 U
的另一个隐式类清单。因此,以下操作有效
scala> def wrap[U: ClassTag](xs: Vector[U]) = evenElems(xs)
def wrap[U](xs: Vector[U])(implicit evidence$1: scala.reflect.ClassTag[U]): Array[U]
此示例还表明,U
定义中的上下文绑定只是此处名为 evidence$1
的隐式参数的速记,类型为 ClassTag[U]
。
总之,泛型数组创建要求类清单。因此,每当创建类型参数 T
的数组时,还需要为 T
提供隐式类清单。最简单的方法是使用 ClassTag
上下文绑定声明类型参数,如 [T: ClassTag]
。