Инициализация переменной Kotlin для дочернего класса ведет себя странно для инициализации переменной со значением 0

16

Я создал следующую иерархию классов:

open class A {
    init {
        f()
    }

    open fun f() {
        println("In A f")
    }
}

class B : A() {
    var x: Int = 33

    init {
        println("x: " + x)
    }

    override fun f() {
        x = 1
        println("x in f: "+ x)
    }

    init {
        println("x2: " + x)
    }
}

fun main() {
    println("Hello World!!")
    val b = B()
    println("in main x : " + b.x)
}

Выход этого кода

Hello World!!
x in f: 1
x: 33
x2: 33
in main x : 33

Но если я изменю инициализацию xс

var x: Int = 33

в

var x: Int = 0

результат показывает вызов метода в отличие от вывода выше:

Hello World!!
x in f: 1
x: 1
x2: 1
in main x : 1

Кто-нибудь знает, почему инициализация с 0вызывает другое поведение, чем с другим значением?

ПРАТЮШ СИНГХ
источник
4
Не имеет прямого отношения, но вызов переопределяемых методов из конструкторов, как правило, не является хорошей практикой, поскольку это может привести к непредвиденному поведению (и к эффективному нарушению контракта / инвариантов суперкласса из подклассов).
Адам Хошек

Ответы:

18

Суперкласс инициализируется перед подклассом.

Вызов конструктора B вызывает конструктор A, который вызывает функцию f, печатающую "x in f: 1", после инициализации A инициализируется остальная часть B.

По сути, установка значения перезаписывается.

(Когда вы инициализируете примитивы с их нулевым значением в Kotlin, они технически просто не инициализируются вообще)

Вы можете наблюдать это «перезаписать» поведение, изменив подпись с

var x: Int = 0 в var x: Int? = 0

Поскольку xбольше не является примитивом int, поле фактически инициализируется значением, что приводит к выводу:

Hello World!!
x in f: 1
x: 0
x2: 0
in main x : 0
Sxtanna
источник
5
Когда вы инициализируете примитивы с их нулевым значением в Kotlin, они технически просто не инициализируются вообще, это то, что я хотел прочитать ... Спасибо!
deHaar
Это все еще похоже на ошибку / несоответствие.
Кроппеб
2
@Kroppeb это просто Java, такое же поведение можно наблюдать только в коде Java. Это не имеет ничего общего с Kotlin
Sxtanna
8

Это поведение описано в документации - https://kotlinlang.org/docs/reference/classes.html#derived-class-initialization-order.

Если любое из этих свойств используется в логике инициализации базового класса (прямо или косвенно, через другую переопределенную реализацию открытого члена), это может привести к некорректному поведению или сбою во время выполнения. Поэтому при разработке базового класса следует избегать использования открытых членов в конструкторах, инициализаторах свойств и блоках инициализации.

UPD:

Существует ошибка, которая вызывает это несоответствие - https://youtrack.jetbrains.com/issue/KT-15642

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

vanyochek
источник
1
Кроме того, IntelliJ предупреждает вас об этом. Вызов f()в initблоке Aвыдает предупреждение «Вызов не финальной функции f в конструкторе»
Кроппеб
В предоставленной вами документации говорится, что «инициализация базового класса выполняется в качестве первого шага и, таким образом, происходит до запуска логики инициализации производного класса», что именно и происходит в первом примере вопроса. Однако во втором примере инструкция инициализации ( var x: Int = 0) производного класса вообще не выполняется, что противоречит тому, что написано в документации, что приводит меня к мысли, что это может быть ошибкой.
Субару Таширо
@SubaruTashiro Да, вы правы. Это еще одна проблема - youtrack.jetbrains.com/issue/KT-15642 .
ванёчек