【问题标题】:How are overridden properties handled in init blocks?在 init 块中如何处理被覆盖的属性?
【发布时间】:2016-01-09 18:20:35
【问题描述】:

我试图理解为什么会抛出以下代码:

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 块中它解析为空?

我想我对可能发生的事情有一个模糊的概念:由于input 既用作构造函数参数又用作属性,赋值在内部调用this.input,但this 没有完全初始化然而。真的很奇怪:在 IntelliJ 调试器中,input 正常解析(解析为值“test”),但是当我调用表达式评估窗口并手动检查 input 时,它突然为空。

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

更新: 我发布了两个更简洁的代码 sn-ps 来说明混淆的根源:

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

【问题讨论】:

  • 如果在子类中删除了override val,则不会发生错误。我猜这是一个错误,因为即使为基类(和/或子类)提供可选值仍然会导致空错误
  • 从示例代码中看不出来,但我实际上需要覆盖该属性。在我的真实代码中的构造函数中传递的属性类型在这个位置是协变的,因此它必须重新定义(并因此覆盖)基类属性。
  • 这一分钟越来越奇怪。看看这个更简洁的测试用例,其中我使属性协方差的问题更加明显:gist.github.com/mttkay/9fbb0ddf72f471465afc 这里我在构造函数中传递了 42,它打印出 0。^^
  • 为什么不将 Base 声明为 Base(val value: T) 并做 Sub(value: SubValue) : Base(value),而不是覆盖属性?跨度>
  • @VinceEmigh 当然,我可以做到。这意味着我必须返回并更改我的整个代码库以现在使用泛型类型,而在 Java 中,非泛型类型和协变 getter 就足够了。我发现我在@yole 的答案的评论中发布了另一个解决方案。显然,可以通过将主 ctor 中的属性仅转换为 ctor 参数并将实际的 val 声明移动到类主体中来巧妙地改变程序语义。这将使 init 块中的 input 直接引用参数而不是属性。

标签: kotlin


【解决方案1】:

原来的例子等价于下面的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 值的后备字段仍然为空。这不是 Kotlin 中的错误;在纯 Java 代码中很容易遇到同样的问题。

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

【讨论】:

  • 我同意会发生这种情况,但是在创建 Base("test") 时,它不会抛出(即使在 Base 类的 init 块中显式取消引用 this.input 时);尽管在这种情况下,主构造函数在init 块之后也被调用。这对我来说似乎是矛盾的,但是如果这是预期的行为你能解释基类和子类创建过程之间的差异(我认为这是对原始问题的重新表述)吗?
  • 为什么说主构造函数是在init块之后调用的呢?在 Java 字节码级别,所有 init 块中的代码都附加到主构造函数,并在将构造函数参数复制到支持字段的代码之后执行。
  • 对,既然你这么说,很明显发生了什么。你不认为我在超类构造函数中引用input 非常令人困惑,它可以作为我的参数使用,并且我传递了一个正确的值,但它还没有初始化?如果我是唯一一个认为这是完全倒退的人,我会感到惊讶。
  • @desseim:我已经编辑了我的答案,将 Kotlin 完整翻译成等效的 Java 代码,所以我希望行为上的差异不再令人眼花缭乱
  • @Matthias:我们对主构造函数的设计有效地迫使我们使用相同的名称来引用两个不同的东西(构造函数参数和属性),确实有时这会导致令人惊讶的行为.但是,我们没有替代设计来解决这些意外,同时仍然允许保留主要的构造函数语法。
【解决方案2】:

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

如果您将派生类更改为实际使用超类型的属性,一切都会再次正常。

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

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2013-01-19
  • 1970-01-01
  • 2018-09-17
  • 2015-04-24
  • 1970-01-01
  • 2011-05-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多