開篇先是舉了一個Int類型棧的例子,說明如果想要一個String類型的棧,就要重寫這些相同的東西。一個避免寫重復代碼的方法就是把類型參數化(parameterize)。泛型的寫法如下:
abstract class Stack[A]{ def push(x : A) : Stack[A] = ... def isEmpty : Boolean def top : A def pop : Stack[A] }
在上面的定義中,“A”是一個類型參數,可被用在Stack類和它的子類中。類參數可以是任意(arbitrary)的名字。用[]來包圍,而不是用()來包圍,用以和值參數進行區別。
也可以用類型去參數化方法。例如:
def isPrefix[A](p: Stack[A] , s : Stack[A]) : Boolean = { p.isEmpty || p.top == s.top && isPrefix[A]( p.pop , s.pop) }
這個方法參數被叫作“多形性”(polymorphic)。意思為“有很多的類型(forms)”。在使用它的時候,應該把類型參數(如String,Int)和方法參數一起傳過去。
本地類型推斷(Inference):如果在程序中一直使用傳像[Int]或是[String]這樣的類型參數會變得乏味。經常性的,在一個類型參數中的信息是多余的(redundant),因為當前的類型參數可以從檢查函數的值或者期望的返回值類型中得到確定。Scala中有一個強大的類型推斷器,可以推斷出所需的類型參數。
8.1 類型參數邊界
在用類型參數定義了一個抽象類Set[A]后,在實現中要用到比較(<>),但是不能確定A的具體類型,因此不能直接使用。一個解決辦法就是對合法類型進行限制,對只含有方法<>的類型放行。在標准庫里有一個特質Ordered[A],用來表示可比較的類型。現在可以強制要求這個類型為Ordered的子類型。可以通過給出一個上界(upper bound)的方式來解決這個問題:
trait Set[A <: Ordered[A]] { def incl(x : A) : Set[A] def contains(x : A) : Boolean }
這樣定義后,傳入的A類型參數必須是Ordered[A]的子類型。也即,可以被比較。
class EmptySet[A <: Ordered[A]] extends Set[A] { def contains(x: A): Boolean = false def incl(x: A): Set[A] = new NonEmptySet(x, new EmptySet[A], new EmptySet[A]) } class NonEmptySet[A <: Ordered[A]] (elem: A, left: Set[A], right: Set[A]) extends Set[A] { def contains(x: A): Boolean = if (x < elem) left contains x else if (x > elem) right contains x else true def incl(x: A): Set[A] = if (x < elem) new NonEmptySet(elem, left incl x, right) else if (x > elem) new NonEmptySet(elem, left, right incl x) else this }
可以看到,在new NonEmptySet(...)時,沒有寫入類型參數。因為可以從期待的返回值類型中推斷出來。
先創建一個Ordered的子類,如下 :
case class Num(value : Double) extends Ordered[Num] { //compare(that : A) : Int 是Ordered[A] 的抽象方法 def compare(that : Num) : Int = if ( this.value < that.value) -1 else if ( this.value > that.value) 1 else 0 }
一個關於參數邊界的問題就是它們需要考慮到以下問題:如果Sum類不是聲明為一個Ordered的子類,就不能在集合中使用它。同樣,Java中的Int,Double什么的也都不是Ordered子類,所以也不能聲明為這里的元素。
一個稍為復雜的設計,允許使用這些元素,就View Bounds來代替原來的類型邊界(type bounds)。唯一的變化是如下:
trait Set[A <% Orfered[A]] ... class EmptySet[A <% Ordered[A]] ...
View Bounds比原來的邊界要弱:<% 指明A必須可以轉化為邊界類型T,通過使用隱式轉換。(implicit conversion)
8.2 變化型注解(variance annotation):即討論了協變(co-variant)和逆變(contra-variant)的問題。如果Stack[String]是否是Stack[AnyRef]的子類。可以通過如下定義進行協變:
class Stack[+A]{ ... }
類型參數A前面的+表示這是一個協變的。同樣,“-”表示的是逆變的。Java中的List之類的是不是協變的,但數組是協變的。這樣可以通過編譯,但在執行時會報錯。
在純凈的函數式世界里,所有類型都是協變的。但是當引入了可變數據后,情況就變了。在Scala里默認不是協變的。協變類型應該出現在協變位置,這些位置包括:類里值的參數類型;方法的返回值類型;以及其他協變類型中的參數。放在其他地方會被拒絕。如下,則會被拒絕:
class Array[+A] { def apply(index : Int) : A def update(index : Int , elem : A) }
協變參數不能放在傳值參數類型里,這樣會破壞完整性。不過可以通過下界(lower bounds)來解決這個問題。
8.3 下界
已經見過上界(upper bounds)。在類型聲明中聲明:T >: S,則T就嚴格的成為了S的父類了。可以這樣同時定義上界和下界:
T >:S <: U。如果做如下定義,就可以解決剛才的問題:
class Stack[+A] { def push[ B >: A](x : B) : Stack[B] = new NonEmptySet(x , this) }
現在A所在的位置就是一個協變位置。
總之,不要猶豫在數據結構中去使用變化型注解。編譯器會檢查潛在的健全性問題。
8.5 元組(tuples):
有時一個函數會返回多個值。可以用到元組,而不用構造一個新的類。Scala里給了一個類Tuple2,可以如下使用:
def divmod(x : Int , y : Int) = new Tuple2[Int , Int ]( x / y , x % y )
想要訪問元組里的數據,可以如下:val xy = divmod(x , y ) ; xy._1 , xy._2。或者也可以用模式匹配來得到每個值。元組是個case class 。也可以直接通過(...)來表示元組。所以上面的例子也可以被定義為:
def divmod( x : Int , y : Int ) : (Int , Int) = (x / y , x % y)
8.6 函數
Scala是函數式語言,所以函數是頭等值,同時它也是面向對象語言,所以所有的值都是對象。Scala里Function1 trait的定義如下:
package scala trait Function1[-A , +B]{ def apply( x : A) : B }
(又涉及到逆變的概念,不是很懂。)