问题描述
在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中实现而不会被警告不要在构造函数中使用非最终函数?
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
块)时,派生类构造函数尚未运行.但是,即使从超类构造函数调用时,被重写的成员仍保留其逻辑.这可能导致从super构造函数调用依赖于某个状态的重写成员,该状态特定于派生类,这可能导致错误或运行时失败.这也是在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
的后备字段仍为null.因此,构造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 正确指出的那样,您可以使用:
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在构造函数中调用非最终函数的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!