【问题标题】:Covariance and Contravariance in Scala [duplicate]Scala中的协变和逆变[重复]
【发布时间】:2020-11-11 03:55:49
【问题描述】:

我在理解协方差类型受方法参数限制时感到困惑。我阅读了许多材料,但无法理解以下概念。

class SomeThing[+T] {
   def method(a:T) = {...}   <-- produces error
}

在上面这段代码中,a 是类型 T。为什么我们不能传递 T 的子类型?方法对参数 x 的所有期望,都可以通过 T 的子类型完美满足。

同样,当我们有逆变类型 T (-T) 时,它不能作为方法参数传递;但这是允许的。为什么我认为它不能通过是:例如,说 method 调用方法(存在于对象 a 中) 在 T 中存在的 a 上。当我们传递 T 的超类型时,它可能不存在。但它是编译器允许的。这让我很困惑。

class SomeThing[-T] {
   def method(a:T) = {...}   <-- allowed
}

因此,通过查看上述内容,在方法参数和返回类型中都应该允许协变。不能应用逆变。

谁能帮我理解一下。

【问题讨论】:

标签: scala covariant contravariant


【解决方案1】:

关于方差的关键在于它会影响类的外观从外部

协方差表示SomeThing[Int] 的实例可以被视为SomeThing[AnyVal] 的实例,因为AnyValInt 的超类。

在这种情况下你的方法

def method(a: Int)

会变成

def method(a: AnyVal)

这显然是个问题,因为您现在可以将 Double 传递给应该只接受 Int 值的 SomeThing[Int] 方法。请记住,实际对象不会改变,只会改变类型系统感知它的方式。

逆变表示SomeThing[AnyVal]可以被视为SomeThing[Int]所以

def method(a: AnyVal)

变成

def method(a: Int)

这没关系,因为您始终可以在需要 AnyVal 的地方传递 Int

如果您遵循返回类型的逻辑,您会发现它的工作方式相反。返回协变类型是可以的,因为它们总是可以被视为超类类型。您不能返回逆变类型,因为返回类型可能是实际类型的子类型,这是无法保证的。

【讨论】:

  • ` 类 A 类 B 扩展 A 类 C 扩展 B 抽象类 Box[-T] { def set(x : T) :Unit } val x = new Box[B] { def set(b :B) = 控制台 println "B" } val y = new Box[A] { def set(a:A) = Console println "A" } val z = new Box[C] { def set(c:C) =控制台 println "C" } x.set(new A)
  • 嗨,蒂姆,感谢您宝贵的时间,对于我的延迟回复,我深表歉意。我现在似乎有了一个想法..你能不能看看上面的代码..根据逆变的定义(超类实例将被接受),我的最后一个陈述应该被接受;但它会抛出类型错误。
  • 但同时,如果我给 x.set(new C),它是被接受的,这意味着我们基本上只提供子类型。
  • 由于此问题已结束,因此最好将其作为一个新问题提出,而不是尝试扩展此问题。
  • 嗨@Tim,很好的解释,我也有同样的疑问。您能否用一个示例详细说明您所做的声明“如果您遵循返回类型的逻辑,您将看到它反过来工作”,或者请指出一些可以消除对返回类型的怀疑的资源。提前致谢。
【解决方案2】:

我认为你是在反击问题。如果 T 是协变的,则不能将 a:T 作为方法的参数这一事实是一个约束,因为否则一些不合逻辑的代码将完全有效

class A
class B extends A
class C extends B

val myBThing = new SomeThing[B]

这里,myBThing.method 接受B,你说得对,我们可以传递任何扩展B 的东西,所以myBThing.method(new C) 完全没问题。但是,myBThing.method(new A) 不是!

现在,既然我们用协变定义了SomeThing,我也可以这样写

val myAThing: SomeThing[A] = myBThing // Valid since B <: A entails SomeThing[B] <: Something[A] by definition of covariance
myAThing.method(new A) // What? You're managing to send an A to a method that was implemented to receives B and subtypes!

您现在可以看到为什么我们施加不将T 作为参数传递的约束(参数处于“逆变位置”)。

我们可以对返回位置的逆变进行类似的论证。请记住,逆变意味着B &lt;: A 需要 ``SomeThing[A] <: something>

假设您正在定义以下内容

class A
class B extends A
class SomeThingA[-T](val value: T) // Compiler won't like T in a return type like myThing.value

// If the class definition compiled, we could write
val myThingA: SomeThing[A] = new SomeThing(new A)
val someA: A = myThingA.value
val myThingB: SomeThing[B] = myThingA // Valid because T contravariant
val someB: B = myThingB.value // What? I only ever stored an A!

更多详情请见this answer

【讨论】:

  • 您好,francoisr,感谢您的回复,并为延误道歉。我在第一个答案中评论了另一个问题。
【解决方案3】:

对于class SomeThing[T],在T 之前放置+- 实际上对类本身的影响比对类型参数的影响更大。

考虑以下几点:

val instanceA = new SomeThing[A]
val instanceB = new SomeThing[B]

如果SomeThingT 上是不变的(不是+-),那么实例将没有方差关系。

如果SomeThingT ([+T]) 上是协变的,那么实例将具有与AB 相同的方差关系。换句话说,如果AB 的子类型(反之亦然),那么实例将反映相同的关系。

如果SomeThingT ([-T]) 上是逆变的,那么实例将具有与AB 具有相反的方差关系。换句话说,如果AB 的子类型,那么instanceB 将是instanceA 的子类型。

但是方差指示符确实会影响类型参数的使用方式。如果T 标记为+,则不能将其置于逆变位置,同样,如果标记为-,则不能将其置于协变位置。我们在定义方法时经常遇到这种情况。

Scala 方法与 Scala 函数特征密切相关:Function0Function1Function2 等。

考虑Function1的定义:

trait Function1[-T1, +R] extends AnyRef

现在假设您要传递这种类型的函数。

def useThisFunc(f: A => B):Unit = {...}

因为Function1 在其接收参数上是逆变的并且在其结果上是协变的,所以以下所有参数都可以作为useThisFunc() 参数接受。

val a2b       : A => B             = ???
val supa2b    : SuperOfA => B      = ???
val a2subb    : A => SubOfB        = ???
val supa2subb : SuperOfA => SubOfB = ???

因此,总而言之,如果SomeThingT 上是协变的,那么您不能将T 作为成员方法的传递参数,因为FunctionX 在其参数类型上是逆变的。同样,如果 SomeThingT 上是逆变的,则您不能将 T 作为成员方法返回类型,因为 FunctionX 在其返回类型上是协变的。

【讨论】:

  • 您好 jwvh,感谢您宝贵的时间提供答案。我为我的延误道歉。
猜你喜欢
  • 2015-02-09
  • 1970-01-01
  • 2013-11-29
  • 2013-07-11
  • 2011-04-06
  • 2020-01-25
  • 1970-01-01
  • 1970-01-01
  • 2011-10-06
相关资源
最近更新 更多