Подтвердить что ты не робот

Нуль в Scala... почему это возможно?

Я кодировал в Scala и делал несколько быстрых рефакторингов в Intellij, когда я наткнулся на следующую часть странности...

package misc

/**
 * Created by abimbola on 05/10/15.
 */
object WTF extends App {

  val name: String = name
  println(s"Value is: $name")
}

Затем я заметил, что компилятор не жаловался, поэтому я решил попытаться запустить это, и у меня получился очень интересный вывод

Value is: null
Process finished with exit code 0

Может ли кто-нибудь сказать мне, почему это работает?

ИЗМЕНИТЬ:

  • Первая проблема, значение name получает ссылку на себя, даже если она еще не существует; почему точно компилятор Scala не взорвался с ошибками???

  • Почему значение присваивания null?

4b9b3361

Ответ 1

1.) Почему компилятор не взорвался

Вот приведенный пример. Это компилируется, потому что через заданный тип можно вывести значение по умолчанию:

class Example { val x: Int = x }

scalac Example.scala 
Example.scala:1: warning: value x in class Example does nothing other than call itself recursively
class Example { val x: Int = x }

Это не компилируется, потому что значение по умолчанию не может быть выведено:

class ExampleDoesNotCompile { def x = x }

scalac ExampleDoesNotCompile.scala 
ExampleDoesNotCompile.scala:1: error: recursive method x needs result type
class ExampleDoesNotCompile { def x = x }

1.1 Что происходит здесь

Мое толкование. Поэтому будьте осторожны: принцип равномерного доступа срабатывает. Назначение val x вызывает аксессор x(), который возвращает унифицированное значение x. Таким образом, для x установлено значение по умолчанию.

class Example { val x: Int = x }
                             ^
[[syntax trees at end of                   cleanup]] // Example.scala
package <empty> {
  class Example extends Object {
    private[this] val x: Int = _;
    <stable> <accessor> def x(): Int = Example.this.x;
    def <init>(): Example = {
      Example.super.<init>();
      Example.this.x = Example.this.x();
      ()
    }
  }
}                            ^

2.) Почему значение равно null

Значения по умолчанию определяются средой Scala, скомпилированной в.

В примере, который вы указали, он выглядит так, как будто вы запускаете JVM. Значение по умолчанию для объекта здесь null.

Поэтому, когда вы не предоставляете значение, значение по умолчанию используется как резерв.

Значения по умолчанию JVM:

byte  0
short 0
int   0
long  0L
float 0.0f
double    0.0d
char  '\u0000'
boolean   false
Object    null // String are objects.

Также значение по умолчанию является допустимым значением для данного типа: Вот пример в REPL:

scala> val x : Int = 0
x: Int = 0

scala> val x : Int = null
<console>:10: error: an expression of type Null is ineligible for implicit conversion
val x : Int = null
                   ^
scala> val x : String = null
x: String = null

Ответ 2

почему именно компилятор Scala не взорвался с ошибками?

Потому что эта проблема не может быть решена в общем случае. Знаете ли вы проблему ? Проблема остановки говорит о том, что невозможно написать алгоритм, который обнаруживает, что программа когда-либо останавливается. Поскольку проблема выяснения того, приведет ли рекурсивное определение к нулевому присваиванию, может быть сведена к проблеме остановки, также решить ее невозможно.

Ну, теперь довольно легко запретить рекурсивные определения вообще, это делается, например, для значений, которые не являются значениями класса:

scala> def f = { val k: String = k+"abc" }
<console>:11: error: forward reference extends over definition of value k
       def f = { val k: String = k+"abc" }
                                 ^

Для значений класса эта функция не запрещена по нескольким причинам:

  • Их объем не ограничен.
  • JVM инициализирует их значением по умолчанию (которое является нулевым для ссылочных типов).
  • Рекурсивные значения полезны

Ваш случай использования тривиален, как это:

scala> val k: String = k+"abc"
k: String = nullabc

Но как насчет этого:

scala> object X { val x: Int = Y.y+1 }; object Y { val y: Int = X.x+1 }
defined object X
defined object Y

scala> X.x
res2: Int = 2

scala> Y.y
res3: Int = 1

scala> object X { val x: Int = Y.y+1 }; object Y { val y: Int = X.x+1 }
defined object X
defined object Y

scala> Y.y
res4: Int = 2

scala> X.x
res5: Int = 1

Или это:

scala> val f: Stream[BigInt] = 1 #:: 1 #:: f.zip(f.tail).map { case (a,b) => a+b }
f: Stream[BigInt] = Stream(1, ?)

scala> f.take(10).toList
res7: List[BigInt] = List(1, 1, 2, 3, 5, 8, 13, 21, 34, 55)

Как вы можете видеть, довольно легко писать программы, где уже не очевидно, какое значение они приведут. И так как проблема остановки не разрешима, мы не можем позволить компилятору выполнить эту работу для нас в нетривиальных случаях.

Это также означает, что тривиальные случаи, как показано в вашем вопросе, могут быть жестко закодированы в компиляторе. Но так как не существует алгоритма, который мог бы обнаружить все возможные тривиальные случаи, все случаи, которые когда-либо были найдены, должны быть жестко закодированы в компиляторе (не говоря уже о том, что определение тривиального случая не существует). Поэтому было бы неразумно даже начинать жестко кодировать некоторые из этих случаев. В конечном итоге это приведет к более медленному компилятору и компилятору, который будет сложнее поддерживать.

Можно утверждать, что для случая использования, который сжигает каждого второго пользователя, было бы разумно, по крайней мере, жестко кодировать такой экстремальный сценарий. С другой стороны, некоторые люди просто должны быть сожжены, чтобы узнать что-то новое.;)

Ответ 3

Я думаю, что @Andreas answer уже имеет необходимую информацию. Я просто попробую дать дополнительное объяснение:

Когда вы пишете val name: String = name на уровне класса, это делает несколько разных вещей одновременно:

  • создать поле name
  • создать getter name()
  • создать код для назначения name = name, который становится частью основного конструктора

Это то, что явствует из 1.1

Андреаса,
package <empty> {
  class Example extends Object {
    private[this] val x: Int = _;
    <stable> <accessor> def x(): Int = Example.this.x;
    def <init>(): Example = {
      Example.super.<init>();
      Example.this.x = Example.this.x();
      ()
    }
  }
}

Синтаксис не Scala, это (как предложено [[syntax trees at end of cleanup]]) текстовое представление того, что компилятор позже преобразует в байт-код. В некотором незнакомом синтаксисе мы можем это интерпретировать, как JVM:

  • JVM создает объект. На этом этапе все поля имеют значения по умолчанию. val x: Int = _; похож на int x; в Java, то есть используется значение по умолчанию JVM, которое 0 для I (т.е. int в Java или int в Scala)
  • конструктор вызывается для объекта
  • (вызывается суперструктор)
  • конструктор вызывает x()
  • x() возвращает x, который равен 0
  • x присваивается 0
  • конструктор возвращает

как вы можете видеть, после начального шага синтаксиса в дереве синтаксиса ничего не получается, что кажется неправильным, хотя исходный исходный код выглядит неправильно. Я бы не сказал, что это поведение, которое я ожидаю, поэтому я бы представил одну из трех вещей:

  • Либо разработчики Scala видели, что это слишком сложно распознать и запретить
  • или, это регрессия и просто не найдена как ошибка
  • или, это "функция", и есть законная потребность в этом поведении.

(упорядочение отражает мое мнение о вероятности, в порядке убывания)