文章目录
- 类型参数的意思就是类型作为参数
- 用类型参数实现类和函数,这样的类和函数可以用于实现多种类型,例如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
类型约束
- 另一个限定类型的方式,三种关系
T =:= U
, T 是否等于UT <:< U
,T是否为U的子类型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)
官网的例子
- https://docs.scala-lang.org/tour/variances.html
- 协变
- List[+A]
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])=()
注意:本文归作者所有,未经作者允许,不得转载