scala-18 類型參數

  • 類型參數的意思就是類型作爲參數
  • 用類型參數實現類和函數,這樣的類和函數可以用於實現多種類型,例如Array[T]可以放任意類型T的元素
  • 可以指定類型如何根據類型參數的變化而變化
  • 類、特質、方法、函數都可以有類型參數
  • 類型參數放置在名稱之後,用方括號括起來
  • 類型界定的語法, T <: 上界,T >: 下界,T : ContextBound
  • 類型約束來約束一個方法
  • +T 協變表示某個泛型類的子類型關係和參數T方向一致,或用 -T逆變表示方向相反
  • 協變適用於表示輸出的類型參數,例如不可變集合中的元素
  • 逆變適用於表示輸入的類型參數,例如函數參數
  • 協變和逆變是一對辯證法

泛型類

  • 類和特質可以帶類型參數,用方括號來定義類型參數
  • 如下所示,兩個類型參數,T和S,類的定義中可以用類型參數定義變量、方法參數、返回值類型
  • 帶有一個或多個類型參數的類是泛型的,將類型參數替換成實際的類型就得到一個普通的類,Scala會自動推斷
//泛型類Pair
class Pair[T,S](val first:T,val second:S)
//使用泛型類,自動推斷類型
scala> val p = new Pair(42,"String")
p: Pair[Int,String] = Pair@4525e9e8
//指定類型
scala> val p2 = new Pair[Int,Int](4,5)
p2: Pair[Int,Int] = Pair@10f477e2

泛型函數

  • 函數和方法帶有類型參數,例如
def getMiddle[T](a:Array[T]):T = a(a.length / 2)

scala> getMiddle(Array(1,2,3,4,5))
res2: Int = 3

scala> getMiddle[Double](Array(1,2,3,4,5))
res3: Double = 3.0
  • 方法轉換爲函數,同時指定類型
scala> val f = getMiddle[Double] _
f: Array[Double] => Double = <function1>

類型變量界定

  • 有時候對類型變量進行限制,例如定一個上界,也就是父類型
  • 例如下面的例子,對於Pair類,smaller方法獲取較小的值,添加一個上界 T <: Comparable[T],這就意味着T必須是Comparable[T]的子類型,這樣可以實例化Pair[Java.lang.String],但是不能實例化Pair[java.net.URL],Pair[Int]也不能
class Pair[T <: Comparable[T]](val first:T,val second:T){
  def smaller:T = if (first.compareTo(second)<0) first else second
}
//實例化
scala> val a = new Pair("a","b")
a: Pair[String] = Pair@5513a46b
//錯誤的實例化
scala> val a = new Pair(1,2)
<console>:12: error: inferred type arguments [Int] do not conform to class Pair's type parameter bounds [T <: Comparable[T]]
       val a = new Pair(1,2)
               ^
<console>:12: error: type mismatch;
 found   : Int(1)
 required: T
       val a = new Pair(1,2)
                        ^
<console>:12: error: type mismatch;
 found   : Int(2)
 required: T
       val a = new Pair(1,2)
                          ^
  • 類似的,也可以爲類型指定一個下界
  • 先看一個沒有下界的,用新的元素替換第一個元素
class Pair[T](val first:T,val second:T){
  def replaceFirst(newFirst:T) = new Pair[T](newFirst,second)
}
  • 更進一步,假定有一個Pair[Student],允許使用Person來替換第一個元素,這樣做的結果是得到一個Pair[Person],通常來說,替換進來的類型必須是原類型的超類型,例如R應該是T類型或T類型的超類型。最好手動指明類型
class Person(name:String){
  override def toString: String = name
}
class Student(val name:String, id:Int) extends Person(name){
  override def toString: String = s"${name}  ${id}"
}
//泛型類
class Pair[T](val first:T,val second:T){
  //泛型函數
  def replaceFirst[R >: T](newFirst:R) = new Pair[R](newFirst,second)

  override def toString: String = s"[${first.toString} , ${second.toString}]"
}

val a = new Person("zhang")
val b = new Student("wang",100)
val c = new Student("zhao",101)
val pair1 = new Pair[Student](b,c)
println(pair1)  //[wang  100 , zhao  101]
val pair2 = pair1.replaceFirst(a)
println(pair2) //[zhang , zhao  101]

視圖界定

  • 視圖界定 T <% V意味着T可以被隱式轉換成V
  • 前面出現的Int不是Comparable[Int]的子類型,不過,RichInt實現了Comparable[Int],同時還有一個Int到RichInt的隱式轉換
class Pair[T <% Comparable[T]]
  • Scala的視圖界定將退出歷史舞臺,可以使用類型約束type constraint替換視圖界定
class Pair2[T](val first:T,val second:T)(implicit ev:T => Comparable[T]){
  def smaller:T = if (first.compareTo(second)<0) first else second
}

上下文界定

  • context bound的形式爲T : M,其中M是另一個泛型類,要求必須存在一個類型爲M[T]的隱式值implicit value
  • 下例中,要求必須存在一個類型爲Ordering[T]的隱式值,該隱式值可以被用在該類的方法中,當聲明一個使用隱式值的方法時,需要添加一個隱式參數implicit parameter
class Pair[T : Ordering]
  • 第21章將會看到,隱式值比隱式轉換更爲靈活

ClassTag上下文界定

  • 在虛擬機中,泛型的相關類型信息是被抹掉的
  • 要實例化一個泛型的Array[T],需要一個ClassTag[T]對象,要想讓基本類型的數組能正常工作的話,這是必須的,如果編寫一個構造泛型數組的泛型函數,需要傳遞一個class tag標籤,即上下文界定
  • 如果調用makePair(4,9),編譯器將會定位到隱式的ClassTag[Int]並實際上調用makePair(4,9)(classTag),這樣方法調用的就是classTag.newArray,本例中是一個將構造出基本類型數組int[2]的ClassTag[Int]
  • 如果不適用classtag會直接報錯
def makePair[T](first:T,second:T):Array[T]={
  val r = new Array[T](2)
  r(0) = first
  r(1) = second
  r
}
<console>:12: error: cannot find class tag for element type T
             val r = new Array[T](2)
                     ^
  • 還是要使用
import scala.reflect.ClassTag
def makePair[T : ClassTag](first:T, second:T):Array[T]={
  val r = new Array[T](2)
  r(0) = first
  r(1) = second
  r
}
  • 實驗表明,使用List,Vector,Set等Collection的時候不需要使用ClassTag

多重界定

  • 類型變量同時使用上界和下界,語法爲T >: Lower <: Upper
  • 但不能同時又多個上界或多個下界,不過可以要求一個類型實現多個特質,就像T <: Comparable[T] with Serializable with Cloneable
  • 同時多個上下文界定T : Ordering : ClassTag

類型約束

  • 另一個限定類型的方式,三種關係
  1. T =:= U, T 是否等於U
  2. T <:< U,T是否爲U的子類型
  3. T => U,T能否被轉換爲U
  • 要使用這種約束,需要添加隱式類型證明參數,例如
class Pair3[T] (val first :T , val second : T)(implicit ev:T <:< Comparable[T])

類型約束用途一

  • 類型約束可以在泛型類中定義只能在特定條件下使用的方法,例如下面的例子,Pair4(3,4)可以被實例化,但是無法使用smaller,
class Pair4[T](val first:T,val second:T){
  def smaller(implicit ev: T <:< Ordered[T]):T = 
    if (first.compareTo(second)<0) first else second
}

類型約束用途二

  • 改進類型推斷
  • 像下面這樣不能推斷出A來,使用firstLast(List(1,2,3))
def firstLast[A, C <: Iterable[A]](it:C) = (it.head,it.last)
  • 改進的方法,首先匹配C再匹配A
//改進的方法
def firstLast2[A, C](it:C)(implicit ev: C <:< Iterable[A]) = (it.head,it.last)

型變

書上的例子

  • 如果有個函數對Pair[Person]做處理,def makeFriends(p:Pair[Person]),如果Student是Person的子類,不能使用Pair[Student]作爲參數調用,即Pair[Person]和Pair[Student]之間沒有關係。
  • 如果想讓他們之間有關係怎麼辦?使用型變
  • 協變covariant :類型變化的方向和子類型的方向相同,如果Student是Person的子類型,那麼Pair[Student]也是Pair[Person]的子類型,換句話說,如果一個函數需要的參數爲Pair[T],那麼傳入Pair[T的子類型] 也是可以的,例如當T爲Person的時候,T的子類型爲Student。
class Pair[+T](val first:T,val second:T){...}
  • 逆變 contravariant:類型變化的方向和子類型的方向相反,如果Student是Person的子類型,那麼Pair[Student]是Pair[Person]的超類型。如果一個函數需要的參數爲Pair[T],那麼傳入Pair[T的超類型] 也是可以的,例如當T爲Student,T的超類型爲Person。
  • 考慮Friend[T]表示希望與類型T的人成爲朋友的人。fred想和任何Person類型成爲朋友,他也會想和susan成爲朋友。Student是Person的子類型,但是Friend[Student]是Friend[Person]的超類型
trait Friend[-T]{
  def befriend(someone:T)
}

def makeFriendWith(s:Student,f:Friend[Student])={f.befriend(s)}
class Person extends Friend[Person]{
  override def befriend(someone: Person): Unit = ()}

class Student extends Person

val susan = new Student
val fred = new Person

makeFriendWith(susan,fred)
  • 在泛型的類型聲明中,可以同時使用這兩種型變,例如Scala中,單參數函數的類型爲Function1[-A, +R],也就意味着可以函數類型可以是A的父類 => R的子類,findStudent的類型爲Person => Student,friends的第二個參數的類型爲Student => Person,但是findStudent也可以作爲friends的第二個參數
trait Friend[-T]{
  def befriend(someone:T)
}
def makeFriendWith(s:Student,f:Friend[Student])={f.befriend(s)}
class Person extends Friend[Person]{
  override def befriend(someone: Person): Unit = ()}

class Student extends Person

//Function1
def friends(students:Array[Student], find:Function1[Student,Person])={
  //第二個參數是一個函數,可以寫爲 find:Student => Person
  for(s <- students) yield find(s)
}
def findStudent(p:Person):Student = new Student

val fs = friends(Array(new Student,new Student),findStudent)
println(fs)

官網的例子

abstract class Animal {
  def name: String
}
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal
object CovarianceTest extends App {
  def printAnimalNames(animals: List[Animal]): Unit = {
    animals.foreach { animal =>
      println(animal.name)
    }
  }

  val cats: List[Cat] = List(Cat("Whiskers"), Cat("Tom"))
  val dogs: List[Dog] = List(Dog("Fido"), Dog("Rex"))

  printAnimalNames(cats)
  // Whiskers
  // Tom

  printAnimalNames(dogs)
  // Fido
  // Rex
}
  • 逆變
abstract class Animal {
  def name: String
}
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal

abstract class Printer[-A] {
  def print(value: A): Unit
}

class AnimalPrinter extends Printer[Animal] {
  def print(animal: Animal): Unit =
    println("The animal's name is: " + animal.name)
}

class CatPrinter extends Printer[Cat] {
  def print(cat: Cat): Unit =
    println("The cat's name is: " + cat.name)
}

object testcontra extends App{
  val myCat: Cat = Cat("Boots")

  def printMyCat(printer: Printer[Cat]): Unit = {
    printer.print(myCat)
  }

  val catPrinter: Printer[Cat] = new CatPrinter
  val animalPrinter: Printer[Animal] = new AnimalPrinter

  printMyCat(catPrinter)
  printMyCat(animalPrinter)
}

協變點和逆變點

  • 上一節的Function1[-A,+R]的參數A是逆變的,而返回值R是協變的,通常而言,某個對象消費的值(參數)適用逆變,而產出的值(返回值)適用協變,如果同時消費和產出某值,類型應該保持不變invariant,通常適用於可變數據結構
  • 例如scala的數組不支持型變,不能講Array[Student]轉換爲Array[Person]反過來也不行
  • 嘗試聲明一個協變的可變對偶,是行不通的,協變的類型T出現了逆變點first_=(value:T)參數位置是逆變點,返回類型的位置是協變點
  • 協變點接受協變或不變的類型,逆變點接受逆變或不變的類型
scala> class Pair[+T](var first:T,var second:T)
<console>:11: error: covariant type T occurs in contravariant position in type T of value first_=
       class Pair[+T](var first:T,var second:T)
             ^

  • 特殊情況:函數F作爲參數的時候,這個函數F的參數是協變的,返回值是逆變的,例如Iterable[+A]的foldLeft方法中的op,A是協變的,在函數定義中,A位於協變點。中文版的第306頁倒數第四行的+和-的位置有錯誤下圖爲正確的協變逆變標註。一般情況下,參數是逆變點,返回值是協變點,函數作爲參數是特殊情況,反之。
    在這裏插入圖片描述
  • 這些規則簡單安全,但是也妨礙做一些沒有風險的事情,例如下面Pair2的replaceFirst,是編譯不通過的,因爲T出現在了replaceFirst的逆變點。解決方法是給方法加上另一個類型參數,T作爲下界,見Pair3,R可以是T的超類,newFirst的位置是逆變的,但是R是不變的,可以出現在逆變的位置上。
class Pair2[+T](val first:T,val second:T){
//下面的編譯不過
  def replaceFirst(newFirst: T) = new Pair2[T](newFirst, second) 
}

class Pair3[+T](val first:T,val second:T) {
  def replaceFirst[R >: T](newFirst: R) = new Pair3[R](newFirst, second)
}

對象不能泛型

  • 不能給對象添加類型參數,例如可變列表
  • 下例中,T是協變的,List[Nothing]可以轉換爲List[Int],即T是Int型是,Nothing是所有類型的子類,也是Int類型的子類
abstract class List[+T]{
  def isEmpty:Boolean
  def head:T
  def tail:List[T]
}
class Node[T](val head:T,val tail:List[T]) extends List[T]{
  override def isEmpty: Boolean = false
}
//不能將empty變成對象 object Empty[T] extends List[T] 錯誤
//不能將參數化的類型添加到對象,解決方法是object Empty extends List[Nothing]
object Empty extends List[Nothing]{
  override def isEmpty: Boolean = true
  //head tail是Nothing類型,Nothing是所有類型的子類
  def head = throw new UnsupportedOperationException
  def tail = throw new UnsupportedOperationException
}

val lst = new Node(42,Empty)

類型通配符

  • java中所有的泛型類是不變的,可以使用時用通配符改變它們的類型
//java代碼
void makeFriends(List<? extends Person> people)
  • scala中使用通配符
def process(people:java.util.List[_ <: Person])
  • 在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]) = () //逆變使用通配符
  • 某些複雜的情形,通配符不完善
//scala中下面的聲明行不通
def min[T <: Comparator[_ >: T]](p:Pair[T])=()
//使用下面的解決方法
type SuperComparable[T] = Comparator[_ >: T]
def min[T <: SuperComparable[T]](p:Pair[T])=()
發佈了75 篇原創文章 · 獲贊 83 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章