【问题标题】:Scala ClassCastException on Option.orNullOption.orNull 上的 Scala ClassCastException
【发布时间】:2019-05-30 02:37:32
【问题描述】:

当我尝试运行以下代码时:

  def config[T](key: String): Option[T] = {
    //in reality this is a map of various instance types as values
    Some("string".asInstanceOf[T])
  }
  config("path").orNull

我收到错误:

java.lang.String 不能转换为 scala.runtime.Null$ java.lang.ClassCastException

以下尝试工作正常:

config[String]("path").orNull
config("path").getOrElse("")

由于getOrElse 工作令人困惑,为什么 null 如此特殊并引发错误。 orNull 有没有办法在不指定泛型类型的情况下工作?

scalaVersion := "2.12.8"

【问题讨论】:

  • 问题不是orNull,而是你的asInstanceOf。基本上,由于您没有指定 type 参数 T,它会从上下文中推断出来。在这种情况下,它只能推断出Null,因为这是orNull 的输出。然后它尝试执行s.asInstanceOf[Null],其中sString,这是一个错误。 - getOrElse 有效,因为它推断输出类型应该是 String。 - 作为旁注,我希望您使用orNull 只是为了与Java 互操作,因为nullScalar 中不是惯用的。其次,尽量避免asInstanceOf没有人比编译器更聪明。
  • 嗨米格尔,谢谢你的解释。我正在使用此代码获取存储在 Map 中的配置,这些值具有各种类型(String、Int、List、Map ...... - 从 yaml 文件中解析),我需要以某种方式说服编译器该类型是正确的- 因此使用asInstanceOf,有什么更好的方法?
  • 一个 ADT 表示值的类型,一个 typeclasstype-safe 方式获取值, 并隐含地实现它。但是,这可能是一种 “高级” 技术,如果您刚刚开始使用 Scala,请记住这些概念以备将来使用,我将在几个小时,所以你可以看到它。 - 现在,您可以离开asInstanceOf,但是您已经看到了问题所在,它非常脆弱。您至少可以这样做Try(value.asInstanceOf[T]).toOption 以使其更安全。
  • @TomasBartalos re。什么是更好的方法:config[String]("path") 将是您的情况。您必须向编译器指示期望的类型。 val foo: String = config("path").orNull 之类的东西也应该可以。另外......不要使用空值。
  • 嗨 Dima,是的,两种方法都可以正常工作。它是如此脆弱,以至于它不是编译时错误,甚至在值为空时也可以工作。因此,在我的情况下,更改配置会导致意外的运行时错误。我需要将空值传递给 3-rd 方 java lib,否则我知道空值是邪恶的......

标签: scala


【解决方案1】:

只是为了说明如何避免使用 asInstanceOf 从类型化配置中获取值。

sealed trait Value extends Product with Serializable
final case class IntValue(value: Int) extends Value
final case class StringValue(value: String) extends Value
final case class BooleanValue(value: Boolean) extends Value

type Config = Map[String, Value]

sealed trait ValueExtractor[T] {
  def extract(config: Config)(fieldName: String): Option[T]
}

object ValueExtractor {
  implicit final val IntExtractor: ValueExtractor[Int] =
    new ValueExtractor[Int] {
      override def extract(config: Config)(fieldName: String): Option[Int] =
        config.get(fieldName).collect {
          case IntValue(value) => value
        }
    }

  implicit final val StringExtractor: ValueExtractor[String] =
    new ValueExtractor[String] {
      override def extract(config: Config)(fieldName: String): Option[String] =
        config.get(fieldName).collect {
          case StringValue(value) => value
        }
    }

  implicit final val BooleanExtractor: ValueExtractor[Boolean] =
    new ValueExtractor[Boolean] {
      override def extract(config: Config)(fieldName: String): Option[Boolean] =
        config.get(fieldName).collect {
          case BooleanValue(value) => value
        }
    }
}

implicit class ConfigOps(val config: Config) extends AnyVal {
  def getAs[T](fieldName: String)(default: => T)
              (implicit extractor: ValueExtractor[T]): T =
    extractor.extract(config)(fieldName).getOrElse(default)
}

那么,你就可以这样使用了。

val config = Map("a" -> IntValue(10), "b" -> StringValue("Hey"), "d" -> BooleanValue(true))

config.getAs[Int](fieldName = "a")(default = 0) // res: Int = 10
config.getAs[Int](fieldName = "b")(default = 0) // res: Int = 0
config.getAs[Boolean](fieldName = "c")(default = false) // res: Boolean = false

现在,问题变成了如何从原始源创建类型化配置。
更好的是,如何将配置直接映射到 案例类

但是,这些更复杂,最好只使用已经完成的东西,例如pureconfig


作为一个学术练习,让我们看看我们是否可以支持ListsMaps

让我们从列表开始,一种天真的方法是为列表值创建另一个案例类,并为每种列表创建一个提取器工厂(这个过程正式称为隐式派生) em>。

import scala.reflect.ClassTag

final case class ListValue[T](value: List[T]) extends Value

...

// Note that, it has to be a def, since it is not only one implicit.
// But, rather a factory of implicits.
// Also note that, it needs another implicit parameter to construct the specific implicit.
// In this case, it needs a ClasTag for the inner type of the list to extract.
implicit final def listExtractor[T: ClassTag]: ValueExtractor[List[T]] =
  new ValueExtractor[List[T]] {
    override def extract(config: Config)(fieldName: String): Option[List[T]] =
      config.get(fieldName).collect {
        case ListValue(value) => value.collect {
          // This works as a safe caster, which will remove all value that couldn't been casted.
          case t: T => t
        }
      }
  }

现在,你可以这样使用了。

val config = Map("l" ->ListValue(List(1, 2, 3)))

config.getAs[List[Int]](fieldName = "l")(default = List.empty)
// res: List[Int] = List(1, 2, 3)
config.getAs[List[String]](fieldName = "l")(default = List("Hey"))
// res: String = List() - The default is not used, since the field is a List...
// whose no element could be casted to String.

但是,如果您需要其他泛型类型的列表,例如列表列表,则此方法仅限于普通类型。那么,这将不起作用。

val config = Map("l" ->ListValue(List(List(1, 2), List(3))))

val l = config.getAs[List[List[String]]](fieldName = "l")(default = List.empty)
// l: List[List[String]] = List(List(1, 2), List(3)) ???!!!
l.head
// res: List[String] = List(1, 2)
l.head.head
// java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

这里的问题是类型擦除,ClassTags不能解决,你可以尝试使用TypeTags,它可以保留完整的类型,但是解决起来比较麻烦。
对于 Maps,解决方案非常相似,尤其是如果您将密钥类型固定为 String(假设您真正想要的是嵌套配置)。但是,这篇文章现在太长了,所以我把它留给读者练习。


尽管如此,正如已经说过的,这很容易被破坏,并且不是完全健壮的。
有更好的方法,但我自己对这些(还)不是很熟练,即使我会,答案也会更长,根本没有必要。

幸运的是,即使 pureconfig 不直接支持 YAML,也有一个 module 支持,pureconfig-yaml
我建议您看一下该模块,如果您还有其他问题,请直接提出一个新问题,标记 pureconfigyaml。另外,如果只是小问题,可以尝试在gitter channel提问。

【讨论】:

  • 我刚刚看到您的评论,您说您需要底层 Java 库的空值。在这种情况下,您可以从我的示例中删除 default 部分并改用 orNull
  • 感谢您提供如此详尽的回答。我尝试采用您的示例,但在使用通用集合(列表、地图)时遇到问题。显然,我无法命名 Map 可以具有的所有可能类型,有什么技巧可以说这是具有任何泛型类型的 Map 的 ValueExtractor(Map[ _, _] 没有帮助)?建议一个 lib 确实是最好的办法,但是我们使用的是 yaml 配置文件,而 pureconfig 似乎不支持这些(我目前正在使用snakeyaml java lib)。
  • @TomasBartalos 我刚刚编辑了回答您的新问题的答案。我希望你会发现最后一段让人松了一口气。
  • 感谢 Miguel 的所有建议,也许我今天不会使用它们,也许它们与最初的问题有些不同,但它们绝对非常有用。我将使用 pureconfig-yaml 稍作改动 - 覆盖系统属性中的原语(对 docker 容器很有用)。我已经准备好代码,而且效果很好。
猜你喜欢
  • 2017-09-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多