Scala: who can explain this?

2020-06-07 08:22发布

问题:

Consider the following Scala code:

case class Data[T](value: Option[T]) {
  def get: T = try {
    doGet
  } catch {
    case e: Exception => throw new IllegalArgumentException
  }

  def doGet: T = value match {
    case Some(v) => v
    case None => ().asInstanceOf[T]
  }
}

Data[Unit](None).get
Data[Integer](None).get // which exception is thrown here?

[spoiler] It is a ClassCastException; who can explain why it is not caught and replaced by an IllegalArgumentException?

PS: To preempt any questions on why I would want to do this: this is a simplified version of some code that uses json4s to parse some string into an Option[T]; if the parsing fails None is returned, which is OK if T was Unit and not OK if T is some other type.

回答1:

Explanation

Exception isn't thrown here:

().asInstanceOf[T]

because this is an unchecked cast - JVM cannot verify if it is possible to cast () into T, because it has no information about T due to type erasure.

Instead, exception is thrown here

Data[Integer](None).get

because the result of get is cast into an Integer and that is something that JVM can verify. So, ClassCastException is actually thrown outside of get.

BTW, javac always warns about unchecked casts, I don't know why scalac doesn't.

Workaround

To some extent, it is possible to work around type erasure here using ClassTag and reflection-based casting:

import scala.reflect.{ClassTag, classTag}

case class Data[T: ClassTag](value: Option[T]) {
  def get: T = try {
    doGet
  } catch {
    case e: Exception => throw new IllegalArgumentException
  }

  def doGet: T = value match {
    case Some(v) => v
    case None => classTag[T].runtimeClass.asInstanceOf[Class[T]].cast(())
  }
}

Hackaround

For this use case, you can inspect the ClassTag directly:

scala> case class Data[T](value: Option[T])(implicit t: ClassTag[T]) {
     | def get: T = value getOrElse (t match {
     |   case ClassTag.Unit => ().asInstanceOf[T]
     |   case _ => throw new IllegalArgumentException
     | })
     | }
defined class Data

scala> Data[Unit](None)
res6: Data[Unit] = Data(None)

scala> .get

scala> Data[Int](None).get
java.lang.IllegalArgumentException