为什么参数位于逆变位置?

31

我正在尝试在trait内使用协变类型参数来构造一个类似于case-class的结构,代码如下:

trait MyTrait[+T] {
  private case class MyClass(c: T)
}

编译器提示:
error: covariant type T occurs in contravariant position in type T of value c

我尝试了以下方法,但仍然无法解决问题:
trait MyTrait[+T] {
  private case class MyClass[U <: T](c: U)
}

这次的错误是:
error: covariant type T occurs in contravariant position in type >: Nothing <: T of type U

有谁能解释一下这里为什么T处于协变位置,并提供一个解决问题的方法吗?谢谢!


你能解释一下你真正想做什么吗?为什么你想要T是协变而不是不变的? - Daniel Martin
2个回答

61
这是面向对象编程的一个基本特性,但往往没有得到足够的重视。
假设你有一个集合C[+T]。这里的+T表示如果U <: T,那么C[U] <: C[T]。很好理解。但什么是子类呢?它意味着每个方法都应该可用于原始类。因此,假设你有一个方法m(t: T)。这意味着你可以拿任何t并对其进行操作。但是C[U]只能对U进行操作,而这可能不是T的全部!所以你立即就否认了C[U]C[T]的子类的说法。它不是。有些事情你可以用C[T]做,但你不能用C[U]做。
那么,如何解决这个问题呢?
一种选择是使类不变(去掉+)。另一种选择是如果你需要一个方法参数,允许使用任何超类m[S >: T](s: S)。现在如果T变成U,也没关系:T的超类也是U的超类,方法将正常工作。(但是,你必须更改你的方法以处理这些情况。)
对于一个 case class,除非你使它不变,否则很难做到正确。我建议这样做,并将泛型和方差推到其他地方。但我需要看到更多的细节才能确定这是否适用于你的用例。

谢谢您的回答。然而,在这种情况下,您的解决方案对我不起作用。删除协变并使特质不变会起作用,但这不是我想要的。允许方法(或在我的情况下是 case-class)接受超类型也不令人满意。 我很好奇为什么 case-class 更难正确使用。请注意,没有 case 关键字的相同代码可以正常工作。 - lapislazuli
2
@lapislazuli - 因为 case 类包括一个伴生方法来创建它们(以 T 作为参数),所以您必须遵守上面的方法限制。如果不包括 case,则该类不会暗示在接口中有一个以 T 为参数的方法。 - Rex Kerr
1
class F[+A] { def f(x: A) = ??? } 中,为什么 x 处于 逆变位置?这是由于 [Function-T1, +R 中的 T逆变的 所致吗? - Kevin Meredith

13
几乎完成了。这里是:
scala> trait MyTrait[+T] {
     |   private case class MyClass[U >: T](c: U)
     | }
defined trait MyTrait

这意味着MyClass[Any]对于所有的T都是有效的。这就是为什么不能在该位置使用T的根本原因,但演示需要比我现在想象的代码更多。 :-)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接