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)
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
的執行個體之間有一個隱式的「包裝」轉換,而 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
的另一個隱式轉換。畢竟,這兩個轉換都會將陣列對應到支援反向方法的類型,而這是輸入所指定的。這個問題的答案是,這兩個隱式轉換有優先順序。ArrayOps
轉換的優先順序高於 ArraySeq
轉換。第一個轉換定義在 Predef
物件中,而第二個轉換定義在類別 scala.LowPriorityImplicits
中,而 Predef
繼承了這個類別。子類別和子物件中的隱式轉換優先於基底類別中的隱式轉換。因此,如果兩個轉換都適用,則會選擇 Predef
中的轉換。一個非常類似的機制適用於字串。
So now you know how arrays can be compatible with sequences and how they can support all sequence operations. What about genericity? In Java, you cannot write a T[]
where T
is a type parameter. How then is Scala’s Array[T]
represented? In fact a generic array like Array[T]
could be at run-time any of Java’s eight primitive array types byte[]
, short[]
, char[]
, int[]
, long[]
, float[]
, double[]
, boolean[]
, or it could be an array of objects. The only common run-time type encompassing all of these types is AnyRef
(or, equivalently java.lang.Object
), so that’s the type to which the Scala compiler maps Array[T]
. At run-time, when an element of an array of type Array[T]
is accessed or updated there is a sequence of type tests that determine the actual array type, followed by the correct array operation on the Java array. These type tests slow down array operations somewhat. You can expect accesses to generic arrays to be three to four times slower than accesses to primitive or object arrays. This means that if you need maximal performance, you should prefer concrete to generic arrays. Representing the generic array type is not enough, however, there must also be a way to create generic arrays. This is an even harder problem, which requires a little of help from you. To illustrate the issue, consider the following attempt to write a generic method that creates an array.
// 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]
。