scala雾中风景(10): 逆变点与协变点

这个问题来自之前这篇scala类型系统:15) 协变与逆变的评论里的问题

遇见一个这样的问题

class In[+A]{ def fun(x:A){} }

会提示

error: covariant type A occurs in contravariant position in type A of value x
class In[+A]{def fun(x:A){}}
                     ^

而这样不会出现问题

class In[-A]{ def fun(x:A){} }

要解释清楚这个问题,需要理解协变点(covariant position) 和 逆变点(contravariant position)

首先,我们假设 class In[+A]{ def fun(x:A){} } 可以通过编译,那么对于 In[AnyRef]In[String] 这两个父子类型来说,fun方法分别对应:

父类型 In[AnyRef] 中的方法 fun(x: AnyRef){}

子类型 In[String] 中的方法 fun(x: String){}

根据里氏替换原则,所有使用父类型对象的地方都可以换成子类型对象。现在问题就来了,假设这样使用了父类:

father.fun(notString)

现在替换为子类:

child.fun(notString) // 失败,非String类型,不接受

之前父类型的 fun 可以接收 AnyRef类型的参数,是一个更广的范围。而子类型的 fun 却只能接收String这样更窄的范围。显然这不符合里氏替换原则了,因为父类做的事情,子类不能完全胜任,只能部分满足,是无法代替父类型的。

所以要想符合里氏替换,子类型中的fun函数参数类型必须是父类型中函数参数的超类(至少跟父类型中的参数类型一致),这样才能满足父类中fun方法可以做的事情,子类中fun方法也都可以做。

正是因为需要符合里氏替换法则,方法中的参数类型声明时必须符合逆变(或不变),以让子类方法可以接收更大的范围的参数(处理能力增强);而不能声明为协变,子类方法可接收的范围是父类中参数类型的子集(处理能力减弱)。

方法参数的位置称为做逆变点(contravariant position)。所以上面声明类型参数A是协变的,用在方法参数中时会编译报错,声明A是逆变(或不变)时则符合。

现在看看什么是协变点(covariant position),还用上面的例子,稍微修改一下:

// 方法返回值类型可以是协变的
scala> class In[+A]{ def fun(): A = null.asInstanceOf[A] }
defined class In

// 方法返回值类型不能是逆变的
scala> class In[-A]{ def fun(): A = null.asInstanceOf[A] }
<console>:8: error: contravariant type A occurs in covariant position in type ()A of method fun

同样用里氏替换法则来套:

父类型 In[AnyRef] 中的方法 fun() 得到结果 AnyRef

子类型 In[String] 中的方法 fun() 得到结果 String

这是很容易理解的,子类方法得到的结果比父类更“具象”一些,也就是说子类方法的处理能力更强一些。如果结果类型是逆变的,那子类方法的处理能力是减弱的,不符合里氏替换。

方法返回值的位置称为协变点(covariant position)。同理,A类型声明协变(或不变),编译时符合要求;声明逆变则报错。

现在我们再回顾:

class In[-A] { def fun(x: A) {} } 

我们完全可以把它看做一个函数类型,即 A => UnitFunction1[-A, Unit]等价,而

class In[+A]{ def fun(): A = null.asInstanceOf[A] }

则与 Function0[+A] 等价。

另外参考:
1) scala中函数类型的多态
2) scala类型系统:16) 函数类型

scala雾中风景(10): 逆变点与协变点》上有8条评论

  1. breeze

    博主好。最近在看scala for the impatient。正好看到这样一章。
    里面有一段说法感觉有问题。
    里面说法是
    不过,在函数参数中,型变是反转过来的–它的参数是协变的。比如下面Iterable[+A]的foldLeft方法:
    foldLeft[B](z:B)(op:(B,A)=>B):B
    – ++ – +
    我看了一下foldLeft的签名是。
    override def foldLeft[B](z : B)(f : scala.Function2[B, A, B]) : B
    其中Function2的签名是
    trait Function2[@scala.specialized -T1, @scala.specialized -T2, @scala.specialized +R] extends scala.AnyRef

    看起来也是参数逆变,返回协变啊。是不是书上写错了。

    回复
  2. mmm

    感谢分享。感觉对 trait Function1[-T1, +R] { ..} 开始共鸣。在另外一个地方看到contravariant functor,以前学的时候貌似见过例子,但一时想不起来了。

    回复
  3. sangs

    class In[+A]{def fun[A](x:A){}}
    函数加上泛型声明就没问题了

    回复
  4. 晨过.微语

    感谢博主,看快学Scala还有些疑惑,看博主的文章简单易懂

    回复
    1. 夏天

      没看懂,协变就是可以变化,逆变就是不变的意思??

      回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注