问题描述
我刚刚开始学习Scala,我正在上Odersky的课程,并且坚持理解方差。我也指的是这个博客-https://medium.com/@wiemzin/variances-in-scala-9c7d17af9dc4
我不理解对象扩展类型A
的示例,但仍然会引发编译错误:
sealed trait A
case object B extends A
case object C extends A
case object D extends A
,然后再更新
:val list: List[A] = Cons(B,Cons(C,Cons(D,Nil()))) //compile error: class List is invariant in type A.
我不明白为什么编译失败。对象B
,C
,D
确实扩展了A,是A的子类型,所以为什么不允许此操作?
在我看来,“协变量”和“反变量”这两个词在某种程度上似乎并不直观,而且我无法理解博客文章中确切传达的内容。
这是强制执行严格的编译时类型检查的方法吗?我来自Java背景,但是以某种方式我发现这一切都很令人困惑。
解决方法
差异注释的目的是为了防止在编译时出现不希望得到的值。
考虑
sealed trait Animal
class Dog() extends Animal
存在三种有意义的方差类型。
- 协方差:
CovariantOf[Dog]
是CovariantOf[Animal]
的子类型 - 协方差:
ContravariantOf[Animal]
是ContravariantOf[Dog]
的子类型 - 不变性:
InvariantOf[Animal]
和InvariantOf[Dog]
之间没有关系(即既不协变也不相反)
让我们首先考虑一些可以使我们受益的东西:
trait Producer[A] {
def getOne: A
}
让我们想象Producer
是逆变的:Producer[Animal]
是Producer[Dog]
。那么我们可以:
class Wildebeest() extends Animal
val animalProducer = new Producer[Animal] { def getOne: Animal = new Wildebeest() }
val dogProducer: Producer[Dog] = animalProducer
val dog: Dog = dogProducer.getOne
animalProducer
仅承诺产生Animal
,而实际上仅产生Wildebeest
,但是最后一行期望Dog
。唯一棘手的事情是将Producer[Animal]
视为Producer[Dog]
,因为一般的Producer[Animal]
不会承诺只产生Dog
。由于逆变意味着我们可以做到这一点,因此宣布Producer
为逆变是不合法的。
另一方面,如果生产者是协变的,我们可以
val dogProducer = new Producer[Dog] { def getOne: Dog = new Dog() }
val animalProducer: Producer[Animal] = dogProducer
val animal: Animal = animalProducer.getOne
这显然是可以的,因为Dog
是Animal
,所以可以声明
trait Producer[+A] {
def getOne: A
}
同时,让我们建模一些我们只能产生Animal
到的东西:Animal
进来但它们不出来的东西:
trait Consumer[A] {
def consume(a: A): Unit
}
Consumer
应该是协变的吗?不,因为想象我们有一个狗叫声:
val dogPetter = new Consumer[Dog] {
def consume(dog: Dog): Unit = println("petting a dog")
}
dogPetter
只懂得如何养狗。让它pet成一团是很愚蠢的。但是,如果我们使Consumer
为协变量,则可以将其设为pet slug:
class Slug() extends Animal
val animalPetter: Consumer[Animal] = dogPetter // no cast needed,since covariance would mean a Consumer[Dog] is a Consumer[Animal]
val slug = new Slug()
animalPetter.consume(slug) // Uh-oh,the dogPetter is petting a slug!
dogPetter
的定义非常好。将Slug
当作动物是完全可以的:Consumer[Animal]
绝对应该能够consume
和Slug
。因此,问题必须出在将dogPetter
视为Animal
字符的情况下,这意味着问题在于协方差。
在另一面,假设我们有一个物体可以称赞Animal
,任何Animal
:
val animalPraiser = new Consumer[Animal] {
def consume(a: Animal): Unit = println("I praise this ${a.getClass} (like I should)")
}
它可以明显地称赞Dog
或Slug
或任何其他Animal
,所以这应该是有效的:
val dogPraiser: Consumer[Dog] = animalPraiser
dogPraiser.consume(new Dog)
因此,Consumer
可以是反变量:
class Consumer[-A] { ... }
直觉(绝对正确)是,A
中通用的类型如果不消耗A
,则只能在A
中进行协变;同样,如果它不产生A
,则只能在A
中变。
如果我们有一些可以生产和消费的东西怎么办?好吧,它必须是不变的,因为它不能协变,也不能不变。不变性是保守的默认值,因为假设协变或逆变会导致某些不合理的位置。
Java同样具有协方差和相反方注解:
-
Foo<? extends Bar>
进行协方差 -
Foo<? super Bar>
求逆
Scala的主要区别在于,在Java中,您将在使用站点上具有批注,而在Scala中,它将在定义站点上具有批注。 Scala方法可以减少重复的方差注释。