【问题标题】:Why does var cause a type variance error where val compiles in Scala?为什么 var 在 Scala 中编译 val 时会导致类型差异错误?
【发布时间】:2016-01-03 13:07:08
【问题描述】:
scala> case class Data[+T](val value:T=null)
defined class Data

scala> val foo=Data[ArrayBuffer[Data[Any]]]()
foo: Data[scala.collection.mutable.ArrayBuffer[Data[Any]]] = Data(null)

scala> foo.value+=Data[String]()
java.lang.NullPointerException
  ... 33 elided

我想要一个 Data[String]、Data[ArrayBuffer[Data[Any]]] 或 Data[Map[String,Data[Any]]] 实例化的 Data 类。在上面的示例中,我尝试将其实例化为 Data[ArrayBuffer[Data[Any]]] 并将 Data[String] 添加到其数组缓冲区。当然我得到一个空指针异常,因为值是空的。但是这个例子的重点是它至少可以编译和运行。

现在,在 Data 构造函数中,我想根据初始 null 值的类型将值实例化为 Data[String]、ArrayBuffer[Data[Any]] 或 Map[String,Data[Any]]由 getClass 方法返回。但是为此,我需要将 value 设为 var,以便在检查其 null 值的类型后对其进行修改。

但是我得到了这个错误:

scala> case class Data[+T](var value:T=null)
<console>:11: error: covariant type T occurs in contravariant position in type T of value value_=
       case class Data[+T](var value:T=null)

【问题讨论】:

  • 您使用null 而非Option 的任何原因?
  • 不要在 Scala 中使用 null。它实际上只存在于与 Java 的互操作性。请改用Option。使案例类可变是不好的做法;保持不变。

标签: scala types variance


【解决方案1】:

使您的DataT 中保持不变。只需删除 +: Data[T] - 这应该可以编译。 更好的是,重新考虑您的设计以摆脱空值和可变变量 - 它们都有异味。

编辑:阅读您的 cmets 后,我更了解您要做什么。例如,考虑这样的事情作为选项之一。

sealed trait Node 
case class ListNode(list: Seq[Node]) extends Node
case class MapNode(map: Map[String, Node]) extends Node
case class LeafNode(data: String) extends Node

现在您可以使用类似的方式解析您的文档(这是“伪代码”,将其调整为您正在使用的任何 xml 解析库):

def parseNode(tag: XMLTag): Node = tag.tagType match {
   case LIST => 
      val subNodes = tag.getNestedTags.map(parseNode)
      ListNode(subNodes)
   case MAP => 
      val subNodes = tag.getNestedTags.map { tag => 
        tag.name -> parseNode(tag) 
      }
      MapNode(subNodes.toMap)
   case _ =>  
      LeafNode(tag.text)
}

【讨论】:

  • 它不能用 T 编译,看我的回答。
  • 当然可以:scala&gt; case class Data[T](var value:T=null) defined class Data
  • 我的意思是整个程序没有编译。如果我想以我在问题中描述的方式使用它,那么我需要 +T。否则,我无法将 Data[String] 添加到 ArrayBuffer[Data[Any]] 中。 arraybuffer 必须能够包含混合类型的数据,它的一些元素将是 Data[String],一些 Data[ArrayBuffer[Data[Any]] 等。
  • 哦,我明白了...这比我最初想象的更滥用语言:) 你是不是有 perl 编程背景?
  • 你怎么知道的?其实我之前用 Perl 写过代码。
【解决方案2】:

http://like-a-boss.net/2012/09/17/variance-in-scala.html#variance_and_type_safety

变体和类型安全

当使用 var 字段定义泛型类时,我们可能会遇到编译时错误:

scala> class Invariant[T](var t: T)
defined class Invariant

scala> class Covariant[+T](var t: T)
<console>:7: error: covariant type T occurs in contravariant position in type T of value t_=
      class Covariant[+T](var t: T)
            ^

scala> class Contravariant[-T](var t: T)
<console>:7: error: contravariant type T occurs in covariant position in type => T of method t
      class Contravariant[-T](var t: T)

让我们稍微分解一下。为什么编译器不允许在 Covariant 类中使用 getter?

scala> abstract trait Covariant[+T] {
    |   def take(t: T): Unit
    | }
<console>:8: error: covariant type T occurs in contravariant position in type T of value t
        def take(t: T): Unit
                  ^

scala> abstract trait Contravariant[-T] {
    |   def take(t: T): Unit
    | }
defined trait Contravariant

为什么?让我们考虑一下协方差的用法假设我们有一个类:

class Printer[+T] {
    |    def print(t: T): Unit = ???
    | }
<console>:8: error: covariant type T occurs in contravariant position in type T of value t
          def print(t: T): Unit = ???

如果 print 方法可以打印 Dogs 是否有意义(通常)它也应该打印 Animals?也许有时,但在一般意义上,如果我们想泛化 Printer 类,我们应该使用逆变。编译器足够聪明,可以为我们检查这种类型的用法。

让我们考虑第二个用例:返回一个泛型参数:

scala> class Create[-T] {
    |   def create: T = ???
    | }
<console>:8: error: contravariant type T occurs in covariant position in type => T of method create
        def create: T = ???

再一次 - Create 应该通过逆变来泛化是否有意义?如果 Create 返回 Animal 类的实例,我们是否应该能够在每个需要 Create[Dog] 的地方使用它? scala 编译器足够聪明,如果我们尝试一下,它就会在我们面前爆炸。

【讨论】:

  • 我怎样才能让我的例子工作呢?目前我有一个 Data 类,它有 var 字段 String、ArrayBuffer[Data] 和 Map[String,Data],我通过将其类型作为 String 参数(如 Data("String") 传递来构造它。这似乎不是太优雅了。
  • @sbtpr 既然有数据,为什么不构造Data 类呢?为什么需要var
  • 我可以这样做,但是我必须在构造时显式初始化值,这似乎有点过于冗长。看我的回答。
【解决方案3】:

我可以这样工作:

package data

case class Data[+T](val value:T)
{
    println(value.getClass)
}

这样我必须从构造函数显式初始化值。这没什么问题,只是我觉得它有点太冗长了。

import data.Data

import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.Map

object Run extends App
{

    val a=Data[ArrayBuffer[Data[Any]]](ArrayBuffer[Data[Any]]())

    a.value+=Data[String]("bar")

    println(a.value)

    val m=Data[Map[String,Data[Any]]](Map[String,Data[Any]]())

    m.value+=("foo"->Data[String]("bar"))

    println(m.value)

}

打印出来:

class scala.collection.mutable.ArrayBuffer
class java.lang.String
ArrayBuffer(Data(bar))
class scala.collection.mutable.HashMap
class java.lang.String
Map(foo -> Data(bar))

程序只编译带有+T类型参数的数据,否则我得到错误:

type mismatch;
[error]  found   : data.Data[String]
[error]  required: data.Data[Any]
[error] Note: String <: Any, but class Data is invariant in type T.
[error] You may wish to define T as +T instead. (SLS 4.5)
[error]         a.value+=Data[String]("bar")

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-03
    • 1970-01-01
    • 2018-04-08
    • 2011-06-09
    • 1970-01-01
    • 2012-10-05
    • 2017-04-08
    • 2021-12-29
    相关资源
    最近更新 更多