Неявные преобразования и null

Следующий код

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

Я ожидал, что оба значения будут Int и равны 1.

По-видимому, первый val - это String и равен "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. Я не говорю о лучших практиках здесь. Не стесняйтесь рассматривать вопрос как предмет академического интереса.


person Dr Y Wit    schedule 28.09.2018    source источник
comment
Не ответ, не предложение улучшить вопрос, а просто забавный факт, чтобы сравнить странное поведение + с любым другим методом (здесь +++): 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. Я не уверен, почему это работает наоборот с +.   -  person Andrey Tyukin    schedule 28.09.2018


Ответы (2)


Я постараюсь ответить на свой вопрос.

Тема немного вводит в заблуждение, и на самом деле к выражению для val x вообще не применяются неявные преобразования. Null является подтипом String, а Int имеет метод abstract def +(x: String): String, поэтому его можно применять и к Null.

Это также подтверждается выводом Xprint:typer, потому что он должен показывать все неявные преобразования и, по-видимому, ничего не показывает для выражения для x.

И отвечая на вопросы «есть ли способ включить имплицит nullToInt», единственный способ включить его — указать явно в этом случае, потому что компилятор не будет рассматривать использование каких-либо имплицитов, когда код успешно скомпилирован без них.

person Dr Y Wit    schedule 03.10.2018
comment
О, ну... Хорошо, тогда это (почти) очевидно. Итак, первоначальное предположение вопроса Нет символьных методов для int, принимающих null, оказалось не совсем верным. Я должен был перепроверить это, прежде чем продолжить поиск неявных преобразований :] - person Andrey Tyukin; 03.10.2018
comment
Отлично, благодаря этому я изменил свой ответ, он был проще, чем я думал, теперь, когда вы упомянули, что конвертация невозможна. Я сделал отладку и заметил, что он прыгнул прямо в StringBuffer. Я решил просто посмотреть, что сделал any2addString. Смотрите измененный ответ. - person Daniel Hinojosa; 04.10.2018

Итак, то, что говорит @AndreyTyukin, верно, механически я думаю, что это нечто большее. Есть две вещи, которые происходят относительно того, почему.

  1. Any украшен implicit в Predef, см. следующее:

    implicit final class any2stringadd[A] extends AnyVal

Как видите, any2stringadd отвечает за +, и вы можете увидеть подпись здесь:

def +(other: String): String

Исправление: implicit conversions нет, было еще проще

  1. Глядя внутрь исходного кода Predef и any2stringadd, который действительно работает, можно увидеть следующее.

implicit final class any2stringadd[A](private val self: A) extends AnyVal { def +(other: String): String = String.valueOf(self) + other }

String.valueOf из 1 вернет String из 1. В Java (и проверьте с помощью jshell) String из 1, добавленное к null, станет 1null.

jshell> "1" + null
$1 ==> "1null"
person Daniel Hinojosa    schedule 28.09.2018
comment
implicitly[Null => String] на самом деле тот же объект, что и implicitly[Null <:< String], и этот объект существует, потому что, как я уже сказал, Null является подтипом всех AnyRef типов, в частности подтипом String. Я несколько удивлен, что есть any2stringadd, я думал, что это просто небольшой глюк документации, и метод был на самом деле на Any, но действительно, есть это преобразование. - person Andrey Tyukin; 28.09.2018
comment
Также я не понимаю последний абзац про augmentString и т.д. - что он должен был демонстрировать? Выражение new StringOps(null) аварийно завершает работу с NullPointerException в REPL. Несмотря на то, что есть any2stringadd, я все еще не понимаю, почему он применяется к целому числу 1 вместо преобразования аргумента. - person Andrey Tyukin; 29.09.2018
comment
Да, @AndreyTyukin, забыл упомянуть. В REPL это не удается, потому что я предполагаю, что он пытается вернуть интерпретацию String для каждой строки. Но работая в приложении или скрипте, он возвращает String из null. augmentString необходимо, чтобы обернуть java.lang.String StringOps, который, вероятно, должен был называться RichString. Итак, я думаю, что происходит то, что вызывается augmentString, а затем, когда он понимает, что ему нужно немедленно unaugmentString, он просто становится String. null ни в коем случае нельзя было увеличивать. - person Daniel Hinojosa; 29.09.2018
comment
Хорошо, хорошо: scala.Predef.unaugmentString(new StringOps(null)) действительно возвращает строку "null" в ответе - это нормально. Но это все еще не объясняет, почему 1 + null просто не преобразует null в 0 и не возвращает 1. В моем комментарии к вопросу метод +++ делает именно это: он преобразует аргумент типа N в I, а затем вызывает +++ из I для создания I +++ I. Таким образом, операнд right преобразуется неявно. В случае 1 + null операнд left кажется неявно преобразованным. Почему это? - person Andrey Tyukin; 29.09.2018
comment
Поскольку фактический вызов для 1 + "Foo" — это any2stringadd(1) + "Foo", а поскольку null является строкой благодаря отмыванию augment/unaugment, теперь это String с именем "null". Что касается того, что он должен возвращать, я не соглашусь, я думаю, что 1 + null должна быть ошибкой компиляции, как в Java, bad operand types for binary operator '+'. - person Daniel Hinojosa; 29.09.2018
comment
Кстати, если кто-то использует IntelliJ, сделайте CMD+SHIFT+A/CTRL+SHIFT+A и выберите Показать неявные подсказки, и он покажет, что он на самом деле вызывает. Сначала я не поверил, но теперь я думаю, что это делает большую работу. ;) - person Daniel Hinojosa; 29.09.2018
comment
Утверждает ли intelliJ, что имеет место augment/unaugment отмывание денег? Думает ли реальный компилятор так же? После более внимательного прочтения спецификации я пришел к выводу, что компилятор на самом деле должен предпочесть неявное преобразование any2stringadd перед попыткой преобразования null, так что все сходится. Итак, я ожидаю, что это интерпретируется как any2stringadd(1).+(null) без каких-либо augment/unaugment. - person Andrey Tyukin; 29.09.2018
comment
Я тоже этого ожидаю, null никогда не следует идти к augment, а затем к unaugment. Так как augment и unaugment должны быть зарезервированы для украшения String - person Daniel Hinojosa; 29.09.2018
comment
Спасибо за опцию Показать неявные подсказки. Он доступен только в 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);. Конечно, он дает не тот результат, что исходный код. - person Dr Y Wit; 03.10.2018
comment
@AndreyTyukin, я не уверен, что scala.Predef.unaugmentString(new StringOps(null)) происходит, потому что 1) StringOps не виден в моем исходном коде 2) Scala не допускает двойных преобразований, таких как implicit1(implicit2(argument)) - person Dr Y Wit; 03.10.2018