In Kotlin, it warns you when calling an abstract function in a constructor, citing the following problematic code:

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

And the output makes sense because when calculate is called, x hasn't been initialized yet.

This is something I had never considered when writing java, as I have used this pattern without any issues:

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

And this has worked fine because the overriden extractArea functions don't rely on any uninitialized data, but they are unique to each respective derived class (hence the need to be abstract). This also works in kotlin, but it still gives the warning.

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

A potential solution is to move the line area = extractArea() to each derived constructor, but this doesn't seem ideal since it's just repeated code that should be part of the super class.

推荐答案

The initialization order of a derived class is described in the language reference: Derived class initialization order, and the section also explains why it is a bad (and potentially dangerous) practice to use an open member in initialization logic of your class.

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

But then you need to be careful and not use code anywhere else in the base class construction logic.

另一个选项是要求将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
    }
}

This, however, complicates the code of the derived classes in cases when the same logic is used both in overridden members and for calculating the values passed to the super constructor.

Kotlin相关问答推荐

如何防止Android Studio在每一行的选项卡凹痕和文本之间添加空白?

Kotlin和JavaFX:绑定行为奇怪

将 java Optional 转换为 Kotlin Arrow Option

如果带注释的成员未被特定块包围,则发出 IDE 警告

Kotlin Path.useLines { } - 如何不获取 IOException("Stream closed")?

在 Kotlin 中,为什么在 `+` 之前但在 `.` 之前没有换行符?

通用接口继承

Kotlin SAM/功能接口抛出 AbstractMethodError

Jetpack Compose - 单击 LazyColumn 的项目时应用程序崩溃

Kotlin 使用迭代索引过滤 lambda 数组

将 Kotlin 类属性设置器作为函数引用

如何使 TextInputEditText 只读?

如何使用 Coil 从 URL 获取位图?

如何通过反射使用 Kotlin 对象

Kotlin 创建snackbar

在java代码中使用kotlin库

如何解决:将Java类转换为Kotlin后出现error: cannot find symbol class ...?

使用Dagger 2提供函数依赖性

使用导航组件在不同的图形之间导航

Java中lazy的Kotlin类似功能是什么?