为什么该示例无法编译,即(同、对、内)方差如何工作?

发布于 2024-07-15 12:17:49 字数 1072 浏览 7 评论 0原文

这个问题之后,有人可以在 Scala 中解释以下内容:

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

}

我理解类型声明中 +TT 之间的区别(如果我使用 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 类,其类型协变?< /strong>

编辑 2:呃! 我使用了 var 而不是 val。 以下是我想要的:

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

Following on from this question, can someone explain the following in Scala:

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

}

I understand the distinction between +T and T in the type declaration (it compiles if I use T). But then how does one actually write a class which is covariant in its type parameter without resorting to creating the thing unparametrized? How can I ensure that the following can only be created with an instance of T?

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

EDIT - now got this down to the following:

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

this is all good, but I now have two type parameters, where I only want one. I'll re-ask the question thus:

How can I write an immutable Slot class which is covariant in its type?

EDIT 2: Duh! I used var and not val. The following is what I wanted:

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

少女情怀诗 2024-07-22 12:17:49

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

trait List[+A]

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

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

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

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

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

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

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

注意,T1' 必须是 T1 的子类型(或相同类型),而 T2 则相反T2'。 在英语中,这可以读作如下:

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

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

凭借您新发现的协变和逆变知识,您应该能够明白为什么以下示例无法编译:

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

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

这里我们唯一的两个选择是 a) 使 A 不变,失去协方差的良好、直观的子类型属性,或者 b) 将本地类型参数添加到 cons 中将 A 定义为下限的方法:

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

现在有效。 您可以想象 A 向下变化,但 B 能够相对于 A 向上变化,因为 A是它的下界。 通过这个方法声明,我们可以让 A 协变并且一切正常。

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

Generically, a covariant type parameter is one which is allowed to vary down as the class is subtyped (alternatively, vary with subtyping, hence the "co-" prefix). More concretely:

trait List[+A]

List[Int] is a subtype of List[AnyVal] because Int is a subtype of AnyVal. This means that you may provide an instance of List[Int] when a value of type List[AnyVal] is expected. This is really a very intuitive way for generics to work, but it turns out that it is unsound (breaks the type system) when used in the presence of mutable data. This is why generics are invariant in Java. Brief example of unsoundness using Java arrays (which are erroneously covariant):

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

We just assigned a value of type String to an array of type Integer[]. For reasons which should be obvious, this is bad news. Java's type system actually allows this at compile time. The JVM will "helpfully" throw an ArrayStoreException at runtime. Scala's type system prevents this problem because the type parameter on the Array class is invariant (declaration is [A] rather than [+A]).

Note that there is another type of variance known as contravariance. This is very important as it explains why covariance can cause some issues. Contravariance is literally the opposite of covariance: parameters vary upward with subtyping. It is a lot less common partially because it is so counter-intuitive, though it does have one very important application: functions.

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

Notice the "-" variance annotation on the P type parameter. This declaration as a whole means that Function1 is contravariant in P and covariant in R. Thus, we can derive the following axioms:

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

Notice that T1' must be a subtype (or the same type) of T1, whereas it is the opposite for T2 and T2'. In English, this can be read as the following:

A function A is a subtype of another function B if the parameter type of A is a supertype of the parameter type of B while the return type of A is a subtype of the return type of B.

The reason for this rule is left as an exercise to the reader (hint: think about different cases as functions are subtyped, like my array example from above).

With your new-found knowledge of co- and contravariance, you should be able to see why the following example will not compile:

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

The problem is that A is covariant, while the cons function expects its type parameter to be invariant. Thus, A is varying the wrong direction. Interestingly enough, we could solve this problem by making List contravariant in A, but then the return type List[A] would be invalid as the cons function expects its return type to be covariant.

Our only two options here are to a) make A invariant, losing the nice, intuitive sub-typing properties of covariance, or b) add a local type parameter to the cons method which defines A as a lower bound:

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

This is now valid. You can imagine that A is varying downward, but B is able to vary upward with respect to A since A is its lower-bound. With this method declaration, we can have A be covariant and everything works out.

Notice that this trick only works if we return an instance of List which is specialized on the less-specific type B. If you try to make List mutable, things break down since you end up trying to assign values of type B to a variable of type A, which is disallowed by the compiler. Whenever you have mutability, you need to have a mutator of some sort, which requires a method parameter of a certain type, which (together with the accessor) implies invariance. Covariance works with immutable data since the only possible operation is an accessor, which may be given a covariant return type.

装迷糊 2024-07-22 12:17:49

@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 将在运行时抛出错误,因为它未能成功地将 Animal 转换为 Dog< /代码>(呃!)。

一般来说,可变性与协变和逆变不太相配。 这就是所有 Java 集合都是不变的原因。

@Daniel has explained it very well. But to explain it in short, if it was allowed:

  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 will then throw an error at runtime as it was unsuccessful in converting an Animal to Dog (duh!).

In general mutability doesn't go well with co-variance and contra-variance. That is the reason why all Java collections are invariant.

你另情深 2024-07-22 12:17:49

请参阅 Scala 示例,第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 

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

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

See Scala by example, page 57+ for a full discussion of this.

If I'm understanding your comment correctly, you need to reread the passage starting at the bottom of page 56 (basically, what I think you are asking for isn't type-safe without run time checks, which scala doesn't do, so you're out of luck). Translating their example to use your construct:

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 

If you feel I'm not understanding your question (a distinct possibility), try adding more explanation / context to the problem description and I'll try again.

In response to your edit: Immutable slots are a whole different situation...* smile * I hope the example above helped.

苦行僧 2024-07-22 12:17:49

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

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

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

You need to apply a lower bound on the parameter. I'm having a hard time remembering the syntax, but I think it would look something like this:

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

The Scala-by-example is a bit hard to understand, a few concrete examples would have helped.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文