148

this question之后,有人可以在Scala中解释以下内容:

class Slot[+T] (var some: T) { 
   //  DOES NOT COMPILE 
   //  "COVARIANT parameter in CONTRAVARIANT position"

}

+T我理解类型声明中和之间的区别T(如果我使用它会编译T)。但是,如何在不诉诸于创建未参数化的事物的情况下,实际编写一个类型参数协变的?如何确保只能使用 的实例创建以下内容T

class Slot[+T] (var some: Object){    
  def get() = { some.asInstanceOf[T] }
}

编辑-现在将其归结为以下内容:

abstract class _Slot[+T, V <: T] (var some: V) {
    def getT() = { some }
}

这一切都很好,但我现在有两个类型参数,我只想要一个。我将重新提出这个问题:

如何编写一个类型协变的不可变 Slot类?

编辑2:呃!我用过var,没有val。以下是我想要的:

class Slot[+T] (val some: T) { 
}
4

4 回答 4

304

通常,协变类型参数是允许随着类被子类型化而向下变化的类型参数(或者,随着子类型而变化,因此使用“co-”前缀)。更具体地说:

trait List[+A]

List[Int]是 的子类型,List[AnyVal]因为Int是 的子类型AnyVal。这意味着您可以提供一个实例来说明List[Int]何时需要类型的值List[AnyVal]。对于泛型来说,这确实是一种非常直观的工作方式,但事实证明,在存在可变数据的情况下使用它是不合理的(破坏了类型系统)。这就是为什么泛型在 Java 中是不变的。使用 Java 数组(错误地协变)的不健全的简要示例:

Object[] arr = new Integer[1];
arr[0] = "Hello, there!";

我们只是将 type 的值分配给 typeString的数组Integer[]。出于显而易见的原因,这是个坏消息。Java 的类型系统实际上在编译时允许这样做。JVM 将“有帮助地”ArrayStoreException在运行时抛出一个。Scala 的类型系统防止了这个问题,因为类上的类型参数Array是不变的(声明是[A]而不是[+A])。

请注意,还有另一种称为逆变的方差。这非常重要,因为它解释了为什么协方差会导致一些问题。逆变实际上与协方差相反:参数随着子类型而向上变化。它不太常见,部分原因是它非常违反直觉,尽管它确实有一个非常重要的应用:函数。

trait Function1[-P, +R] {
  def apply(p: P): R
}

注意类型参数上的“ - ”方差注释。P这个声明作为一个整体意味着在 中Function1是逆变的,在中是P协变的R。因此,我们可以推导出以下公理:

T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']

注意T1'必须是 的子类型(或相同类型) ,而和T1则相反。在英语中,这可以被解读为:T2T2'

如果A的参数类型是B的参数类型的超类型,而A的返回类型是B的返回类型的子类型,则函数A是另一个函数B的子类型。

这条规则的原因留给读者作为练习(提示:考虑不同的情况,因为函数是子类型的,就像我上面的数组示例一样)。

有了你新发现的协变和逆变知识,你应该能够明白为什么下面的例子不能编译:

trait List[+A] {
  def cons(hd: A): List[A]
}

问题是它A是协变的,而cons函数期望它的类型参数是不变的。因此,A正在改变错误的方向。有趣的是,我们可以通过使Listin 逆变来解决这个问题A,但是返回类型List[A]将是无效的,因为cons函数期望它的返回类型是协变的。

我们这里仅有的两个选择是 a) 保持A不变,失去协方差的良好、直观的子类型属性,或者 b) 向cons定义A为下限的方法添加局部类型参数:

def cons[B >: A](v: B): List[B]

这现在是有效的。您可以想象它A向下变化,但能够B相对于它的下限向上变化。使用此方法声明,我们可以协变,一切顺利。AAA

请注意,这个技巧只有在我们返回一个List专门针对不太具体的类型的实例时才有效B。如果您尝试使其List可变,那么事情就会崩溃,因为您最终尝试将 type 的值分配给 typeB的变量A,这是编译器不允许的。每当您具有可变性时,您就需要某种类型的mutator,它需要某种类型的方法参数,这(与访问器一起)意味着不变性。协变适用于不可变数据,因为唯一可能的操作是访问器,它可以被赋予协变返回类型。

于 2009-03-23T16:27:32.433 回答
28

@Daniel 已经很好地解释了它。但简而言之,如果允许的话:

  class Slot[+T](var some: T) {
    def get: T = some   
  }

  val slot: Slot[Dog] = new Slot[Dog](new Dog)   
  val slot2: Slot[Animal] = slot  //because of co-variance 
  slot2.some = new Animal   //legal as some is a var
  slot.get ??

slot.get然后将在运行时抛出一个错误,因为它无法将 an 转换AnimalDog(duh!)。

一般来说,可变性不适用于协方差和逆变。这就是为什么所有 Java 集合都是不变的原因。

于 2013-12-17T07:56:43.533 回答
7

有关此问题的完整讨论,请参见Scala by example , page 57+。

如果我正确理解您的评论,您需要重新阅读从第 56 页底部开始的段落(基本上,我认为您所要求的如果没有运行时检查就不是类型安全的,而 scala 不会这样做,所以你运气不好)。翻译他们的示例以使用您的构造:

val x = new Slot[String]("test") // Make a slot
val y: Slot[Any] = x             // Ok, 'cause String is a subtype of Any
y.set(new Rational(1, 2))        // Works, but now x.get() will blow up 

如果您觉得我不理解您的问题(很可能),请尝试在问题描述中添加更多解释/上下文,我会再试一次。

回应您的编辑:不可变插槽是完全不同的情况......*微笑*我希望上面的示例有所帮助。

于 2009-03-19T18:00:58.607 回答
3

您需要对参数应用下限。我很难记住语法,但我认为它看起来像这样:

class Slot[+T, V <: T](var some: V) {
  //blah
}

Scala-by-example 有点难以理解,一些具体的例子会有所帮助。

于 2009-03-19T18:33:38.343 回答