Как переопределенные свойства обрабатываются в блоках init?

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

open class Base(open val input: String) { lateinit var derived: String init { derived = input.toUpperCase() // throws! } } class Sub(override val input: String) : Base(input) 

При вызове этого кода:

 println(Sub("test").derived) 

он выдает исключение, потому что в момент toUpperCase input разрешает null . Я нахожу этот счетчик интуитивным: передаю ненулевое значение первому конструктору, но в блоке init суперкласса он разрешает null?

Я думаю, что у меня есть смутное представление о том, что может происходить: поскольку input служит как аргументом конструктора, так и свойством, внутреннее назначение присваивает this.input , но this еще не полностью инициализировано. Это действительно странно: в отладчике IntelliJ input разрешается нормально (до значения «test»), но как только я вызываю окно оценки выражения и проверяю input вручную, он неожиданно null.

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

ОБНОВЛЕНИЕ: я опубликовал два еще более сжатых фрагмента кода, которые иллюстрируют, откуда возникает путаница:

https://gist.github.com/mttkay/9fbb0ddf72f471465afc https://gist.github.com/mttkay/5dc9bde1006b70e1e8ba

Исходный пример эквивалентен следующей программе Java:

 class Base { private String input; private String derived; Base(String input) { this.input = input; this.derived = getInput().toUpperCase(); // Initializes derived by calling an overridden method } public String getInput() { return input; } } class Derived extends Base { private String input; public Derived(String input) { super(input); // Calls the superclass constructor, which tries to initialize derived this.input = input; // Initializes the subclass field } @Override public String getInput() { return input; // Returns the value of the subclass field } } 

Метод getInput () переопределяется в Sub-классе, поэтому код вызывает Sub.getInput (). В это время конструктор класса Sub не выполнялся, поэтому поле поддержки, содержащее значение Sub.input, по-прежнему равно нулю. Это не ошибка в Котлине; вы можете легко запустить ту же проблему в чистом Java-коде.

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

Путаница возникает из-за того, что вы создали два хранилища для input значения (поля в JVM). Один из них находится в базовом классе, один в производном. Когда вы читаете input значение в базовом классе, он вызывает виртуальный метод getInput под капотом. getInput переопределяется в производном классе для возврата собственного хранимого значения, которое не инициализируется до вызова базового конструктора. Это типичная проблема «виртуального вызова в конструкторе».

Если вы измените производный класс на фактическое использование свойства супер-типа, все будет хорошо.

 class Sub(input: String) : Base(input) { override val input : String get() = super.input }