13.Scala-类型参数

第13章 类型参数

13.1 泛型类

类和特质都可以带类型参数,用方括号来定义类型参数,可以用类型参
数来定义变量、方法参数和返回值。带有一个或多个类型参数的类是泛型的。如下 p1,
如果实例化时没有指定泛型类型,则 Scala 会自动根据构造参数的类型自动推断泛型的具体类型。
class Pair[T, S](val first: T, val second: S) {
  override def toString = "(" + first + "," + second + ")"
}

//从构造参数推断类型 val p = new Pair(42, "String")
//设置类型 val p2 = new Pair[Any, Any](42, "String")

13.2 泛型函数

函数和方法也可以有类型(泛型)参数:
def getMiddle[T](a: Array[T]) = a(a.length / 2)

// 从参数类型来推断类型
println(getMiddle(Array("Mary", "had", "a", "little", "lamb")).getClass.getTypeName)

// 指定类型,并保存为具体的函数。
val f = getMiddle[String] _
println(f(Array("Mary", "had", "a", "little", "lamb")))

13.3 类型变量限

在 Java 泛型里表示某个类型是 Test 类型的子类型,使用 extends 关键
字:
 <T extends Test>
//或用通配符的形式:
 <? extends Test>
这种形式也叫为泛型的 上限上界,同样的意思在 scala 的写法为:
[T <: Test]
需要用<:来表示,叫做上界。上界:类型参数应该是某某的子类型。
//或用通配符:
[ _ <: Test ]
class Pair1[T <: Comparable[T]](val first: T, val second: T) {
  def smaller = if (first.compareTo(second) < 0) first else second
}
val p3 = new Pair1("Fred", "Brooks")
println(p3.smaller)
 
在 Java 泛型里表示某个类型是 Test 类型的父类型,使用 super 关键字:
 <T super Test>
//或用通配符的形式:
 <? super Test>
 
这种形式也叫下限或下界,同样的意思在 scala 的写法为:
[T >: Test]
//或用通配符:
[_ >: Test]
class Pair2[T](val first: T, val second: T) {
  def replaceFirst[R >: T](newFirst: R) = new Pair2[R](newFirst, second)
  override def toString = "(" + first + "," + second + ")"
}
Java 里,T 同时是 A 和 B 的子类型,称为 multiple bounds
 
Scala 里对上界和下界不能有多个,不过变通的做法是使用复合类型
(compund type):
[T <: A with B]
 
而对于下界,在 java 里则不支持 multiple bounds 的形式:
//java 不支持
 
Scala 里对复合类型同样可以使用下界
[T >: A with B]
因为 Scala 里对于 A with B 相当于 (A with B),仍看成一个类型,

13.4 视图界定

还来看 Comparable 子类的问题,如果输入如 Pair(4,2)则无法比较,
Scala 的 Int 类型没有实现 Comparable 接口,而 RichInt 确实现了,在这里我
们需要一个隐式转换,让 Int 可以转换为 RichInt。
我们可以通过视图界定来解决,用 <% 来表示。 T 可以被隐式转换成
Comparable[T]
[T <% Comparable[T]]
class PairSec04[T <% Comparable[T]](val first: T, val second: T) {
  def smaller = if (first.compareTo(second) < 0) first else second
  override def toString = "(" + first + "," + second + ")"
}

val p4 = new PairSec04(4, 2) // Works
println(p4.smaller)

13.5 上下文界定

视图界定 T <% V 要求必须存在一个从 T 到 V 的隐式转换。上下文界定
的形式为 T:M,其中 M 是另一个泛型类,它要求必须存在一个类型为 M[T]的
隐式值。
class Pair3[T : Ordering](val first: T, val second: T) {
  def smaller(implicit ord: Ordering[T]) ={
    println(ord)
    if (ord.compare(first, second) < 0) first else second
  }
  override def toString = "(" + first + "," + second + ")"
}
 
上述类定义要求必须存在一个类型为 Ordering[T]的隐式值,当你使用了
一个使用了隐式值得方法时,传入该隐式参数。

13.6 Manifest 上下文界定

Manifest 是 scala2.8 引入的一个特质,用于编译器在运行时也能获取泛
型类型的信息。在 JVM 上,泛型参数类型 T 在运行时是被“擦拭”掉的,编译
器把 T 当作 Object 来对待,所以 T 的具体信息是无法得到的;为了使得在运
行时得到 T 的信息,scala 需要额外通过 Manifest 来存储 T 的信息,并作为参
数用在方法的运行时上下文。
 
  def test[T] (x:T, m:Manifest[T]) { ... }
  有了 Manifest[T]这个记录 T 类型信息的参数 m,在运行时就可以根据 m
来更准确的判断 T 了。但如果每个方法都这么写,让方法的调用者要额外传
入 m 参数,非常不友好,且对方法的设计是一道伤疤。好在 scala 中有隐式转
换、隐式参数的功能,在这个地方可以用隐式参数来减轻调用者的麻烦。
def foo[T](x: List[T])(implicit m: Manifest[T]) = {
  println(m)
  if (m <:< manifest[String])
    println("Hey, this list is full of strings")
  else
    println("Non-stringy list")
}
foo(List("one", "two")) // Hey, this list is full of strings
foo(List(1, 2)) // Non-stringy list
foo(List("one", 2)) // Non-stringy list
 
 
  隐式参数 m 是由编译器根据上下文自动传入的,比如上面是编译器根
据 "one","two" 推断出 T 的类型是 String,从而隐式的传入了一个
Manifest[String]类型的对象参数,使得运行时可以根据这个参数做更多的事
情。
  不过上面的 foo 方法定义使用隐式参数的方式,仍显得啰嗦,于是 scala
里又引入了“上下文绑定”,
  def foo[T](x: List[T]) (implicit m: Manifest[T])
  可以简化为:
  def foo[T:Manifest] (x: List[T])
  在引入 Manifest 的时候,还引入了一个更弱一点的 ClassManifest,所谓
的弱是指类型信息不如 Manifest 那么完整,主要针对高阶类型的情况
scala 在 2.10 里却用 TypeTag 替代了 Manifest,用 ClassTag 替代了
ClassManifest,原因是在路径依赖类型中,Manifest 存在问题:
scala> class Foo{class Bar}
defined class Foo

scala> def m(f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar]) = ev
m: (f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar])Manifest[f.Bar]

scala> val f1 = new Foo;val b1 = new f1.Bar
f1: Foo = Foo@67d18ed7
b1: f1.Bar = Foo$Bar@2c78d320

scala> val f2 = new Foo;val b2 = new f2.Bar
f2: Foo = Foo@4b41dd5c
b2: f2.Bar = Foo$Bar@3b96c42e

scala> val ev1 = m(f1)(b1)
ev1: Manifest[f1.Bar] = Foo@67d18ed7.type#Foo$Bar

scala> val ev2 = m(f2)(b2)
ev2: Manifest[f2.Bar] = Foo@4b41dd5c.type#Foo$Bar

scala> ev1 == ev2 // they should be different, thus the result is wrong
res0: Boolean = true

了解之后,我们总结一下,TypeTag到底有啥用呢?看下面的例子: 
请留意: 
=:=,意思为:type equality 
<:< ,意思为:subtype relation 
类型判断不要用 == 或 !=

class Animal{}
class Dog extends Animal{}

object MainFoo extends App{
  override def main(args: Array[String]): Unit = {
    val list1 = List(1, 2, 3)
    val list2 = List("1", "2", "3")
    val list3 = List("1", "2", 3)

    def test1(x: List[Any]) = {
      x match {
        case list: List[Int] => "Int list"
        case list: List[String] => "String list"
        case list: List[Any] => "Any list"
      }
    }
    println(test1(list1)) //Int list
    println(test1(list2)) //Int list
    println(test1(list3)) //Int list

    import scala.reflect.runtime.universe._
    def test2[A : TypeTag](x: List[A]) = typeOf[A] match {
      case t if t =:= typeOf[String] => "String List"
      case t if t <:< typeOf[Animal] => "Dog List"
      case t if t =:= typeOf[Int] => "Int List"
    }

    println(test2(List("string"))) //String List
    println(test2(List(new Dog))) //Dog List
    println(test2(List(1, 2))) //Int List
  }
}

13.7 多重界定

不能同时有多个上界或下界,变通的方式是使用复合类型
T <: A with B
T >: A with B
可以同时有上界和下界,如
T >: A <: B
这种情况下界必须写在前边,上界写在后边,位置不能反。同时A要符合B的子类型,A与B不能是两个无关的类型。
可以同时有多个view bounds
T <% A <% B
这种情况要求必须同时存在 T=>A的隐式转换,和T=>B的隐式转换。

class A{}
class B{}
implicit def string2A(s:String) = new A
implicit def string2B(s:String) = new B
def foo2[ T <% A <% B](x:T)  = println("foo2 OK")
foo2("test")

 可以同时有多个上下文界定 
T : A : B 
这种情况要求必须同时存在C[T]类型的隐式值,和D[T]类型的隐式值。

class C[T];
class D[T];
implicit val c = new C[Int]
implicit val d = new D[Int]
def foo3[ T : C : D ](i:T) = println("foo3 OK")
foo3(2)

13.8 类型约束

 类型约束,提供了限定类型的另一种方式,一共有3中关系声明: 
T =:= U意思为:T类型是否等于U类型 
T <:< U意思为:T类型是否为U或U的子类型 
T <%< U 意思为:T 类型是否被隐式(视图)转化为 U

如果想使用上面的约束,需要添加“隐式类型证明参数” 比如:

class Pair5[T] (val first: T, val second: T)(implicit ev: T <:< Comparable[T]){}

使用举例:

import java.io.File

class Pair6[T](val first: T, val second: T) {
  def smaller(implicit ev: T <:< Ordered[T]) = {
    if(first < second) first else second
  }
}

object Main6 extends App{
  override def main(args: Array[String]): Unit = {
    //构造Pair6[File]时,注意此时是不会报错的
    val p6 = new Pair6[File](new File(""), new File(""))
    //这就报错了
    p6.smaller
  }
}

13.9 型变

术语:

英文 中文 示例
Variance 型变 Function[-T, +R]
Nonvariant 不变 Array[A]
Covariant 协变 Supplier[+A]
Contravariant 逆变 Consumer[-A]
Immutable 不可变 String
Mutable 可变  StringBuilder

 

其中,Mutable常常意味着Nonvariant,但是Noncovariant与Mutable分别表示两个不同的范畴。


即:可变的,一般意味着“不可型变”,但是“不可协变”和可变的,分别表示两个不同范畴。


型变(Variance)拥有三种基本形态:协变(Covariant), 逆变(Contravariant), 不变(Nonconviant),可以形式化地描述为:


一般地,假设类型C[T]持有类型参数T;给定两个类型A和B,如果满足A <: B,则C[A]与 C[B]之间存在三种关系:

如果C[A] <: C[B],那么C是协变的(Covariant);
如果C[A] :> C[B],那么C是逆变的(Contravariant);
否则,C是不变的(Nonvariant)。

 Scala的类型参数使用+标识“协变”,-标识“逆变”,而不带任何标识的表示“不变”(Nonvariable):

trait C[+A]   // C is covariant
trait C[-A] // C is contravariant
trait C[A] // C is nonvariant

如何判断一个类型是否有型变能力:

一般地,“不可变的”(Immutable)类型意味着“型变”(Variant),而“可变的”(Mutable)意味着“不变”(Nonvariant)。
其中,对于不可变的(Immutable)类型C[T]
如果它是一个生产者,其类型参数应该是协变的,即C[+T];
如果它是一个消费者,其类型参数应该是逆变的,即C[-T]。

13.10 协变和逆变点

先给一个示例,定义一个类 A,其类型参数是协变的: 
class A[+T] {
  def func(x: T) {}
} 
上面的代码通不过编译,报错如下:
covariant type T occurs in contravariant position in type T of value x
要解释这个问题,需要理解协变点和逆变点的概念。我们可以考虑这样
一种情况来解释程序为什么报错,既然 A 的类型参数 T 是协变的,那么
A[AnyRef]是 A[String]的父类,A[AnyRef]对应的 func 为 func(AnyRef),
A[String]对应的 func 为 func(String),我们定义 father 是一个 A[AnyRef]实
例,child 是 A[String]实例。当我定义了另一个函数 g,g 的参数为
A[AnyRef],因此 g(father)当然是没有问题的,又因为 child 是 father 的子类,
因此按理来说 g(child)也是没有问题的,但是 father 的 func 可以接受 AnyRef
类型的参数,而 child 的 func 只能接受 String 类型的参数。因此,如果编译
器允许用 child 替换 father,那么替换后 g 中的参数调用 func 就只能传入
String 类型的参数了,相当于 g 的处理范围缩小了。所以编译器不允许这种情
况,因此会报错。反过来一想,如果传入的是 father 的父类,那么 g 的处理
范围就变大了,所有适用于 father 的情况都适用于 father 的父类,因此,如果
把 A 的类型参数 T 声明为逆变的,就不会有问题了。 
 
class A[-T] {
  def func(x: T) {}
} 
总结:传入 A 的类型参数会作为 A 中方法的参数的类型(如果有参数的
话),我们知道一个方法中如果有类型为 X 的参数,那么这个方法可以接受类
型为 X 的子类的参数。同理上面的情况,func 原来可以接受类型为 AnyRef
及 AnyRef 的子类作为参数,但是如果一协变,那么 func 就只能接受类型为
String 及 String 的子类作为参数,作用范围减小了。 
 
 
  因此方法的参数的位置被称为逆变点,A 中的类型参数声明为协变,因
此编译时出错,声明为协变则没有问题。
而方法返回值的位置被称为协变点,同样考虑上面的情况: 
 
class A[+T] {
  def func(): T = {
    null.asInstanceOf[T]
  }
} 
也是考虑 father:A[AnyRef]和 child:A[String],当用 child 替换 father 后,
child 的 func 方法会返回 String 类型的对象来替换 AnyRef,因此是合理的。
 
原文地址:https://www.cnblogs.com/LXL616/p/11133578.html