繁体   English   中英

Kotlin 在构造函数中调用非 final 函数有效

[英]Kotlin calling non final function in constructor works

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

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

基本上,在执行超类构造函数(包括其属性初始值设定项和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
}

(可运行的样本)

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

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


正如@Bob Dagleish正确指出的那样,您可以对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
    }
}

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

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

如果用calculation()的结果来初始化一个成员,或者执行布局什么的,你可以考虑使用lazy Initialization 这将推迟结果的计算,直到真正需要结果。

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

来自: area = extractArea(room);

To: area by lazy { extractArea(room) }

GL

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM