Scala 类型检查的奇怪失败

Weird failure of Scala's type-check

我的应用程序要求 参数提供程序 trait 可以添加到任何 class 以允许传递 任意数字any 类型 的参数。

trait Arg
case class NamedArg(key: String, value: Any) extends Arg

// I extend my classes with this trait
trait ArgsProvider {
  val args: Seq[Arg]

  lazy val namedArgs: Map[String, Any] = {
    args.filter(_.isInstanceOf[NamedArg]).
      map(_.asInstanceOf[NamedArg]).
      map(arg => arg.key -> arg.value).toMap
  }

  ...
}

然后我可以使用 keyArgsProviderargs 中提取 NamedArg,如下所示

trait ArgsProvider {
  ...

  /*
   * Method that takes in a [T: ClassTag] and a (key: String) argument
   * (i) if key exists in namedArgs: Map[String, Any]
   *     - Returns Some(value: T) if value can be casted into T type
   *     - Throws Exception if value can't be casted into T type
   * (ii) if key doesn't exist in namedArgs
   *    Returns None
   */
  def getOptionalTypedArg[T: ClassTag](key: String): Option[T] = {
    namedArgs.get(key).map { arg: Any =>
      try {
        arg.asInstanceOf[T]
      } catch {
        case _: Throwable => throw new Exception(key)
      }
    }
  }
  ...
}

尽管看起来[=44​​=]非常不直观并且冗长,但此设计完美适用于我。但是最近在写某unit tests的时候,发现了其中的一个大漏洞无法执行type-checking。 (或者至少我是这么推断的)


更具体地说,当我尝试将提供的 arg type-cast 错误地 时,它 不会抛出任何异常 输入。例如:

// here (args: Seq[NamedArg]) overrides the (args: Seq[Arg]) member of ArgsProvider
case class DummyArgsProvider(args: Seq[NamedArg]) extends ArgsProvider

// instantiate a new DummyArgsProvider with a single NamedArg having a String payload (value)
val dummyArgsProvider: DummyArgsProvider = DummyArgsProvider(Seq(
    NamedArg("key-string-arg", "value-string-arg")
))

// try to read the String-valued argument as Long
val optLong: Option[Long] = dummyArgsProvider.getOptionalTypedArg[Long]("key-string-arg")

虽然人们会期望上面的代码 throwException;令我沮丧的是,它工作得很好并且 returns 以下输出(在 Scala REPL

optLong: Option[Long] = Some(value-string-arg)


我的问题是:


我正在使用

您在使用 type-erasure 时遇到问题:Option[Long] 实际上存储字符串 "value-string-arg" 并且不关心其已被删除的类型。

但是,如果您这样做 optLong.get,它会尝试将其转换为 Long,这是预期的输出。你会得到 ClassCastException


一点评论:

替换

val namedArgs: Map[String, Any] = {...}

来自

val namedArgs: Map[String, Any] = args.collect{
  case NameArg(k, v) => k -> v
}(collection.breakout)

此外,在您的 getOptionalTypedArg 中,不要捕获所有 Throwable。这是不好的做法(您可能会捕获 OutOfMemoryError 和其他致命错误,而您不应该这样做)。在您的情况下,您想要捕获 ClassCastException。在其他情况下,您不确切知道哪个 Throwable,请尝试使用

正如@AlexeyRomanov 所说,as/isInstanceOf[T] 不要使用 ClassTag

您可以改用模式匹配,它会检查 ClassTag,如果有的话:

trait ArgsProvider {
  /* ... */

  def getOptionalTypedArg[T: ClassTag](key: String): Option[T] = {
    namedArgs.get(key).map {
      case arg: T => arg
      case _ => throw new Exception(key)
    }
  }
}

或者直接使用ClassTag的方法:

import scala.reflect.classTag

def getOptionalTypedArg[T: ClassTag](key: String): Option[T] = {
  namedArgs.get(key).map { arg =>
    classTag[T].unapply(arg).getOrElse(throw new Exception(key))
  }
}