Dans Kotlin, il vous avertit lors de l'appel d'une fonction abstraite dans un constructeur, citant le code problématique suivant:
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)
}
Et la sortie est logique car lorsque calculate
est appelée, x
n'a pas encore été initialisée.
C'est quelque chose que je n'avais jamais envisagé lors de l'écriture de Java, car j'ai utilisé ce modèle sans aucun problème:
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
}
}
Et cela a bien fonctionné car les fonctions surchargées extractArea
ne reposent sur aucune donnée non initialisée, mais elles sont uniques à chaque class
dérivée respective (d'où la nécessité d'être abstraite). Cela fonctionne également dans kotlin, mais il donne toujours l'avertissement.
Est-ce donc une mauvaise pratique en Java/kotlin? Si oui, comment puis-je l'améliorer? Et est-il possible d'implémenter dans kotlin sans être averti de l'utilisation de fonctions non finales dans les constructeurs?
Une solution potentielle consiste à déplacer la ligne area = extractArea()
vers chaque constructeur dérivé, mais cela ne semble pas idéal car il s'agit simplement de code répété qui devrait faire partie de la super classe.
L'ordre d'initialisation d'une classe dérivée est décrit dans la référence du langage: Ordre d'initialisation de la classe dérivée , et la section explique également pourquoi il est mauvais (et potentiellement dangereux) d'utiliser un membre ouvert dans la logique d'initialisation de votre classe.
Fondamentalement, au moment où un constructeur de super classe (y compris ses initialiseurs de propriété et les blocs init
) est exécuté, le constructeur de classe dérivé ne s'est pas encore exécuté. Mais les membres remplacés conservent leur logique même lorsqu'ils sont appelés depuis le constructeur de la super classe. Cela peut conduire à un membre surchargé qui s'appuie sur un état spécifique à la classe dérivée, appelé à partir du super constructeur, ce qui peut entraîner un bogue ou une défaillance d'exécution. C'est également l'un des cas où vous pouvez obtenir un NullPointerException
dans Kotlin.
Considérez cet exemple de code:
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
}
Ici, le size
surchargé repose sur l'initialisation correcte de items
, mais au moment où size
est utilisé dans le super constructeur, le champ de support de items
est toujours nul. La construction d'une instance de Derived
lance donc un NPE.
L'utilisation de la pratique en question en toute sécurité nécessite des efforts considérables, même lorsque vous ne partagez pas le code avec quelqu'un d'autre, et lorsque vous le faites, les autres programmeurs s'attendent généralement à ce que les membres ouverts soient sûrs de remplacer en impliquant l'état de la classe dérivée.
Comme @ Bob Dagleish correctement noté, vous pouvez utiliser initialisation paresseuse pour la propriété code
:
val code by lazy { calculate() }
Mais alors vous devez être prudent et ne pas utiliser code
ailleurs dans la logique de construction de la classe de base.
Une autre option consiste à exiger que code
soit passé au constructeur de la classe de base:
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
}
}
Cependant, cela complique le code des classes dérivées dans les cas où la même logique est utilisée à la fois dans les membres substitués et pour le calcul des valeurs transmises au super constructeur.
C'est définitivement une mauvaise pratique car vous invoquez calculate()
sur un objet partiellement construit. Cela suggère que votre classe a plusieurs phases d'initialisation.
Si le résultat de calculation()
est utilisé pour initialiser un membre, ou effectuer une mise en page ou quelque chose, vous pouvez envisager d'utiliser l'initialisation paresseuse . Cela retardera le calcul du résultat jusqu'à ce que le résultat soit vraiment nécessaire.