首页 > 解决方案 > Option.orNull 上的 Scala ClassCastException

问题描述

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

  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"

标签: scala

解决方案


只是为了展示如何避免使用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


就像一个学术练习,让我们看看我们是否可以支持Lists& Maps

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

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,也有一个模块可以支持,pureconfig-yaml
我建议您看一下该模块,如果您还有其他问题,请直接提出一个新问题,标记pureconfigyaml。此外,如果只是一个小疑问,您可以尝试在gitter 频道中询问。


推荐阅读