Kotlin 在构造函数中调用非 final 函数有效 [英] Kotlin calling non final function in constructor works

查看:86
本文介绍了Kotlin 在构造函数中调用非 final 函数有效的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在 Kotlin 中,它会在构造函数中调用抽象函数时发出警告,引用以下有问题的代码:

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

输出是有意义的,因为当调用 calculate 时,x 还没有被初始化.

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

这是我在编写 java 时从未考虑过的事情,因为我使用这种模式没有任何问题:

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

这很有效,因为重写的 extractArea 函数不依赖于任何未初始化的数据,但它们对于每个派生的 class 都是唯一的(因此需要是抽象的).这也适用于 kotlin,但它仍然给出警告.

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 函数?

So is this poor practice in java/kotlin? If so, how can I improve it? And is it possible to implement in kotlin without being warned about using non-final functions in constructors?

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

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 的情况之一.

Basically, at the point when a super class constructor (including its property initializers and the init blocks) is executed, the derived class constructor has not yet run. But the overridden members retain their logic even when called from the super class constructor. This may lead to an overridden member that relies on some state, that is specific to the derived class, being called from the super constructor, which can lead to a bug or a runtime failure. This is also one of the cases when you can get a NullPointerException in 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
}

(可运行示例)

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

Here, the overridden size relies on items being properly initialized, but at the point when size is used in the super constructor, the backing field of items still holds null. Constructing an instance of Derived therefore throws an NPE.

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

Using the practice in question safely requires considerable effort even when you don't share the code with anyone else, and when you do, other programmers will usually expect open members to be safe to override involving the state of the derived class.

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

As @Bob Dagleish correctly noted, you can use lazy initialization for the code property:

val code by lazy { calculate() }

但是你需要小心,不要在基类构造逻辑的任何其他地方使用code.

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

另一种选择是要求将 code 传递给基类构造函数:

Another option is to require code to be passed to the base class constructor:

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 在构造函数中调用非 final 函数有效的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆