本章主要講Scala中的類型參數化。本章主要分成三個部分,第一部分實現一個函數式隊列的數據結構,第二部分實現該結構的內部細節,最後一個部分解釋其中的關鍵知識點。接下來的實例中將該函數式隊列命名爲Queue
。
一、函數式隊列
函數式隊列是一種具有以下三種操作方法的數據結構,並且這些操作都必須在常量時間內完成:
- head,返回該隊列中的第一個元素
- tail,返回除第一個元素之外的所有元素組成的新隊列
enqueue,將新元素加入原有隊列從而得到一個新隊列
函數式隊列不同於可變隊列(mutable queue)。從上面三種操作可以看出,函數式隊列對象的內容不可變,新增一個元素時得到的是一個新的隊列對象。
我們期望Queue
具有的功能是,執行下面兩句代碼後,
val q = Queue(1, 2, 3)
val q1 = q enqueue 4
q
的元素仍然是1, 2, 3
,而q1
的元素則爲1, 2, 3, 4
。
1、基於List
類型的Queue
實現
由於Queue
類型是不可變的,那麼在實現上面三個方法時也不能基於可變的數據結構來設計。由於List
類型也是非可變的,並且也支持直接操作頭尾元素。下面基於List
來實現第一版Queue
類型。
class SlowAppendQueue[T](elems: List[T]) {
def head = elems.head
def tail = new SlowAppendQueue(elems.tail)
def enqueue(x: T) = new SlowAppendQueue(elems ::: List(x))
}
對Queue
的操作都可以轉換到對List
的操作上。但是,對於enqueuq
操作,時間複雜度會線性相關於當前Queue
對象中的元素個數,不是我們要求的常量時間複雜度。
如果將enqueue
操作改造成常量時間複雜度的話,將傳入的List
對象進行reverse
後調用以下函數,對於head
和tail
的操作時間複雜度又不是常量的了,如下所示,
class SlowHeadQueue[T](smele: List[T]) {
def head = smele.last
def tail = new SlowHeadQueue(smele.init)
def enqueue(x: T) = new SlowHeadQueue(x :: smele)
}
考慮一下,如果將以上兩個函數進行合併,就可以實現三個操作都是常量時間複雜度的Queue
了。在最終的Queue
類型中維持兩個List
對象,一個是leading
,該對象中保存了Queue
對象前半段的元素,另一個對象trailing
包含了Queue
對象後半段元素的反序列。那麼Queue
對象最終的元素爲leading ::: trailing.reverse
。
class Queue[T](private val leading: List[T], private val trailing: List[T]) {
private def mirror =
if (leading.isEmpty)
new Queue(trailing.reverse, Nil)
else
this
def head = mirror.leading.head
def tail = {
val q = mirror
new Queue(q.leading.tail, q.trailing)
}
def enqueue(x: T) =
new Queue(leading, x :: trailing)
}
二、信息隱藏
第一節中最後已經實現了最開始我們需要的Queue
,但是從實現上看並不完美,因爲它將實現細節暴露了出來。即可以在Queue
類型的主構造函數中看到該類中有兩個List
對象。並且,這個Queue
對象並不是我們從直觀上所理解的那樣,因爲構造它時居然需要兩個List
對象,而不是我們直觀上想的那樣只需要傳入一個List
對象。
接下來從多方面對Queue
作進一步的改造,隱藏一些不必要以及對用戶不友好的信息。
1、私有構造器和工廠方法
我們知道,在Java中可以將一個構造器定義爲私有的,從而不對外暴露該類的構造方法。在Scala中一個類的主構造函數由類定義時由類參數以及類的實現間接的定義了。然而,還是可以將Scala的主構造函數使用private
關鍵字進行隱藏,private
關鍵字寫在類名和類參數之間,如下所示
class Queue[T] private (
private val leading: List[T],
private val trailing: List[T]
)
Scala的私有構造方法只能被該類本身,以及該類的伴生對象訪問。此時直接使用Queue
類的主構造函數生成一個Queue
對象,會報以下錯誤,
不能調用主構造函數生成新的Queue
對象,就需要提供新的方法了。最先想到的辦法就是,新建一個輔助構造函數,通過輔助構造函數來生成Queue
對象。比如
def this() = this(Nil, Nil) // 生成空的Queue
def this(elems: T*) = this(eles.toList, Nil) // 使用T*,傳入多個參數
除了輔助構造函數外,我們還可以定義一個外部可以訪問的工廠方法來生成新的Queue
對象。下面在Queue
類文件中新建一個伴生對象Queue
,並實現一個apply
方法,
object Queue {
def apply[T](xs: T*) = new Queue[T](xs.toList, Nil)
}
由於在伴生對象中定義的工廠方法名爲apply
,所以在調用該方法時的使用Queue(1, 2, 3)
很像直接調用Queue
類的構造函數。但是注意,這裏實質上是直接調用了伴生對象Queue
的apply
方法。
2、可選方案:私有對象
私有構造器和私有變量是隱藏類信息的一個方法,另一個更加嚴苛的隱藏方法是直接將該類定義爲私有的,再提供給外界一個只包含公有方法的trait
。如下所示
trait Queue[T] {
def head: T
def tail: Queue[T]
def enqueue(x: T): Queue[T]
}
object Queue {
def apply[T](xs: T*): Queue[T] = new QueueImpl[T](xs.toList, Nil)
private class QueueImpl[T](
private val leading: List[T],
private val trailing: List[T]
) extends Queue[T] {
def mirror =
if (leading.isEmpty)
new QueueImpl(trailing.reverse, Nil)
else
this
def head: T = mirror.leading.head
def tail: QueueImpl[T] = {
val q = mirror
new QueueImpl(q.leading.tail, q.trailing)
}
def enqueue(x: T) = new QueueImpl(leading, x :: trailing)
}
}
三、協變和逆變
這裏主要講到協變和逆變的概念。
上面代碼中的Queue
是trait,而不是一個類型,並且Queue
需要接收一個類型參數T
。在不指定T
的情況下,無法定義Queue
類型的對象。比如下面這行代碼就會報錯,
def doesNotCompile(q: Queue) {}
報錯如下,
因爲代碼在編譯時是不知道T
的具體類型是什麼的。但是如果爲Queue
指定一個特殊的類型參數,例如Queue[String], Queue[Int], Queue[AnyRef]
,程序就能正常編譯,如下所示,
def doesCompile(q: Queue[AnyRef]) {}
運行結果如下,
Queue
在是這裏一個泛型trait,而Queue[String]
是一個類型。帶泛型參數的trait是泛型trait,帶泛型參數的類是泛型類。Queue[Int], Queue[String]
都是泛型trait Queue[T]
的特定實現形式。
1、協變
那麼,假設類型S
是類型T
的子類,Queue[S]
是否是Queue[T]
的子類呢?如果是的話,那麼就可以稱Queue
對於其類型參數T
是協變的。對於這種只有一個類型參數的泛型,可以直接成Queue
是協變的。泛型類Queue
是協變的,意味着在上面的doesCompile
方法中,傳入一個Queue[String]
類型的參數時程序也能夠正常執行,因爲這裏可以接收的參數類型是Queue[AnyRef]
,並且String
是AnyRef
的子類。
雖然從直觀理解上,Queue[String]
類型是Queue[AnyRef]
類型的子類,但是一般情況下,在Scala中泛型類都是非協變的。即在前面的Queue
泛型trait代碼中,Queue[String]
並不是Queue[AnyRef]
的子類。
如果需要指定泛型類對某個類型參數具有協變性,需要在泛型類定義時最前面那個類型參數前加+
符號,指定該泛型類對該指定參數具有協變性。對泛型trait Queue
進行協變改造,將其第一行改成如下形式。
trait Queue[+T] {...}
2、逆變
有沒有想過,在上面的改造代碼中,將+
換成-
,即下面這種情況,是什麼情況?
trait Queue[-T] {...}
如果你往這方面進行思考了,那麼恭喜你,你已經開始嘗試逆變的寫法了。
在這種寫法下,如果類型T
是類型S
的子類,那麼Queue[S]
是Queue[T]
的子類。正好與協變是相反的!
3、可變數據無協變
在函數式編程世界裏,許多類型天然就具有協變特性。然而,當引入可變數據時(mutable data),情況就不是這樣的了。即經常使用可變數據時,大多數類都不具備協變特性。這是爲什麼呢?看一下下面這段代碼,
class Cell[T](init: T) {
private[this] var current = init
def get = current
def set(x: T) { current = x }
}
這段代碼中的Cell
類既不是協變,也不是逆變的,而是不變的。這段代碼可以正常執行,
假如我們將Cell
定義成Cell[+T]
類型,再看一下運行結果,運行報出一個Error
信息,
爲什麼會報錯?我們暫時性忽略上面的這個報錯,假設其可以正常執行。那麼繼續執行下面這幾行代碼,
val c1 = new Cell[String]("abc")
val c2: Cell[Any] = c1
c2.set(1)
val s: String = c1.get
首先定義一個Cell[String]
類型的變量c1
,由於具有協變性,接下來將c1
賦值給Cell[Any]
類型變量c2
。由於是引用型變量,實際上c1
和c2
執行的是同一個具體對象。此時通過c2
的set
方法,將current
變量的值變更成Int
型的1
,這是不會報錯的。接下來再通過c1
的get
方法,獲取current
的值。對於c1
來說,current
是String
類型的,但是已經通過c2
將其改成了Int
類型。這時候c1
獲取current
變量時就會類型不匹配而報錯了。
四、下界和上界
1、下界
下界使用符合>:
表示,比如下面這段代碼中表示類型U
是類型T
的父類,即此處的類型U
最少爲T
,不能比T
的類型更低。
def enqueue[U >: T](x: U) = new Queue[U](leading, x :: trailing)
2、上界
上界使用符合<:
表示,比如T <: U
表示需要類型T
是類型U
的子類,類型T
不能比U
的類型更高。