如何在init块中处理被覆盖的属性?

时间:2016-01-09 18:20:36

标签: kotlin

我试图了解以下代码抛出的原因:

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。我发现这个反直觉:我将一个非null值传递给主构造函数,但是在超类的init块中它解析为null?

我想我对可能发生的事情有一个模糊的概念:因为input既是构造函数参数又是属性,分配内部调用this.input,但是{{1}尚未完全初始化。这真的很奇怪:在IntelliJ调试器中,this正常解析(到值#34; test"),但只要我调用表达式评估窗口并检查{{1}手动地,它突然变为空。

假设这是预期的行为,你建议做什么,即当需要初始化从同一类的属性派生的字段时?

更新 我发布了两个更简洁的代码片段,说明了混淆源自何处:

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

2 个答案:

答案 0 :(得分:8)

原始示例等同于以下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类中重写了getInput()方法,因此代码调用Sub.getInput()。此时,Sub类的构造函数尚未执行,因此保存Sub.input值的后备字段仍为null。这不是Kotlin的错误;您可以轻松地在纯Java代码中遇到同样的问题。

修复方法是不覆盖该属性。 (我已经看过你的评论,但这并没有真正解释为什么你认为你需要覆盖它。)

答案 1 :(得分:3)

这种混淆来自于您为input值创建了两个存储(JVM中的字段)。一个是基类,一个是派生的。当您在基类中读取input值时,它会调用虚拟getInput方法。在派生类中重写getInput以返回其自己的存储值,该值在调用基本构造函数之前未初始化。这是构造函数中的典型"虚拟调用"问题。

如果你改变派生类实际使用超类型的属性,一切都很好。

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