【问题标题】:Kotlin calling non final function in constructor worksKotlin 在构造函数中调用非最终函数有效
【发布时间】:2018-05-07 20:42:55
【问题描述】:

在 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() 移动到每个派生构造函数,但这似乎并不理想,因为它只是重复的代码,应该是超类的一部分。

【问题讨论】:

  • 我重新打开了这个问题,因为它询问的是 Kotlin,而不是 Java。虽然 Kotlin 和 Java 在这方面很相似,但 Kotlin 的实例构造并不等同于 Java 中的实例构造(至少它以不同的构造表示)。
  • @hotkey 好的,我认为接受的答案很好地解释了它。无论如何,我的问题可能与 java 更相关。正是 kotlin 中的警告引发了这个问题。
  • @hotkey "java/kotlin 中的这种糟糕做法也是如此" - 似乎他们都在要求。他们的困惑来自 Java 代码的工作(“错误”没有出现),这是因为它实际上并没有复制 Kotlin 代码正在做的事情。标记副本中的答案肯定解释了他获得的警告和结果。

标签: java oop kotlin


【解决方案1】:

在语言参考中描述了派生类的初始化顺序:Derived class initialization order,该部分还解释了为什么在类的初始化逻辑中使用开放成员是一种不好的(并且可能是危险的)做法。

基本上,在执行超类构造函数(包括其属性初始化程序和init 块)时,派生类构造函数尚未运行。但是,即使从超类构造函数调用,被覆盖的成员也会保留它们的逻辑。这可能会导致从超级构造函数调用依赖于某些状态(特定于派生类)的重写成员,这可能导致错误或运行时故障。这也是在 Kotlin 中可以得到 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
}

(runnable sample)

这里,被覆盖的size 依赖于items 被正确初始化,但是在超级构造函数中使用size 时,items 的支持字段仍然为空。因此,构造 Derived 的实例会引发 NPE。

即使您不与其他任何人共享代码,安全地使用有问题的做法也需要付出相当大的努力,而当您这样做时,其他程序员通常会期望开放成员可以安全地覆盖涉及派生类的状态。


正如@Bob Dagleish 正确指出的那样,您可以将lazy initialization 用于code 属性:

val 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
    }
}

然而,如果在被覆盖的成员和计算传递给超级构造函数的值中使用相同的逻辑时,这会使派生类的代码变得复杂。

【讨论】:

  • 我明白了。一旦我理解了 lint 警告,我就意识到这是不好的做法。谢谢你的解释
  • 很好的解释!但我仍然想知道它背后的机制是什么。编译器如何知道何时将 0 插入 以及何时不启动 ?编译器如何知道在创建派生类时,为打开的属性 赋值需要等到派生类构造函数运行?只是好奇。
【解决方案2】:

这绝对是不好的做法,因为您在 部分构造的对象上调用 calculate()。这表明您的类有多个初始化阶段。

如果calculation()的结果用于初始化成员,或者执行布局什么的,你可以考虑使用惰性初始化。这会将结果的计算推迟到真正需要结果为止。

【讨论】:

    【解决方案3】:

    要在抽象类中调用具体类的函数,请使用by lazy,它允许调用非最终函数。

    发件人:area = extractArea(room);

    收件人:area by lazy { extractArea(room) }

    GL

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-01-10
      • 1970-01-01
      • 1970-01-01
      • 2015-01-24
      • 2014-06-11
      • 2013-12-24
      • 2018-04-01
      • 1970-01-01
      相关资源
      最近更新 更多