Scala 无法解释的程序行为
Scala unexplainable program behavior
对于以下代码:
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"。
有人可以解释为什么这段代码会这样吗?
它是一个 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]
这就是引发异常的原因。
A String
不是 Int。你不能那样投。你可以用这种方式来施放它:
"2".toInt
但那不一样。
一般来说,使用asInstanceOf[]
是危险的。尝试模式匹配。如果您必须 使用,则由您来确保您尝试的转换是有效的。您基本上是在告诉编译器绕过它自己的类型检查,尤其是当您从 Any
.
进行转换时
它在您添加 .toString
时起作用,因为那时您将类型改回字符串,这就是它最初的真实情况。数据是什么类型的谎言已被纠正。
这当然很奇怪。
如果您查看 Scala REPL 中的以下语句:
scala> val x = m.getValue[Int]("2")
x: Option[Int] = Some(two)
我认为发生的事情是这样的:asInstanceOf[T]
语句只是向编译器标记结果应该是 Int
,但不需要强制转换,因为对象仍然只是通过指针引用。 (并且 Int
值装在 Option
/Some
内).toString
之所以有效,是因为每个对象都有一个 .toString
方法,它只对值 "two" 产生 "two"。但是,当您尝试将结果分配给 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
值 "two")除了调用它的 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
class 实例.
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
的return值;请注意,当我们将此方法的结果分配给类型为 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
扩展 的最后一部分。
您告诉编译器 String
是 Int
,现在您正在寻找对结果行为的合理解释。这是可以理解的,但它并没有真正的用处。一旦你告诉编译器一个谎言,所有的赌注都会落空。您可以花时间准确地调查编译器何时何地插入检查以发现您的欺骗行为,但您的时间最好花在编写不会导致您说谎的代码上。
正如前面的答案所指出的,您可以通过使用模式匹配来做到这一点(避免意外说谎)。在上述情况下,您需要 ClassTag
来使模式匹配工作,但最终结果将是类型安全且正确的代码。
对于以下代码:
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"。
有人可以解释为什么这段代码会这样吗?
它是一个 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]
这就是引发异常的原因。
A String
不是 Int。你不能那样投。你可以用这种方式来施放它:
"2".toInt
但那不一样。
一般来说,使用asInstanceOf[]
是危险的。尝试模式匹配。如果您必须 使用,则由您来确保您尝试的转换是有效的。您基本上是在告诉编译器绕过它自己的类型检查,尤其是当您从 Any
.
它在您添加 .toString
时起作用,因为那时您将类型改回字符串,这就是它最初的真实情况。数据是什么类型的谎言已被纠正。
这当然很奇怪。
如果您查看 Scala REPL 中的以下语句:
scala> val x = m.getValue[Int]("2")
x: Option[Int] = Some(two)
我认为发生的事情是这样的:asInstanceOf[T]
语句只是向编译器标记结果应该是 Int
,但不需要强制转换,因为对象仍然只是通过指针引用。 (并且 Int
值装在 Option
/Some
内).toString
之所以有效,是因为每个对象都有一个 .toString
方法,它只对值 "two" 产生 "two"。但是,当您尝试将结果分配给 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
值 "two")除了调用它的 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
class 实例.
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
的return值;请注意,当我们将此方法的结果分配给类型为 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
扩展
您告诉编译器 String
是 Int
,现在您正在寻找对结果行为的合理解释。这是可以理解的,但它并没有真正的用处。一旦你告诉编译器一个谎言,所有的赌注都会落空。您可以花时间准确地调查编译器何时何地插入检查以发现您的欺骗行为,但您的时间最好花在编写不会导致您说谎的代码上。
正如前面的答案所指出的,您可以通过使用模式匹配来做到这一点(避免意外说谎)。在上述情况下,您需要 ClassTag
来使模式匹配工作,但最终结果将是类型安全且正确的代码。