隐式转换和null

8
以下代码:
import scala.language.implicitConversions

object myObj {
  implicit def nullToInt(x: Null) = 0

  def main(args: Array[String]): Unit = {
    val x = 1 + null
    val y = 1 + nullToInt(null)

    println(x + " " + y)
  }
}

以下是给出的结果。
1null 1

我原本期望两个值都是整数且等于1。

显然第一个值是字符串,等于"1null"。

Xprint:typer 显示源代码被翻译成了:

package <empty> {
  import scala.language.implicitConversions;
  object myObj extends scala.AnyRef {
    def <init>(): myObj.type = {
      myObj.super.<init>();
      ()
    };
    implicit def nullToInt(x: Null): Int = 0;
    def main(args: Array[String]): Unit = {
      val x: String = 1.+(null);
      val y: Int = 1.+(myObj.this.nullToInt(null));
      scala.Predef.println(x.+(" ").+(y))
    }
  }
}

对于 int 类型,没有接受 null 值的符号方法。

scala> 10+null
res0: String = 10null

scala> 10*null
<console>:12: error: overloaded method value * with alternatives:
  (x: Double)Double <and>
  (x: Float)Float <and>
  (x: Long)Long <and>
  (x: Int)Int <and>
  (x: Char)Int <and>
  (x: Short)Int <and>
  (x: Byte)Int
 cannot be applied to (Null)
       10*null
         ^

scala> 10-null
<console>:12: error: overloaded method value - with alternatives:
  (x: Double)Double <and>
  (x: Float)Float <and>
  (x: Long)Long <and>
  (x: Int)Int <and>
  (x: Char)Int <and>
  (x: Short)Int <and>
  (x: Byte)Int
 cannot be applied to (Null)
       10-null
         ^

我假设"1"和"null"都被转换为字符串而不是使用隐式的nullToInt。有人可以解释一下编译器是如何得出这个结果的吗?使用了什么逻辑或工作流程?
另外一个问题是是否有方法启用隐式的nullToInt?
PS. 我不是在讨论最佳实践,这个问题可以认为是学术兴趣。

1
这是一个有趣的事实,可以与任何其他方法(这里是 +++)的奇怪行为形成对比,但不是答案,也不是改进问题的建议。代码如下:import language.implicitConversions; class I { def +++(i: I) = "I +++ I" };class S { def +++(a: Any) = "S +++ Any" }; class N; implicit def n2i(n: N): I = new I; implicit def i2s(i: I): S = new S; println((new I) +++ (new N)); 这将 N 转换为 I 并调用 I+++,而不是 S+++。我不确定为什么使用 + 时会产生相反的结果。 - Andrey Tyukin
2个回答

3
所以,@AndreyTyukin所说的是正确的,从机械上来看,我认为有更多的东西。有两件事情正在发生,原因如下。
  1. Any被装饰成Predef中的implicit,请参见以下内容:

    implicit final class any2stringadd[A] extends AnyVal

正如您所看到的,any2stringadd就是负责+的,您可以在此处看到签名:

def +(other: String): String
更正:并没有隐式转换,事实比这更简单。
  1. 查看Predefany2stringadd的源代码,可以看到以下内容:
implicit final class any2stringadd[A](private val self: A) extends AnyVal { def +(other: String): String = String.valueOf(self) + other } 在Java中,String.valueOf返回的是一个字符串类型的数字。如果将1null相加,则会得到1null的结果(在jshell中验证)。
jshell> "1" + null
$1 ==> "1null"

1
implicitly[Null => String]其实就是implicitly[Null <:< String]的同一个对象,这个对象存在是因为,正如我所说的,Null是所有AnyRef类型的子类型,特别是是String的子类型。我有些惊讶地发现了any2stringadd,我原以为它只是一点文档上的小错误而已,而方法实际上是在Any上,但事实上,确实存在这种转换。 - Andrey Tyukin
另外,我不理解最后一段关于augmentString等的内容 - 它应该演示什么?在REPL中,表达式new StringOps(null)会崩溃并显示NullPointerException。尽管有any2stringadd,但我仍然不明白为什么它被应用于整数1而不是转换参数。 - Andrey Tyukin
好的,没问题:在repl中,scala.Predef.unaugmentString(new StringOps(null))确实返回字符串“null” - 这很好。但这仍然无法解释为什么1 + null不会简单地将null转换为0并返回1。在我在问题下面的评论中,+++方法正是这样做的:它将类型为N的参数转换为I,然后调用I+++来产生I +++ I。因此,操作数被隐式转换了。在1 + null的情况下,操作数似乎被隐式转换了。为什么会这样呢? - Andrey Tyukin
IntelliJ声称有一些 augment / unaugment 洗钱的问题吗?实际编译器是否也这样认为?在更仔细地阅读规范后,我现在相信编译器实际上应该优先选择 any2stringadd 隐式转换,然后尝试将 null 转换,所以一切都很配合。因此,我希望它被解释为 any2stringadd(1).+(null),没有任何 augment / unaugment - Andrey Tyukin
感谢“显示隐式提示”选项。它仅适用于2018.2,而不适用于2018.1,所以我之前不知道。但是,当我显示提示并使它们明确时,我得到了下面的代码示例:implicit def nullToInt(x: Null) = 0; val x = 1 + Predef.Character2char(null); val y = 1 + nullToInt(null); println(Predef.any2stringadd(x) + " " + y);。当然,它不能提供与原始代码相同的结果。 - Dr Y Wit
显示剩余5条评论

3

我尝试回答自己的问题。

这个主题有点误导性,实际上表达式val x没有应用任何隐式转换。 NullString 的子类型,而 Int 有方法 abstract def +(x: String): String 因此也可以应用于 Null 。

这也可以通过Xprint:typer的输出来确认,因为它应该显示所有隐式转换,但对于x的表达式似乎并没有显示任何内容。

回答“是否有一种方法启用 implcit nullToInt”的问题,唯一的方法是在这种情况下明确指定,因为编译器将不考虑在成功编译代码时使用任何隐式。


哦,好吧...那么,这(几乎)是显而易见的。因此,问题的最初假设“没有接受null的int符号方法”被证明并不完全正确。在继续搜索隐式转换之前,我应该再次核实一下:] - Andrey Tyukin
太好了,谢谢你让我改变了答案。你提到没有可能进行转换后,我发现这其实比我想象的要简单。我进行了调试并注意到它直接跳转到StringBuffer。我决定看看any2addString做了什么。请查看修改后的答案。 - Daniel Hinojosa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接