scala类型系统:15) 协变与逆变

对于一个带类型参数的类型,比如 List[T],如果对A及其子类型B,满足 List[B]也符合 List[A]的子类型,那么就称为covariance(协变),如果 List[A]List[B]的子类型,即与原来的父子关系正相反,则称为contravariance(逆变)

协变:

 _____               _____________ 
|     |             |             |
|  A  |             |  List[ A ]  |
|_____|             |_____________|
   ^                       ^ 
   |                       | 
 _____               _____________ 
|     |             |             |
|  B  |             |  List[ B ]  |
|_____|             |_____________|  

逆变:

 _____               _____________ 
|     |             |             |
|  A  |             |  List[ B ]  |
|_____|             |_____________|
   ^                       ^ 
   |                       | 
 _____               _____________ 
|     |             |             |
|  B  |             |  List[ A ]  |
|_____|             |_____________|  

如果一个类型支持协变或逆变,则称这个类型为variance(翻译为可变的或变型),否则称为invariant(不可变的)

在Java里,泛型类型都是invariant,比如 List<String> 并不是 List<Object> 的子类型。Java并不支持声明点变型(declaration-site variance,即在定义一个类型时声明它为可变型,也称definition-site),而scala支持,可以在定义类型时声明(用加号表示为协变,减号表示逆变),如:

trait List[+T] // 在类型定义时(declaration-site)声明为协变 

这样会把List[String]作为List[Any]的子类型。

不过Java支持使用点变型(use-site variance),所谓“使用点“,也就是在声明变量时:

List<? extends Object> list = new ArrayList<String>();

scala为了兼容java泛型通配符的形式,引入存在类型(existential type,后边再讲)时,也支持了使用点变型(use-site variance)

scala> val a : List[_ <: Any] = List[String]("A")
a: List[_] = List(A)    

要注意variance并不会被继承,父类声明为variance,子类如果想要保持,仍需要声明:

scala> trait A[+T]

scala> class C[T] extends A[T]  // C是invariant的

scala> class X; class Y extends X;

scala> val t:C[X] = new C[Y]
<console>:11: error: type mismatch; 
 found   : C[Y]
 required: C[X]
Note: Y <: X, but class C is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)

必须也对C声明为协变的才行:

scala> class C[+T] extends A[T]

scala> val t:C[X] = new C[Y]
t: C[X] = C@6a079142

对于协变与逆变的使用,参考《Effective Java》的PECS原则。这里有篇旧文也可供参考。

scala类型系统:14) multiple bounds

在之前的:scala类型系统:11) upper bounds & lower bounds 结尾提到过在multiple bounds(多重界定)情况下java与scala的一些差异,这篇详细说一下scala里的multiple bounds。

1 不能同时有多个upper bounds 或 lower bounds,变通的方式是使用复合类型
T <: A with B

T >: A with B
2 可以同时有upper bounds 和 lower bounds,如
T >: A <: B

这种情况 lower bounds 必须写在前边,upper bounds写在后边,位置不能反。同时A要符合B的子类型,A与B不能是两个无关的类型。

3 可以同时有多个view bounds
T <% A <% B

这种情况要求必须同时存在 T=>A的隐式转换,和T=>B的隐式转换。

scala> implicit def string2A(s:String) = new A
scala> implicit def string2B(s:String) = new B

scala> def foo[ T <% A <% B](x:T)  = println("OK")

scala> foo("test")
OK
4 可以同时有多个context bounds
T : A : B

这种情况要求必须同时存在A[T]类型的隐式值,和B[T]类型的隐式值。

scala> class A[T];
scala> class B[T];

scala> implicit val a = new A[Int]
scala> implicit val b = new B[Int]

scala> def foo[ T : A : B ](i:T) = println("OK")

scala> foo(2)
OK

scala类型系统:13) context bounds

view bounds一样context bounds(上下文界定)也是隐式参数的语法糖。context bounds的出现是scala2.8版本才增加的,起因是2.8版本修正了数组类型的设计(关于这个话题后续单独讨论),同时为语法上的方便,引入了”上下文界定”这个概念。

看一个简单的例子,对于比较两个数的大小,采用隐式参数的做法:

scala> def max[T](a:T, b:T) (implicit cp : Comparator[T]) = { 
            if (cp.compareTo(b) > 0) a else b 
        }

可以简化为:

scala> def max[T : Comparator] (a:T, b:T) = { … }

上面的类型参数声明 T : Comparator 就表示存在一个 Comparator[T]类型的隐式值。

等等,原max方法里是要用到 cp 这个隐式参数的,省略后怎么用这个参数呢?有两个途径:

第一种是,在内部定义函数并声明隐式参数

scala> def max[T : Comparator] (a:T, b:T) = { 

            def inner(implicit c:Comparator[T]) = c.compare(a,b);  

            if(inner>0) a else b 
        }

这种做法只是把外部方法的隐式参数隐藏了,放到内部嵌套函数上;还有一个推荐的做法,使用implicitly方法:

scala> def max2[T : Comparator] (a:T, b:T) = { 

            val cp = implicitly[Comparator[T]]

            if( cp.compare(a,b)>0) a else b 
        }

implicitly是在Predef.scala里定义的,它是一个特殊的方法,编译器会记录当前上下文里的隐式值,而这个方法则可以获得某种类型的隐式值。

如果上下文中存在一个Comparator[Int]类型的隐式值:

scala> implicit val c = new Comparator[Int]{ 
                override def compare(a:Int, b:Int) = a - b 
        }

那么对maxmax2都可以对Int型的参数正确执行:

scala> max(1,2)
res4: Int = 2

scala> max2(3,2)
res5: Int = 3

scala类型系统:12) view bounds

很早前在来往的扎堆里发过一段模拟C#的using代码:

def using[ C <: { def close(): Unit }, T ] (resource: C) (handle: C => T): T = {
    try {
        handle(resource)
    } finally {
      closeQuietly(resource)
    }
}

Eric(药师)看到后,说 <% 更好

<%的意思是“view bounds”(视界),它比<:适用的范围更广,除了所有的子类型,还允许隐式转换过去的类型

def method [A <% B](arglist): R = ...

等价于:

def method [A](arglist)(implicit viewAB: A => B): R = ...

或等价于:

implicit def conver(a:A): B = …

def method [A](arglist): R = ...

<% 除了方法使用之外,class声明类型参数时也可使用:

scala> class A[T <% Int]
defined class A

但无法对trait的类型参数使用 <%

scala> trait A[T <% Int]
<console>:1: error: traits cannot have type parameters with context bounds `: ...' nor view bounds `<% ...'

补充,注意上面 using 方法中声明的结构类型 {def close():Unit},它的实现是通过反射做到的,所以有一定性能问题,在需要考虑性能的场景下,这里明确的用 java.io.Closeable 接口更合适,灵活性与性能你总要做一个选择。