Нуль в 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?
Ответы
Ответ 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 видели, что это слишком сложно распознать и запретить
- или, это регрессия и просто не найдена как ошибка
- или, это "функция", и есть законная потребность в этом поведении.
(упорядочение отражает мое мнение о вероятности, в порядке убывания)