class Pets[+A](val pet:A) {
def add(pet2: A): String = "done"
}
我对这段代码片段的理解是Pets是协变的,可以接受A子类型的对象。但是,add函数仅接受类型为A的参数。协变意味着Pets可以接受类型为A及其子类型的参数。那么这个代码应该怎么出错呢?反变甚至不应该出现任何问题。
对上述错误消息的任何说明都将非常有帮助。谢谢
class Pets[+A](val pet:A) {
def add(pet2: A): String = "done"
}
我对这段代码片段的理解是Pets是协变的,可以接受A子类型的对象。但是,add函数仅接受类型为A的参数。协变意味着Pets可以接受类型为A及其子类型的参数。那么这个代码应该怎么出错呢?反变甚至不应该出现任何问题。
对上述错误消息的任何说明都将非常有帮助。谢谢
TL;DR:
你的 Pets
类可以通过返回成员变量 pet
来产生类型为 A
的值,因此 Pet[VeryGeneral]
不能是 Pet[VerySpecial]
的子类型,因为当它产生一个 VeryGeneral
的东西时,它不能保证它也是一个 VerySpecial
的实例。因此,它不能是逆变的。
你的 Pets
类可以通过将类型为 A
的值作为参数传递给 add
来消耗这些值。因此,Pet[VerySpecial]
不能是 Pet[VeryGeneral]
的子类型,因为它会在任何不是 VerySpecial
的输入上出错。因此,你的类不能是协变的。
唯一剩下的可能性是:Pets
在 A
上必须是不变的。
###说明:协变与逆变
我将利用这个机会呈现一个改进且更加严谨的版本这个漫画。它是一种编程语言的协变和逆变概念的说明,具有子类型和声明站点的方差注释(显然,即使Java人也发现它足够启迪人心,尽管问题是关于使用站点方差的)。
首先,这是插图:
现在是更详细的说明,包括可编译的Scala代码。
###逆变解释(图1左侧)
考虑以下能源来源层次结构,从非常通用到非常特定:
class EnergySource
class Vegetables extends EnergySource
class Bamboo extends Vegetables
Consumer[-A]
,它只有一个consume(a: A)
方法:trait Consumer[-A] {
def consume(a: A): Unit
}
让我们来实现一些这个特质的例子:
object Fire extends Consumer[EnergySource] {
def consume(a: EnergySource): Unit = a match {
case b: Bamboo => println("That's bamboo! Burn, bamboo!")
case v: Vegetables => println("Water evaporates, vegetable burns.")
case c: EnergySource => println("A generic energy source. It burns.")
}
}
object GeneralistHerbivore extends Consumer[Vegetables] {
def consume(a: Vegetables): Unit = a match {
case b: Bamboo => println("Fresh bamboo shoots, delicious!")
case v: Vegetables => println("Some vegetables, nice.")
}
}
object Panda extends Consumer[Bamboo] {
def consume(b: Bamboo): Unit = println("Bamboo! I eat nothing else!")
}
Consumer
必须在A
方面是逆变的呢?让我们尝试实例化几种不同的能源,然后将它们提供给各种消费者:val oilBarrel = new EnergySource
val mixedVegetables = new Vegetables
val bamboo = new Bamboo
Fire.consume(bamboo) // ok
Fire.consume(mixedVegetables) // ok
Fire.consume(oilBarrel) // ok
GeneralistHerbivore.consume(bamboo) // ok
GeneralistHerbivore.consume(mixedVegetables) // ok
// GeneralistHerbivore.consume(oilBarrel) // No! Won't compile
Panda.consume(bamboo) // ok
// Panda.consume(mixedVegetables) // No! Might contain sth Panda is allergic to
// Panda.consume(oilBarrel) // No! Pandas obviously cannot eat crude oil
火
可以消耗广义食草动物
所能消耗的一切,反过来广义食草动物
可以消耗熊猫
所能吃的一切。因此,只要我们关心能量来源的消耗能力,就可以在需要蔬菜消费者
的地方替换为能量源消费者
,并且可以在需要竹子消费者
的地方替换为蔬菜消费者
。因此,能量源消费者 <: 蔬菜消费者
和蔬菜消费者 <: 竹子消费者
是有意义的,即使类型参数之间的关系完全相反:type >:>[B, A] = A <:< B
implicitly: EnergySource >:> Vegetables
implicitly: EnergySource >:> Bamboo
implicitly: Vegetables >:> Bamboo
implicitly: Consumer[EnergySource] <:< Consumer[Vegetables]
implicitly: Consumer[EnergySource] <:< Consumer[Bamboo]
implicitly: Consumer[Vegetables] <:< Consumer[Bamboo]
###协方差的解释(图1右侧)
定义产品的层次结构:
class Entertainment
class Music extends Entertainment
class Metal extends Music // yes, it does, seriously^^
定义一个特征,能够生成类型为A
的值:
trait Producer[+A] {
def get: A
}
定义各种不同专业水平的“来源”/“生产者”:
object BrowseYoutube extends Producer[Entertainment] {
def get: Entertainment = List(
new Entertainment { override def toString = "Lolcats" },
new Entertainment { override def toString = "Juggling Clowns" },
new Music { override def toString = "Rick Astley" }
)((System.currentTimeMillis % 3).toInt)
}
object RandomMusician extends Producer[Music] {
def get: Music = List(
new Music { override def toString = "...plays Mozart's Piano Sonata no. 11" },
new Music { override def toString = "...plays BBF3 piano cover" }
)((System.currentTimeMillis % 2).toInt)
}
object MetalBandMember extends Producer[Metal] {
def get = new Metal { override def toString = "I" }
}
BrowseYoutube
是最通用的娱乐
来源之一:它可以提供基本上任何类型的娱乐:猫视频、杂耍小丑或(偶然)一些音乐。在图1中,这种通用的娱乐
来源由原型小丑代表。
RandomMusician
已经有了一定的专业性,至少我们知道这个对象产生音乐(即使没有限制于特定的流派)。
最后,MetalBandMember
非常专业化:保证get
方法仅返回非常具体的金属
音乐。
让我们尝试从这三个对象获取不同类型的娱乐
:
val entertainment1: Entertainment = BrowseYoutube.get // ok
val entertainment2: Entertainment = RandomMusician.get // ok
val entertainment3: Entertainment = MetalBandMember.get // ok
// val music1: Music = BrowseYoutube.get // No: could be cat videos!
val music2: Music = RandomMusician.get // ok
val music3: Music = MetalBandMember.get // ok
// val metal1: Metal = BrowseYoutube.get // No, probably not even music
// val metal2: Metal = RandomMusician.get // No, could be Mozart, could be Rick Astley
val metal3: Metal = MetalBandMember.get // ok, because we get it from the specialist
我们可以看到,所有三个Producer[Entertainment]
、Producer[Music]
和Producer[Metal]
都可以生产某种类型的Entertainment
。
我们可以看到,只有Producer[Music]
和Producer[Metal]
能够保证生产Music
。
最后,我们可以看到,只有非常专业化的Producer[Metal]
能够保证仅生产Metal
而不是其他任何东西。因此,Producer[Music]
和Producer[Metal]
可以替换为一个Producer[Entertainment]
。 Producer[Metal]
可以替换Producer[Music]
。
一般来说,一个更具体产品的生产者可以替换为一个不太专业的生产者:
implicitly: Metal <:< Music
implicitly: Metal <:< Entertainment
implicitly: Music <:< Entertainment
implicitly: Producer[Metal] <:< Producer[Music]
implicitly: Producer[Metal] <:< Producer[Entertainment]
implicitly: Producer[Music] <:< Producer[Entertainment]
相关链接
关于Java 8中? extends A
和? super B
的类似讨论:Java 8 Comparator
comparing()
静态函数
经典问题:“在我自己的Either
实现中,flatMap
的正确类型参数是什么”:Type L
appears in contravariant position in Either[L, R]
类 Pets
在其类型 A 上是 协变的(因为它被标记为 +A),但您正在一个 逆变的 位置使用它。这是因为,如果您查看 Scala 中的 Function 特质,您会发现输入参数类型是逆变的,而返回类型是协变的。 每个函数在其输入类型上是逆变的,在其返回类型上是协变的。
例如,接受一个参数的函数具有以下定义:
trait Function1[-T1, +R]
S
成为函数F
的子类型来说,它需要“要求(相同或)更少并提供(相同或)更多”。这也被称为Liskov替换原则。在实践中,这意味着Function特质需要在其输入方面是逆变的,在其输出方面是协变的。通过在其输入方面进行逆变,它需要“相同或更少”,因为它接受T1
或任何其超类型(这里的“更少”意味着“超类型”,因为我们放宽了限制,例如从Fruit到Food)。此外,通过在其返回类型方面进行协变,它需要“相同或更多”,这意味着它可以返回R
或任何比它更具体的东西(这里的“更多”意味着“子类型”,因为我们添加了更多信息,例如从Fruit到Apple)。val f: Fruit => Fruit
val s: Food => Apple
函数s
是f
函数的有效子类型,因为它需要更少的信息(从水果到食品时我们“失去”了信息),并提供更多的信息(从水果到苹果时我们“获得”了信息)。请注意,s
具有输入类型,该类型是f
输入类型的超类型(逆变性),并且具有返回类型,该类型是f
返回类型的子类型(协变性)。现在让我们想象一段使用这些函数的代码:
def someMethod(fun: Fruit => Fruit) = // some implementation
调用 someMethod(f)
和 someMethod(s)
都是有效的。方法 someMethod
内部使用 fun
来将水果应用到它上面,并从中接收水果。由于 s
是 f
的子类型,这意味着我们可以提供 Food => Apple
作为 fun
的完全良好实例。在 someMethod
中的代码将在某个时间点上向 fun
提供一些水果,这是可以接受的,因为 fun
接受食物,而水果是食物。另一方面,fun
具有 Apple
作为返回类型也是可以的,因为 fun
应该返回水果,通过返回苹果来遵守合同。
希望我能澄清一些问题,如有进一步问题,请随时提出。
class Pets[A]
,但如果这不是一个选项,你可以通过 def add[B >: A](pet2: B): String = "done"
来规避它。 - slouc