io.circe.Decoder 的奇怪 NPE

Strange NPE with io.circe.Decoder

我声明了2个变量,

 implicit val decodeURL: Decoder[URL] = Decoder.decodeString.emapTry(s => Try(new URL(s)))     // #1

 implicit val decodeCompleted = Decoder[List[URL]].prepare(_.downField("completed")) // #2

两行编译 运行。

但是,如果我用类型注释 #2,即 implicit val decodeCompleted: Decoder[List[URL]] = Decoder[List[URL]].prepare(_.downField("completed"))。它编译并且#2 将在 运行 时间内抛出 NullPointerException (NPE)。

怎么会这样?我不知道这是 Circe 还是普通的 Scala 问题。为什么#2 与#1 不同?谢谢

问题是您应该始终使用带注释的隐式。

现在,当您在未注释的情况下使用它们时,您会进入某种 undefined/invalid 行为区。这就是为什么在未注释的情况下会发生这样的事情:

  • 我想用Decoder[List[URL]]
  • 范围内没有(注释)Decoder[List[URL]]隐含
  • 让我们正常推导它(不需要 generic.auto._ 因为它的定义在伴随对象中)
  • 派生后调用 .prepare(_.downField("completed"))
  • 最终结果的类型是Decoder[List[URL]],所以推断的类型是decodeCompleted

现在,如果你注释会发生什么?

  • 我想用Decoder[List[URL]]
  • decodeCompleted 声明为满足该定义的东西
  • 使用decodeCompleted
  • 但是 decodeCompleted 没有初始化!事实上我们现在正在初始化它!
  • 结果你得到 decodeCompleted = null

这实际上等于:

val decodeCompleted = decodeCompleted

除了间接层妨碍了编译器发现其荒谬之处。 (如果您将 val 替换为 def,您最终会遇到无限递归和堆栈溢出):

@ implicit val s: String = implicitly[String] 
s: String = null

@ implicit def s: String = implicitly[String] 
defined function s

@ s 
java.lang.WhosebugError
  ammonite.$sess.cmd1$.s(cmd1.sc:1)
  ammonite.$sess.cmd1$.s(cmd1.sc:1)
  ammonite.$sess.cmd1$.s(cmd1.sc:1)
  ammonite.$sess.cmd1$.s(cmd1.sc:1)

是的,它被编译器搞砸了。你没有做错,在一个完美的世界里它会起作用。

Scala 社区通过区分来缓解这种情况:

  • 自动派生 - 当你需要在某处隐含时它会自动派生而无需你为它定义变量
  • 半自动推导 - 当您推导一个值,并使该值隐式时

在后一种情况下,您通常会有一些实用程序,例如:

import io.circe.generic.semiauto._

implicit val decodeCompleted: Decoder[List[URL]] = deriveDecoder[List[URL]]

之所以有效,是因为它采用 DerivedDecoder[A] 隐式,然后从中提取 Decoder[A],因此您永远不会遇到 implicit val a: A = implicitly[A] 场景。

确实问题是你引入了一个递归 val,就像@MateuszKubuszok 解释的那样。

最直接——虽然有点难看——的解决方法是:

implicit val decodeCompleted: Decoder[List[URL]] = {
  val decodeCompleted = null
  Decoder[List[URL]].prepare(_.downField("completed"))
}

通过在 right-hand 侧隐藏 decodeCompleted,隐式搜索将不再将其视为该代码块内的候选者,因为它不再被引用。