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
}
...
}
然后我可以使用 key
从 ArgsProvider
的 args
中提取 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")
虽然人们会期望上面的代码 throw
和 Exception
;令我沮丧的是,它工作得很好并且 returns 以下输出(在 Scala
REPL
)
optLong: Option[Long] = Some(value-string-arg)
我的问题是:
- 为什么类型检查在这里失败?
- 通常在什么情况下 Scala 的类型检查会失败?
- 这个设计可以改进吗?
我正在使用
Scala 2.11.11
SBT 1.0.3
您在使用 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))
}
}
我的应用程序要求 参数提供程序 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
}
...
}
然后我可以使用 key
从 ArgsProvider
的 args
中提取 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")
虽然人们会期望上面的代码 throw
和 Exception
;令我沮丧的是,它工作得很好并且 returns 以下输出(在 Scala
REPL
)
optLong: Option[Long] = Some(value-string-arg)
我的问题是:
- 为什么类型检查在这里失败?
- 通常在什么情况下 Scala 的类型检查会失败?
- 这个设计可以改进吗?
我正在使用
Scala 2.11.11
SBT 1.0.3
您在使用 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))
}
}