scala - 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"
解决方案
只是为了展示如何避免使用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。
我建议您看一下该模块,如果您还有其他问题,请直接提出一个新问题,标记pureconfig和yaml。此外,如果只是一个小疑问,您可以尝试在gitter 频道中询问。
推荐阅读
- r - 确定一天中的最后一次和第一次测量
- python - 在 python 上绘制 PSD(功率谱密度)时需要帮助
- c# - 具有组织/Office 365/Microsoft 帐户身份验证的 Blazor(服务器端);如何做本地多个角色?
- django - 我在 Django 的 Gunicorn 的 Procfile 中放了什么?
- nexus - Flutter pub_hosted_url 镜像 sonatype nexus
- javascript - Javascript querySelectorAll 具有特定类的父元素的元素
- html - 从子导航菜单中删除边距
- python - 有没有办法用多个字符替换空格?
- swift - 如何在 pi 图中从中心到自定义角度画一条线?我正在使用 danielgindi/图表
- pandas - 执行 groupby.median() 时如何保存分类列?