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

jef*_*son 19 java oop kotlin

在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)
}
Run Code Online (Sandbox Code Playgroud)

输出是有意义的,因为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
    }
}
Run Code Online (Sandbox Code Playgroud)

这样做很好,因为覆盖extractArea函数不依赖于任何未初始化的数据,但它们对于每个相应的派生都是唯一的class(因此需要是抽象的).这也适用于kotlin,但仍然会发出警告.

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

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

hot*_*key 29

派生类的初始化顺序在语言参考:派生类初始化顺序中描述,该部分还解释了为什么在类的初始化逻辑中使用open成员是一种糟糕的(并且可能是危险的)实践.

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

考虑以下代码示例:

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
}
Run Code Online (Sandbox Code Playgroud)

(可运行的样本)

这里,重写size依赖于items正确初始化,但是当size在超级构造函数中使用时,items仍然保持null 的支持字段.Derived因此构造一个实例会抛出一个NPE.

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


正如@Bob Dagleish正确指出的那样,您可以对属性使用延迟初始化code:

val code by lazy { calculate() }
Run Code Online (Sandbox Code Playgroud)

但是,你需要小心,不要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
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,如果在重写成员中使用相同的逻辑并计算传递给超级构造函数的值,则会使派生类的代码复杂化.


Bob*_*ish 6

这是实践绝对差,因为要调用calculate()一个上部分构造的对象。这表明您的类具有多个初始化阶段。

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