Kotlin 在构造函数工作中调用非最终函数

2022-08-31 16:25:32

在 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)
}

输出是有意义的,因为调用时,尚未初始化。calculatex

这是我在编写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
    }
}

这工作得很好,因为被覆盖的函数不依赖于任何未初始化的数据,但它们对于每个各自的派生都是唯一的(因此需要抽象)。这在 kotlin 中也有效,但它仍然发出警告。extractAreaclass

那么,这在java/kotlin中是糟糕的做法吗?如果是这样,我该如何改进它?是否有可能在 kotlin 中实现,而不会被警告在构造函数中使用非最终函数?

一个潜在的解决方案是将行移动到每个派生的构造函数,但这似乎并不理想,因为它只是重复的代码,应该是超类的一部分。area = extractArea()


答案 1

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

基本上,在执行超类构造函数(包括其属性初始值设定项和块)时,派生类构造函数尚未运行。但是,即使从超类构造函数调用,被覆盖的成员也会保留其逻辑。这可能会导致从超级构造函数调用依赖于特定于派生类的某个状态的重写成员,这可能导致 Bug 或运行时故障。这也是你可以在Kotlin中获得a的情况之一。initNullPointerException

请考虑以下代码示例:

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
}

(可运行样品)

在这里,被覆盖依赖于正确初始化,但是在超级构造函数中使用时,的支持字段仍然保持 null。因此,构造 一个实例会抛出一个 NPE。sizeitemssizeitemsDerived

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


正如@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
    }
}

但是,这在重写的成员中使用相同的逻辑以及用于计算传递给超构造函数的值的情况下,会使派生类的代码复杂化。


答案 2

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

如果 的结果用于初始化成员或执行布局或其他操作,则可以考虑使用惰性初始化。这将推迟结果的计算,直到真正需要结果。calculation()