如何在 init 块中处理重写的属性?
How are overridden properties handled in init blocks?
我正在尝试理解为什么会抛出以下代码:
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
。我发现这违反直觉:我将一个非空值传递给主构造函数,但在 super class 的 init 块中它解析为 null?
我想我对可能发生的事情有一个模糊的想法:因为 input
既作为构造函数参数又作为 属性,赋值在内部调用 this.input
, 但 this
尚未完全初始化。这真的很奇怪:在 IntelliJ 调试器中,input
正常解析(到值 "test"),但是一旦我调用表达式评估 window 并手动检查 input
,它突然为空。
假设这是预期的行为,您建议改为做什么,即当需要初始化从相同属性派生的字段时 class?
更新:
我已经发布了两个更简洁的代码片段来说明混淆的根源:
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
}
}
Sub class 中重写了 getInput() 方法,因此代码调用了 Sub.getInput()。此时Subclass的构造函数还没有执行,所以Sub.input的backing字段还是null。这不是 Kotlin 中的错误;你可以很容易地 运行 在纯 Java 代码中解决同样的问题。
修复是不覆盖 属性。 (我看过你的评论,但这并不能真正解释为什么你认为你需要覆盖它。)
混淆是因为您为 input
值(JVM 中的字段)创建了两个存储。一个在基础 class 中,一个在派生中。当您读取基 class 中的 input
值时,它会在后台调用虚拟 getInput
方法。 getInput
在派生 class 中被覆盖为 return 它自己的存储值,在调用基本构造函数之前未初始化。这是典型的 "virtual call in constructor" 问题。
如果您将派生 class 更改为实际使用超类型的 属性,一切都会好起来的。
class Sub(input: String) : Base(input) {
override val input : String
get() = super.input
}
我正在尝试理解为什么会抛出以下代码:
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
。我发现这违反直觉:我将一个非空值传递给主构造函数,但在 super class 的 init 块中它解析为 null?
我想我对可能发生的事情有一个模糊的想法:因为 input
既作为构造函数参数又作为 属性,赋值在内部调用 this.input
, 但 this
尚未完全初始化。这真的很奇怪:在 IntelliJ 调试器中,input
正常解析(到值 "test"),但是一旦我调用表达式评估 window 并手动检查 input
,它突然为空。
假设这是预期的行为,您建议改为做什么,即当需要初始化从相同属性派生的字段时 class?
更新: 我已经发布了两个更简洁的代码片段来说明混淆的根源:
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
}
}
Sub class 中重写了 getInput() 方法,因此代码调用了 Sub.getInput()。此时Subclass的构造函数还没有执行,所以Sub.input的backing字段还是null。这不是 Kotlin 中的错误;你可以很容易地 运行 在纯 Java 代码中解决同样的问题。
修复是不覆盖 属性。 (我看过你的评论,但这并不能真正解释为什么你认为你需要覆盖它。)
混淆是因为您为 input
值(JVM 中的字段)创建了两个存储。一个在基础 class 中,一个在派生中。当您读取基 class 中的 input
值时,它会在后台调用虚拟 getInput
方法。 getInput
在派生 class 中被覆盖为 return 它自己的存储值,在调用基本构造函数之前未初始化。这是典型的 "virtual call in constructor" 问题。
如果您将派生 class 更改为实际使用超类型的 属性,一切都会好起来的。
class Sub(input: String) : Base(input) {
override val input : String
get() = super.input
}