Котлин вызывает не финальную функцию в работах конструктора - PullRequest
0 голосов
/ 07 мая 2018

В Kotlin он предупреждает вас при вызове абстрактной функции в конструкторе, ссылаясь на следующий проблемный код:

abstract class Base {
    var code = calculate()
    abstract fun calculate(): Int
}

class Derived(private val x: Int) : Base() {
    override fun calculate(): Int = x
}

fun main(args: Array<String>) {
    val i = Derived(42).code // Expected: 42, actual: 0
    println(i)
}

И вывод имеет смысл, потому что когда вызывается calculate, x еще не инициализирован.

Это то, что я никогда не рассматривал при написании Java, так как я использовал этот шаблон без каких-либо проблем:

class Base {

    private int area;

    Base(Room room) {
        area = extractArea(room);
    }

    abstract int extractArea(Room room);
}

class Derived_A extends Base {

    Derived_A(Room room) {
        super(room);
    }

    @Override
    public int extractArea(Room room) {
        // Extract area A from room
    }
}

class Derived_B extends Base {

    Derived_B(Room room) {
        super(room);
    }

    @Override
    public int extractArea(Room room) {
        // Extract area B from room
    }
}

И это сработало нормально, потому что переопределенные функции extractArea не полагаются на неинициализированные данные, но они уникальны для каждого соответствующего производного class (поэтому необходимо быть абстрактным). Это также работает в kotlin, но все равно выдает предупреждение.

Так это плохая практика в java / kotlin? Если так, как я могу улучшить это? И возможно ли реализовать в kotlin, не предупреждая об использовании не конечных функций в конструкторах?

Потенциальное решение - переместить строку area = extractArea() к каждому производному конструктору, но это не кажется идеальным, поскольку это просто повторяющийся код, который должен быть частью суперкласса.

Ответы [ 2 ]

0 голосов
/ 08 мая 2018

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

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

Рассмотрим этот пример кода:

open class Base {
    open val size: Int = 0
    init { println("size = $size") }
}

class Derived : Base() {
    val items = mutableListOf(1, 2, 3)
    override val size: Int get() = items.size
}

(работающий образец)

Здесь переопределенный size зависит от правильной инициализации items, но в тот момент, когда size используется в супер-конструкторе, поле поддержки items по-прежнему содержит ноль. Поэтому создание экземпляра Derived создает NPE.

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


Как правильно заметил @ Боб Даглиш , вы можете использовать отложенную инициализацию для свойства code:

var code by lazy { calculate() }

Но тогда вам нужно быть осторожным и не использовать code где-либо еще в логике построения базового класса.

Другим вариантом является требование передачи code конструктору базового класса:

abstract class Base(var code: Int) {
    abstract fun calculate(): Int
}

class Derived(private val x: Int) : Base(calculateFromX(x)) {
    override fun calculate(): Int = 
        calculateFromX(x)

    companion object {
        fun calculateFromX(x: Int) = x
    }
}

Это, однако, усложняет код производных классов в тех случаях, когда одна и та же логика используется как в переопределенных членах, так и для вычисления значений, передаваемых в супер-конструктор.

0 голосов
/ 07 мая 2018

Это определенно плохая практика, потому что вы вызываете calculate() на частично построенном объекте . Это говорит о том, что ваш класс имеет несколько фаз инициализации.

Если результат calculation() используется для инициализации члена, выполнения макета или чего-то еще, вы можете рассмотреть возможность использования lazy initialization . Это задержит вычисление результата до тех пор, пока результат действительно не понадобится.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...