Comment ?
Quand une variable est déclarée de type A, elle ne peut jamais contenir null. Quand elle est déclarée de type A?, elle peut être null.
Il est interdit d'appeler une fonction sur un type nullable ou d'affecter une valeur nullable à une variable qui ne l'est pas sans avoir explicité la marche à suivre si la valeur est nulle.
Nous verrons plus loin comment Kotlin vous propose de le faire élégamment dans le code.
Kotlin reprend ainsi la notation de C#, mais étendu à tous les types, références ou primitifs (int, double...). Alors que C# ne l'accepte que pour les types struct et primitifs.
Pourquoi ce choix est-il capital ?
Il y a plusieurs raisons qui rendent la NullPointerException quasi inévitable, peut-être la plus fréquente des exceptions en Java, et parfois assez délicate à analyser :
- elle peut survenir n'importe où : tout appel de fonction sur une variable est source d'exception
- vérifier le contenu de chaque variable ou retour de fonction systématiquement est illusoire et même néfaste pour la lisibilité du code
- lorsqu'elle survient, il n'y aucun paramètre ou information disponible à part le numéro de ligne dans le code source, et parfois par malchance on trouvera plusieurs appels de fonctions à cette ligne
Des solutions existent en Java, dont les annotations @Nullable et @NotNull et en Java 8 l'annotation @NonNull. Ces annotations peuvent être rajoutées pour protéger la valeur d'une variable, mais surtout les entrées/sorties d'une fonction, c'est à dire ses paramètres et son type de retour. Grâce à ces annotations, l'environnement de développement peut détecter certains appels dangereux, et le compilateur peut aussi rajouter des tests automatiques pour lever l'exception de nullité le plus tôt possible.
Le défaut est que ce ne sont que des annotations, donc non obligatoires. En pratique, les librairies ne l'utilisent pas toutes. Et pour avoir essayé de me l'imposer dans mon propre code, on oublie vite les bonnes résolutions lorsque le rythme de développement s'accélère. L'autre défaut est d'alourdir considérablement le code.
Scala, l'alternative principale à Java sur JVM, n'apporte aucune solution, à part de ne plus utiliser de null dans son code, et d'encapsuler les valeurs nullable dans des instances de la classe Option. Je n'ai personnellement pas assez codé en Scala pour être catégorique, mais je n'y vois pas une solution pratique : le code qui gère les références nullable est du coup bien verbeux, et il faudrait en théorie protéger tout ce qui vient d'une libraire extérieure.
Une solution intégrée au langage était nécessaire, mais il fallait la rendre plaisante à utiliser.
La syntaxe dans le détail
val a: String = "hello world !"val l: String = a.substring(0, 6)
Notez que la syntaxe simple par défaut est réservée au cas majoritaire des variables non nullables, dont l'usage est ainsi fortement encouragé.
Maintenant lorsque la variable est nullable, il suffit de rajouter un ? à la fin du type :
val a: String? = f()
Comme annoncé plus haut, il est alors nécessaire de spécifier ce qu'il faut faire quand la variable contient null :
- Pour appeler une fonction
a?.substring(0, 6) retournera simplement null si a est null, c'est donc une expression de type String?
- Pour convertir en type non nullable
a ?: "bye" retournera "bye" si a est null, et a autrement, c'est donc une expression de type String
- Et un combiné des deux est possible
a?.substring(0, 6) ?: "bye" retournera "bye" si a est null. c'est donc une expression de type String (non nullable)
- L'expression après le ?: peut lever une exception
a?.substring(0, 6) ?: throw Exception()
Dans ce cas, l'exception pourra être typée, enrichie avec des paramètres donc plus utile.
Je crois qu'il est difficile de faire plus concis.
En revanche un appel direct à a.substring(0, 6) sera clairement rejeté à la compilation.
Il est bien entendu possible de tester explicitement la nullité avec les opérateurs == et !=. Dans ce cas, la variable sera considérée comme non nulle dans le code exécuté après le test !
C'est à dire que l'on peut écrire :
C'est à dire que l'on peut écrire :
val a: String? = f()
if (a != null){
val l: String = a.substring(0, 6)
}
La variable a change automatiquement de type à l'intérieur du if, et évite ainsi de déclarer une variable supplémentaire.
La gestion des références nullable dans Kotlin est :
Conclusion
- uniforme, car elle est utilisable pour tous les types y compris les types simples comme Int
- fiable, car elle est obligatoire
- pratique, car le cas majoritaire (non null) est la forme la plus concise, des opérateurs nouveaux permettent de gérer facilement les valeurs null, et enfin les variables deviennent automatiquement non nullable après un test.