本章要點
- 類、特質、方法和函數都可以有類型參數
- 將類型參數放置在名稱之后,以方括號括起來。
- 類型界定的語法為 T <: UpperBound、 T >: LowerBound、 T <% ViewBound、 T : ContextBound
- 你可以用類型約束來約束另一個方法,比如(implicit ev: T <:< UpperBound)
- 用+T(協變)來表示某個泛型類的子類型關系和參數T方向一致, 或用-T(逆變)來表示方向相反。
- 協變適用于表示輸出的類型參數,比如不可變集合中的元素
- 逆變適用于表示輸入的類型參數,比如函數參數
泛型類
和Java或C++一樣,類和特質可以帶類型參數。在Scala中,使用方括號來定義類型參數:
class Pair[T, S] (val first: T, val second: S)
帶有一個或多個類型參數的類是泛型的。
泛型函數
def getMiddle[T](a: Array[T]) = a(a.length / 2)
類型變量界定
有時, 你需要對類型變量進行限制。
class Pair[T](val first: T, val second: T) {
def smaller = if (first.compareTo(second) < 0) first else second // error
}
這是錯誤的,我們并不知道first是否有compareTo方法。要解決這個問題,我們可以添加一個上界 T <: Comparable[T] :
class Pair[T <: Comparable[T]](val first: T, val second: T) {
def smaller = if (first.compareTo(second) < 0) first else second
}
這里T必須是Comparable[T]的子類型。這樣我們就可以實例化Pair[java.lang.String],但是不能實例化Pair[java.io.File] 。
你可以為了類型指定一個下界。例如:
class Pair[T] (val first: T, val second: T) {
def peplaceFirst[newFirst: T] = new Pair[T] (newFirst, second)
}
假定我們有一個Pair[Student],我們應該允許使用一個Person來替換第一個組件。這樣做的結果將會是一個Pair[Person]。通常而言,替換進來的類型必須是原類型的超類型。
def replaceFirst[R >: T] (newFirst: R) = new Pair[R](newFirst, second)
// 或者
def replaceFirst[R >: T] (newFirst: R) = new Pair(newFirst, second) // 類型推導
**注意: **如果不寫下界:
def replaceFirst[R] (newFirst: R) = new Pair(newFirst, second)
該方法可以編譯通過,但是它將返回Pair[Any].
視圖界定
在面前上界時我們提供了一個示例:
class Pair[T <: Comparable[T]]
但是如果你new一個Pair(4,2), 編譯器會抱怨說Int不是Comparable[Int]的子類型。因為Scala的Int類型沒有實現Comparable,不過RichInt實現了Comparable[Int],同時還有一個從Int到RichInt的隱式轉換。
解決方法是使用“視圖界定”:
class Pair[T <% Comparable[T]]
<% 意味著T可以被隱式轉換成Comparable[T]。
**說明: **用Ordered特質會更好,它在Comparable的基礎上額外提供了關系操作符:
class Pair[T <% Ordered[T]] (val first: T, val second: T) {
def smaller = if( first < second ) first else second
}
上下文界定
視圖界定 T <% V 要求必須存在一個從T到V的隱式轉換,上下文界定的形式為 T:M,其中M是另一個泛型類。它要求必須存在一個類型為M[T]的“隱式值”。例如:
class Pair[T: Ordering]
上述定義要求必須存在一個類型為Ordering[T]的隱式值。當你聲明一個使用隱式值的方法時,你需要添加一個“隱式參數”。例如:
class Pair[T: Ordering] (val first: T, val second: T) {
def smaller(implicit ord: Ordering[T]) = {
if( ord.compare(first, second) < 0) first else second
}
}
Manifest上下文界定
要實例化一個泛型的Array[T],我們需要一個Manifest[T]對象。 如果你要編寫一個泛型函數來構造泛型數組的話,你需要傳入這個Manifest對象來幫忙。由于它是構造器的隱式參數,可以用上下文界定:
def makePair[T: Manifest](first: T, second: T) {
val r = new Array[T](2)
r(0) = first
r(1) = second
r
}
多重界定
類型變量可以同時有上界和下界。寫法為:
T >: Lower <: Upper
你不能同時有多個上界或多個下界。不過你可以要求一個類型實現多個特質:
T <: Comparable[T] with Serializable with Cloneable
你也可以有多個視圖界定:
T <% Comparable[T] <% String
你也可以有多個上下文界定:
T : Ordering : Manifest
類型約束
類型約束提供給你的是另一個限定類型的方式。總共有三種關系可供使用:
T =:= U // T是否等于U
T <:< U // T是否為U的子類型
T <%< U // T能否被視圖(隱士)轉換為U
要使用這樣一個約束,你需要添加“隱式類型證明參數”:
class Pair[T](val first: T, val second: T) (implicit ev: T <:< Comparable[T])
類型約束讓你可以在泛型類中定義只能在特定條件下使用的方法。例如:
class Pair[T] (val first: T, val second: T) {
def smaller(implicit ev: T <:< Ordered[T]) = {
if (first < second) first else second
}
}
在這里你可以構造出Pair[File], 盡管File并不是帶有先后次序的。只有當你調用smaller方法時,才會報錯。
另一個示例是Option類的orNull方法:
val friends = Map("Fred" -> "Barney", ...)
val friendOpt = friends.get("Wilma")
val friendOrNull = friendOpt.orNull // 要么是String,要么是null
這種做法并不適用于值類型,例如Int。因為orNUll實現帶有約束Null <:< A, 你仍然可以實例化Option[Int],只要你別使用orNull就好了。
類型約束的另一個用途是改進類型推斷。例如:
def firstLast[A, C <: Iterable[A]](it: C) = (it.head, it.last)
當你執行如下代碼:
firstLast(List(1,2,3))
你會得到一個消息,推斷出的類型參數[Nothing, List[Int]]不符合[A, C <: Iterable[A]]。類型推斷器單憑List(1,2,3)無法判斷出A是什么,因為它在同一個步驟中匹配到A和C。解決的方法是首先匹配C,然后在匹配A:
def firstLast[A, C] (it: C) (implicit ev: C <:< Iterable[A]) = (it.head, it.last)
型變
假定我們有一個函數對pair[Person]做某種處理:
def makeFriends(p: Pair[Person])
如果Student是Person的子類,那么可以用Pair[Student]作為參數調用嗎?缺省情況下,這是個錯誤。盡管Student是Person的子類型,但Pair[Student]和Pair[Person]之間沒有任何關系。
如果你想要這樣的關系,則必須在定義Pair類時表明這一點:
class Pair[+T] (val first: T, val second: T)
+號意味著該類型是與T協變的-----也就是說,它與T按痛樣的方向型變。由于Student是Person的子類,Pair[Student]也就是Pair[Person]的子類型。
也可以有另一個方向的型變--逆變。例如:泛型Friend[T],表示希望與類型T的人成為朋友的人。
trait Friend[-T] {
def befriend(someone: T)
}
// 有這么一個函數
def makeFriendWith(s: Student, f: Friend[Student]) { f.befriend(s) }
class Person extends Friend[Person]
class Student extends Person
val s = new Student
val p = new Person
函數調用makeFriendWith(s, p)能成功嗎?這是可以的。
這里類型變化 的方向和子類型方向是相反的。Student是Person的子類,但是Friend[Student]是Friend[Person]的超類。這就是逆變。
在一個泛型的類型聲明中,你可以同時使用這兩中型變,例如 單參數函數的類型為Function1[-A, +R]
def friends(students: Array[Atudent], find: Function1[Student, Person]) = {
for (s <- students) yield find(s)
}
協變和逆變點
從上面可以看出函數在參數上是逆變的,在返回值上則是協變的。通常而言,對于某個對象消費的值適用逆變,而對于產出它的值則適用于協變。 如果一個對象同時是消費和產出值,則類型應該保持不變,這通常適用于可變數據結構。
如果試著聲明一個協變的可變對偶,則是錯誤的,例如:
class Pair[+T] (var first: T, var second: T) // 錯誤
不過有時這也會妨礙我們做一些本來沒有風險的事情。例如:
class Pair[+T] (var first: T, var second: T) {
def replaceFirst(newFirst: T) = new Pair[T](newFirst, second) // error T出現在了逆變點
}
// 解決方法是給方法加上另一個類型參數:
class Pair[+T] (var first: T, var second: T) {
def replaceFirst[R >: T](newFirst: R) = new Pair[R](newFirst, second)
}
對象不能泛型
你不能給對象添加類型參數。
類型通配符
在Java中,所有泛型類都是不變的。不過,你可以在使用通配符改變它們的類型。例如:
void makeFriends(Pair<? extends Person> people) // Java代碼
可以用List<Student>作為參數調用。
你也可以在Scala中使用通配符
def process(people: java.util.List[_ <: Person]) // scala
在Scala中,對于協變的Pair類,不需要通配符。但是假定Pair是不變的:
class Pair[T] (var first: T, var second: T)
// 可以定義函數:
def makeFriends (p: Pair[_ <: Person]) // 可以用Pair[Student]調用
逆變使用通配符:
import java.util.Comparator
def min[T](p: Pair[T]) (comp: Comparator[_ >: T])