過時公告
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
。若要反向進行,從 WrappedArray
到 Array
,您可以使用在 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
呼叫 reverse,它是一個 WrappedArray
,將再次提供一個 WrappedArray
。這是合乎邏輯的,因為包裝陣列是 Seqs
,而對任何 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
中的轉換。一個非常類似的方案適用於字串。
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
}
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 編譯器執行此操作,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]
。