【问题标题】:Scala unexplainable program behaviorScala 无法解释的程序行为
【发布时间】:2017-10-30 16:58:31
【问题描述】:

对于以下代码:

object Test {

  class MapOps(map: Map[String, Any]) {
    def getValue[T](name: String): Option[T] = {
      map.get(name).map{_.asInstanceOf[T]}
    }
  }

  implicit def toMapOps(map: Map[String, Any]): MapOps = new MapOps(map)

  def main(args: Array[String]): Unit = {

    val m: Map[String, Any] = Map("1" -> 1, "2" -> "two")

    val a = m.getValue[Int]("2").get.toString
    println(s"1: $a")

    val b = m.getValue[Int]("2").get
    println(s"2: $b")
  }
}

val a 计算无异常,控制台打印1: two, 但是在计算val b 时,会抛出java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer

另外,如果我执行

val c = m.getValue[Int]("2").get.getClass.toString
println(s"1: $c")

控制台打印“int”。

有人能解释一下为什么这段代码会这样吗?

【问题讨论】:

标签: scala casting type-inference type-erasure


【解决方案1】:

这当然很奇怪。

如果您查看 Scala REPL 中的以下语句:

scala> val x = m.getValue[Int]("2")
x: Option[Int] = Some(two)

我认为正在发生的事情是这样的:asInstanceOf[T] 语句只是向编译器标记结果应该是Int,但不需要强制转换,因为该对象仍然只是通过指针引用。 (并且Int 值被装箱在Option/Some 内).toString 有效,因为每个对象都有一个.toString 方法,它只对值“二”进行操作以产生“二”。但是,当您尝试将结果分配给 Int 变量时,编译器会尝试将存储的整数拆箱,结果是一个强制转换异常,因为该值是 String 而不是装箱的 Int

让我们在 REPL 中逐步验证这一点:

$ scala
Welcome to Scala 2.12.1 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_151).
Type in expressions for evaluation. Or try :help.

scala> class MapOps(map: Map[String, Any]) {
     |     def getValue[T](name: String): Option[T] = {
     |       map.get(name).map{_.asInstanceOf[T]}
     |     }
     |   }
defined class MapOps

scala> import scala.language.implicitConversions
import scala.language.implicitConversions

scala> implicit def toMapOps(map: Map[String, Any]): MapOps = new MapOps(map)
toMapOps: (map: Map[String,Any])MapOps

scala> val a = m.getValue[Int]("2").get.toString
a: String = two

scala> println(s"1: $a")
1: two

到目前为止一切顺利。请注意,到目前为止还没有抛出异常,即使我们已经使用了.asInstanceOf[T] 并在结果值上使用了get。重要的是我们没有尝试对get 调用的结果(名义上是一个装箱的Int,实际上是String 值“二”)做任何事情,除了调用它的toString 方法。这行得通,因为String 值具有toString 方法。

现在让我们对Int 变量进行赋值:

scala> val b = m.getValue[Int]("2").get
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
  at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:101)
  ... 29 elided

现在我们得到了异常!还要注意堆栈跟踪中导致它的函数:unboxToInt - 它显然试图将存储在Some 中的值转换为Int,但它失败了,因为它不是装箱的Int,而是String.

问题的很大一部分是类型擦除。不要忘记 Some(Banana)Some(Bicycle) 在运行时都只是带有指向某个对象的指针的 Some 实例。 .asInstanceOf[T] 无法验证类型,因为该信息已被删除。但是,编译器能够根据您告诉它的内容来跟踪类型应该是什么,但它只能在其假设被证明是错误的情况下检测到错误。

最后,关于getClass 调用的结果。这有点像编译器的花招。它实际上并没有在对象上调用 getClass 函数,而是 - 因为它认为它正在处理一个原始的 Int - 它只是替换了一个 int 类实例。

scala> m.getValue[Int]("2").get.getClass
res0: Class[Int] = int

要验证对象实际上是String,您可以将其转换为Any,如下所示:

scala> m.getValue[Int]("2").get.asInstanceOf[Any].getClass
res1: Class[_] = class java.lang.String

关于get的返回值的进一步验证如下;请注意,当我们将此方法的结果分配给Any 类型的变量时没有异常(因此不需要强制转换),事实上,带有键“1”的有效Int 实际上存储在Any 下作为一个装箱的Int (java.lang.Integer),并且后一个值可以成功地拆箱为常规的Int 原语:

scala> val x: Any = m.getValue[Int]("2").get
x: Any = two

scala> x.getClass
res2: Class[_] = class java.lang.String

scala> val y: Any = m.getValue[Int]("1").get
y: Any = 1

scala> y.getClass
res3: Class[_] = class java.lang.Integer

scala> val z = m.getValue[Int]("1").get
z: Int = 1

scala> z.getClass
res4: Class[Int] = int

【讨论】:

  • 似乎不准确,请阅读问题下方评论中链接的问题
  • 我正在尝试在不同的上下文中重现该错误。我试过val c: Any = "two"; Some(c).map(_.asInstanceOf[Int].toString),但我仍然得到无效的强制转换异常。所以选项框是不够的。
  • @GabrielePetronella 我确实阅读了您链接到的问题。我的回答有什么不准确的地方?
  • 在打印a 后运行`println(m.getValue[Int]("2").map(_.getClass.getName))` 仍然会抛出异常。
  • @eje211 这是一个不同的问题。
【解决方案2】:

eje211's answer的最后部分展开。

您告诉编译器StringInt,现在您正在寻找对结果行为的合理解释。这是可以理解的,但它并不是真的有用。一旦你告诉编译器一个谎言,所有的赌注都没有了。您可以花时间调查编译器在何时何地插入检查以发现您的欺骗行为,但您最好花时间编写不会导致您撒谎的代码。

正如前面的答案所指出的,您可以通过使用模式匹配来做到这一点(避免意外撒谎)。在上述情况下,您需要ClassTag 来进行模式匹配,但最终结果将是类型安全且正确的代码。

【讨论】:

    【解决方案3】:

    这是一个Int,因为您在此行请求一个Int

    val b = m.getValue[Int]("2").get
    

    这调用了这个方法:

    def getValue[T](name: String): Option[T] = {
      map.get(name).map{_.asInstanceOf[T]}
    }
    

    并以这种方式应用它:

    def getValue[Int](name: String): Option[Int] = {
      map.get(name).map{_.asInstanceOf[Int]}
    }
    

    因此,如果您要求Int,您会得到Int

    对于"two",情况如下:

    "two".asInstanceOf[Int]
    

    这就是引发异常的原因。

    String 不是 Int。你不能这样投。你可以用这个方式投射它:

    "2".toInt
    

    但那是不同的。

    一般来说,使用asInstanceOf[] 是危险的。请尝试模式匹配。如果您必须使用,则由您来确保您尝试的强制转换是有效的。你基本上是在告诉编译器绕过它自己的类型检查,特别是当你从 Any 转换时。

    当您添加.toString 时它会起作用,因为那时您将类型改回字符串,这就是它最初的样子。纠正了数据类型的谎言。

    【讨论】:

    • 如果你请求Int,但你有一个字符串,你会得到一个运行时异常,而不是Int
    • 是的。我找到了错误发生的位置。不是什么原因造成的。
    • m.getValue[Int]("2").get.toString 的值为“二”,但 m.getValue[Int]("2").get 的值是一个例外,因为“二”不能转换为 Int。那么添加.toString 是如何工作的呢?此外,m.getValue[Int]("2") 返回为 Some("two")
    • 错误的演员阵容就是错误。由于泛型参数,它被推迟到调用 get
    • @GabrielePetronella 这并不完全正确。 get 在有效的语句中被调用,但没有抛出异常。为什么?只有当get 的结果分配给Int 变量b 时,编译器才知道需要取消装箱Int,才会导致强制转换异常。跨度>
    猜你喜欢
    • 2011-03-23
    • 1970-01-01
    • 2021-03-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多