京东-优惠雷达
无畏Pro 14限量抢购
冷藏饮料节
最高12期免息再送支架
自营热卖

18 类型参数

一枕相思泪 1年前   阅读数 67 0

  • 类型参数的意思就是类型作为参数
  • 用类型参数实现类和函数,这样的类和函数可以用于实现多种类型,例如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])=()
发布了58 篇原创文章 · 获赞 74 · 访问量 7万+

注意:本文归作者所有,未经作者允许,不得转载

全部评论: 0

    我有话说: