在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)
}
输出有意义,因为调用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
}
}
这样做很好,因为覆盖的extractArea
函数不依赖于任何未初始化的数据,但它们对于每个相应的派生class
都是唯一的(因此需要是抽象的)。这也适用于kotlin,但它仍然会发出警告。
这是java / kotlin的糟糕做法吗?如果是这样,我该如何改进呢?是否可以在kotlin中实现而不会被警告在构造函数中使用非最终函数?
一个潜在的解决方案是将行area = extractArea()
移动到每个派生的构造函数,但这似乎并不理想,因为它只是重复代码应该是超类的一部分。
答案 0 :(得分:20)
派生类的初始化顺序在语言参考中描述:Derived class initialization order,该部分还解释了为什么在类的初始化逻辑中使用open成员是一种糟糕的(并且可能是危险的)实践
基本上,在执行超类构造函数(包括其属性初始值设定项和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
}
此处,被覆盖的size
依赖items
正确初始化,但在超级构造函数中使用size
时,items
的支持字段仍然存在保持null。因此,构造Derived
的实例会抛出NPE。
即使您不与其他任何人共享代码,安全地使用相关实践也需要相当大的努力,而当您这样做时,其他程序员通常会期望开放成员可以安全地覆盖涉及派生类的状态。
正如@Bob Dagleish所述,您可以将lazy initialization用于code
属性:
var 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
}
}
但是,如果在重写成员中使用相同的逻辑并计算传递给超级构造函数的值,则会使派生类的代码复杂化。
答案 1 :(得分:4)
这绝对是不好的做法,因为您在部分构建的对象上调用calculate()
。这表明您的类有多个初始化阶段。
如果calculation()
的结果用于初始化成员,或执行布局等,您可以考虑使用延迟初始化。这将推迟计算结果,直到确实需要结果。
答案 2 :(得分:1)
要调用抽象类中具体类的函数,请使用 by lazy
,它允许调用非最终函数。
来自:area = extractArea(room);
至:area by lazy { extractArea(room) }
GL