我相信可以将协变(至少对于对象)定义为“使用较窄(子)类型的值代替较宽(超)类型的值的能力”,而逆变与这。
显然,Scala 函数是逆变参数类型 A1 等和协变返回类型 B 的 Function[-A1,...,+B] 的实例。虽然这对于函数的子类型化很方便,但上述定义不应该意味着我可以将任何超类型作为参数传递吗?
请告知我在哪里弄错了。
我相信可以将协变(至少对于对象)定义为“使用较窄(子)类型的值代替较宽(超)类型的值的能力”,而逆变与这。
显然,Scala 函数是逆变参数类型 A1 等和协变返回类型 B 的 Function[-A1,...,+B] 的实例。虽然这对于函数的子类型化很方便,但上述定义不应该意味着我可以将任何超类型作为参数传递吗?
请告知我在哪里弄错了。
协变和逆变是类的性质而不是参数的性质。(它们是取决于参数的品质,但它们对类做出陈述。)
因此,Function1[-A,+B]
意味着一个接受超类的函数A
可以被视为原始函数的子类。
让我们在实践中看看:
class A
class B extends A
val printB: B => Unit = { b => println("Blah blah") }
val printA: A => Unit = { a => println("Blah blah blah") }
现在假设您需要一个知道如何打印 a 的函数B
:
def needsB(f: B => Unit, b: B) = f(b)
你可以通过printB
。但你也可以传入printA
,因为它也知道如何打印B
s(以及更多!),就好像A => Unit
是B => Unit
. 这正是逆变的意思。这并不意味着您可以传递Option[Double]
并printB
获得任何东西,但编译时错误!
(协方差是另一种情况:M[B] <: M[A]
如果B <: A
。)
这个问题很老了,但我认为更清晰的解释是引用 Liskov 替换原则:关于超类的所有正确的东西都应该适用于它的所有子类。您应该能够使用 SubFoo 完成您可以使用 Foo 完成的所有操作,甚至更多。
假设我们有 Calico <: Cat <: Animal 和 Husky <: Dog <: Animal。我们来看一个Function[Cat, Dog]
。哪些陈述是正确的?那里有两个:
(1) 你可以传入任何 Cat(所以任何 Cat 的子类)
(2) 可以对返回值调用任意 Dog 方法
那么Function[Calico, Dog] <: Function[Cat, Dog]
有意义吗?不,对超类正确的陈述对子类不正确,即陈述(1)。您不能将任何 Cat 传递给只接受 Calico 猫的函数。
但是Function[Animal, Dog] <: Function[Cat, Dog]
有意义吗?是的,所有关于超类的陈述都适用于子类。我仍然可以传入任何 Cat —— 事实上,我可以做的还不止这些,我可以传入任何 Animal —— 我可以在返回值上调用所有 Dog 方法。
所以A <: B
暗示Function[B, _] <: Function[A, _]
现在,Function[Cat, Husky] <: Function[Cat, Dog]
有意义吗?是的,所有关于超类的陈述都适用于子类;我仍然可以传入一个 Cat,我仍然可以在返回值上调用所有 Dog 方法——事实上我可以做的甚至更多,我可以在返回值上调用所有 Husky 方法。
但是Function[Cat, Animal] <: Function[Cat, Dog]
有意义吗?不,对超类正确的陈述对子类不正确,即陈述(2)。我不能在返回值上调用 Dog 上可用的所有方法,只能调用 Animal 上可用的方法。
所以Function[Animal, Husky]
我可以用 a 做所有我能做的事情Function[Cat, Dog]
:我可以传入任何 Cat,并且可以在返回值上调用所有 Dog 方法。我可以做的更多:我可以传入其他动物,我可以调用 Husky 上可用而 Dog 上不可用的方法。所以这是有道理的:Function[Animal, Husky] <: Function[Cat, Dog]
。第一个类型参数可以替换为超类,第二个类型参数可以替换为子类。
这里有两个不同的想法在起作用。一种是使用子类型来允许将更具体的参数传递给函数(称为subsumption)。另一个是如何检查函数本身的子类型。
为了对函数的参数进行类型检查,您只需检查给定的参数是否是声明的参数类型的子类型。结果还必须是声明类型的子类型。这是您实际检查子类型的地方。
参数和结果的反/协方差仅在您想要检查给定函数类型是否是另一个函数类型的子类型时考虑。因此,如果参数具有 type Function[A1, ... ,B]
,则参数必须是函数类型Function[C1, ..., D]
whereA1 <: C1 ...
和D <: B
。
这种推理并不特定于 Scala,并且适用于其他具有子类型的静态类型语言。
简化的解释
class A
class B extends A
val printA: A => Unit = { a => println("Blah blah blah") }
printA(new A()) //"Blah blah blah"
printA(new B()) //"Blah blah blah"
逆变规则:
如果B
是 的子类型A
,则printA[A]
是 的子类型printA[B]
由于printA[B]
是超类,我们可以使用printA(new B())
协变意味着从更宽(超级)转换为更窄(子)。例如,我们有两个类:一个是动物(超级),另一个是猫,然后使用协变,我们可以将动物转换为猫。
反变体与协变体正好相反,协变体意味着猫对动物。
不变意味着它无法转换。