Tutoriel sur le langage Kotlin

Les bases de la syntaxe (partie 2/4)

Laurent Bernabe

Dans cet article en plusieurs parties, vous allez être initié aux bases du langage Kotlin.

Il vise principalement les développeurs étant familiers avec le langage Java et la POO : même si tout langage orienté objet devrait convenir. Je fais de temps en temps référence à Java pour essayer d’aider à visualiser certains concepts.

La version de Kotlin utilisée lors de l’écriture de l’article était la version 1.3.70.

Voici les différentes parties :

Introduction rapide

Bases de la syntaxe

Fonctions et notions sur les classes

Notions avancées sur les classes et diverses fonctionnalités

Cette deuxième partie présente les bases syntaxiques du langage.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum. Commentez

- Titre : Tutoriel sur le langage Kotlin

- Auteur : Laurent Bernabe

- Parution : 18 juin 2020

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2020 Laurent Bernabé. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Les commentaires

- II. Déclarations de variables

- III. Types de base

- IV. Les collections natives

- IV-A. Les tableaux

- IV-B. Les listes

- IV-C. Les tables associatives

- IV-D. Les Sets

- V. Les imports / espaces de nommage

- VI. Interpolation de chaînes

- VII. Inférence de type

- VIII. Expressions

- IX. Tuples et déconstruction

- X. Structures de contrôle

- X-A. L’instruction while

- X-B. L’instruction if

- X-C. L’instruction for

- X-D. L’instruction when

- X-D-1. Cas ultra simple

- X-D-2. Test en fonction d’une variable en entrée

- X-D-3. Pas d’obligation de tester des constantes

- X-D-4. Exécution de bloc

- X-D-5. Regrouper plusieurs valeurs de test

- X-D-6. Test du type de données

- XI. La conversion de type

- XI-A. La conversion classique

- XI-B. Le SmartCast

- XI-C. La conversion de types numériques

- XII. Usage sûr de null

- XIII. Conclusion et remerciements

I - Les commentaires

Les commentaires fonctionnent comme en Java

// Commentaire en une ligne

/*
Commentaire sur plusieurs lignes
*/

II - Déclarations de variables

En Kotlin, on distingue les variables dont on peut changer la valeur, et les variables immuables qui ne sont pas tout à fait des constantes :

- le mot-clé val permet de déclarer une variable immuable ;

- le mot-clé var permet de déclarer une variable altérable.

val nom:String = "Toto"
// nom = "Dodo" // interdit !!! Car nom a été déclaré avec val
var age = 10
age += 12 // aucun problème, car âge est altérable.

Pour rappel, il faut préciser le type de variable après le nom, et pas avant comme en Java.

Évidemment, une variable immuable doit être initialisée lors de sa déclaration.

Cela fonctionne aussi bien pour les types « primitifs » (tout est objet en Kotlin) que pour les types personnels.

class Personne(val nom: String, var age: Int = 10)

val jean:Personne = Personne("Jean", 25)

Même si les classes en Kotlin seront expliquées ultérieurement, sachez qu’ici la classe Personne dispose d’une propriété « nom » en lecture seule, et d’une propriété « age » en lecture-écriture, dont la valeur par défaut est 10. Notez bien cependant qu’ici, la classe Personne n’a pas défini de méthode modificatrice, mais s’il y en avait eu, la variable jean aurait probablement pu voir son état modifié, bien qu’ayant été déclarée comme val ! Ici, le mot-clé val empêche seulement de changer l’objet pointé par jean. De plus, on n’utilise pas de mot-clé « new », contrairement au Java.

III - Types de base

Les types numériques Byte, Short, Int, Long, Float, et Double sont équivalents aux types existants en Java : si ce n’est que, pour rappel, en Kotlin, tout est objet. De surcroît, Kotlin dispose d’un mécanisme dont nous reparlerons ultérieurement et qui permet « d’ajouter » des méthodes aux classes existantes (y compris nos classes personnelles et les classes qui nous sont proposées par les bibliothèques). Ainsi on peut écrire :

2.toString()
10.downTo(0) // génère le range (c’est-à-dire l’intervalle) décroissant de 10 a 0.

Le type Boolean fonctionne aussi comme en Java :

val moinsDe18 = age < 18
val estLucas = nom == "Lucas"
val trouve = moinsDe18 && estLucas

Le type String dispose des mêmes méthodes qu’en Java, mais aussi de méthodes supplémentaires (liste complète ici). Ainsi on peut écrire :

println("martin".all{it.isLowerCase()}) // teste si le mot est entièrement en minuscules

La méthode all, qui permet de tester si tous les éléments d’une collection remplissent un critère, n’existe pas en Java : c’est une extension du langage Kotlin. Le morceau de code constitué par les accolades est une fonction anonyme (ou lambda) ; il en sera question ultérieurement.

Kotlin ajoute aussi le type Range (intervalle), très utile.

On peut notamment exécuter une boucle grâce à un objet Range (notez que la syntaxe basique Java pour effectuer une boucle n’existe pas en Kotlin) :

for (i in 3..7){
println(i)
} // affiche les chiffres de 3 à 7 sur plusieurs lignes.

Mais on peut aussi obtenir un intervalle décroissant avec downTo :

for (i in 10 downTo 0) {
println(i)
} // compte à rebours de 10 a 0, sur plusieurs lignes.

Il est aussi possible de préciser une progression autre que 1 :

for (i in 2..36 step 3) println(i) // affiche les nombres 2, 5, 8,..., 35

Il est également possible de combiner un intervalle décroissant et une progression définie :

for (i in 36 downTo 2 step 3) println(i) // affiche les nombres 36, 33,..., 3

Nous reviendrons plus loin dans ce chapitre sur la boucle for.

Sachez aussi que, comme nous le verrons plus tard avec les fonctions, le type void n’existe pas en Kotlin. On utilise à la place le type Unit (qui ne s’applique pas qu’aux fonctions).

N’oubliez pas que vous pouvez vous rendre compte des méthodes disponibles pour un objet donné dans le playground Kotlin grâce à la commande CTRL+ESPACE après avoir saisi le point accolé à l’objet.

Nous reviendrons sur les tableaux (array) ainsi que sur les tables d’associations (map) dans la prochaine section.

IV - Les collections natives

En Kotlin, il n’y a pas besoin d’importer quoi que ce soit pour utiliser les collections. De plus, grâce au mécanisme de fonctions d’extension (que nous verrons lors du prochain chapitre sur les classes), les collections bénéficient de plusieurs méthodes supplémentaires par rapport à leurs équivalents Java.

IV-A - Les tableaux

Tout d’abord, en Kotlin, les tableaux sont représentés par la classe générique Array : ainsi, on peut déclarer par exemple un Array<Int>, un Array<String>, voire un Array<Personne> (type personnel défini précédemment dans ce chapitre). Évidemment, un Array ne peut contenir que des valeurs d’un même type.

On peut construire un Array de deux manières différentes : soit par le constructeur Array, soit par la fonction arrayOf. La fonction arrayOf est plus proche de ce que l’on connaît en Java, tandis que le constructeur Array permet de bénéficier de plus de contrôle sur l’initialisation :

val tableau1 = arrayOf(2,10,-1,4,9)
val tableau2 = Array(10, { i -> i * 2 }) // utilisation d’une fonction lambda (fonction anonyme)
assert(tableau1[1] == 10) // on accède aux éléments comme en Java
tableau1[0] = 170
assert(tableau1[0] == 170) // de même pour la modification

Ici, j’utilise l’instruction assert dans un but purement illustratif : si l’expression passée en paramètre est fausse, une exception est lancée, sinon, il ne se passe rien de nouveau. C’est un moyen simple de montrer la valeur de la variable ainsi testée au lecteur.

Plusieurs remarques au sujet de l’initialisation via le constructeur :

- le premier paramètre décrit le nombre d’éléments du tableau ;

- le deuxième est une fonction qui prend l’index de l’élément à initialiser et qui retourne une valeur. J’ai utilisé une fonction anonyme pour la clarté du code, mais j’aurais aussi pu utiliser une fonction régulière. Même si nous verrons les fonctions ultérieurement, vous pouvez également remarquer qu’il s’agit d’une situation où l’on passe une fonction à une fonction (ici, la fonction anonyme au constructeur).

Rien ne nous empêche de déclarer des tableaux multidimensionnels, que ce soit par le biais du constructeur ou de la fonction arrayOf :

val tableau1 = arrayOf(arrayOf(1,2,3), arrayOf(4,5))
val tableau2 = Array(3, {i -> Array(3, {j -> i*j})})

Évidemment, les tableaux gardent une taille fixe.

Les tableaux disposent aussi de fonctions supplémentaires par rapport à leur version Java. Parmi la multitude de fonctions, citons :

val tab = arrayOf(7,10,15,3,6,9,12)
tab.sum()
tab.sort()
tab.sorted() // idem que sort() mais se contente de retourner le resultat au lieu de modifier le tableau contenu dans tab
tab.reverse()
tab.reversed()
tab.min()
tab.max()
tab.first()
tab.last()
tab.take(3) // les 3 premiers éléments du tableau sans le modifier
tab.drop(3) // le tableau sans ses 3 premiers éléments sans le modifier

IV-B - Les listes

Les listes sont similaires aux tableaux, mais sont plus puissantes, du moins dans leur version modifiable. En effet, comme pour d’autres types de collections, il y a les listes immuables (non modifiables) et les listes modifiables. Tout dépend de la manière dont elles sont déclarées.

val maListeNonModifiable = listOf(1,2,3)
// interdit !!! maListeNonModifiable[2] = 6;

val maListeModifiable = mutableListOf(1,2,3)
maListeModifiable[2] = 6;
maListeModifiable += 10; // équivalent a maListeModifiable.add(10)

print(maListeModifiable) // => [1, 2, 6, 10]

Ainsi :

- on peut créer une liste immuable grâce à la fonction listOf(), et créer une liste modifiable par l’intermédiaire de la fonction mutableListOf(). Ces deux méthodes sont par ailleurs génériques, mais l’inférence de type nous permet d’éviter d’en déclarer le type ;

- vous pouvez constater que l’on ne peut pas modifier un élément d’un objet List, contrairement à un objet MutableList ;

- on peut facilement ajouter des éléments aux listes modifiables.

Pourquoi aurait-on alors intérêt à utiliser les listes non modifiables ? Par exemple, pour la création d’algorithmes. Et cela grâce à plusieurs mécanismes de la programmation fonctionnelle, rendus plus faciles avec l’immutabilité.

Parmi ces mécanismes, on peut citer la déconstruction: c’est-à-dire la reconnaissance d’une expression en tant que « schéma » et l’attribution dans des variables. Le tout étant effectué simultanément.

Nous reparlerons du mécanisme de déconstruction ultérieurement dans ce tutoriel.

Par ailleurs, rien ne nous empêche d’écrire ce qui suit, générant ainsi une liste immuable à partir d’une autre.

val maListeNonModifiable = listOf(1,2,3)
val maListe2 = maListeNonModifiable + 5 // maListeNonModifiable reste telle quelle.

Le type MutableList étant dérivé du type List, ils ont de nombreuses méthodes communes qui peuvent s’avérer très utiles. On peut notamment citer :

val maListeImmuable = listOf(1,2,3,4,5)
maListeImmuable.isEmpty() // est-elle vide ?
maListeImmuable.contains(2) // le chiffre 2 y figure-t-il ?
maListeImmuable.all({it % 2 == 0}) // ne contient que des chiffres pairs ?
maListeImmuable.chunked(2) // => [[1, 2], [3, 4], [5]]
maListeImmuable.count({it % 2 == 0}) // combien de chiffres pairs a-t-elle ?
maListeImmuable.take(3) // retourne une liste avec les 3 premiers éléments
maListeImmuable.drop(2) // retourne une liste sans les 2 premiers éléments

Certaines lignes du code précédent utilisent les lambdas, des fonctions anonymes que nous verrons ultérieurement.

N’hésitez pas à aller consulter les méthodes disponibles pour List et MutableList.

IV-C - Les tables associatives

Kotlin supporte aussi les tables associatives : des collections qui associent leurs valeurs à des clés, appelées Map en anglais.

Comme pour les listes, il y a les tables associatives immuables (Map) et les tables associatives modifiables (MutableMap), et on peut les instancier grâce aux fonctions mapOf() et mutableMapOf()

val monDictionnaireImmuable = mapOf("Sandra" to 27, "Alex" to 10)
val monDictionnaireModifiable = mutableMapOf("Sandra" to "0102030405", "Alex" to "0104050607")

monDictionnaireModifiable += ("Beatrice" to "0809101112")
monDictionnaireModifiable["Sandra"] = "0802030405"
val monDictionnaireImmuableEnrichi = monDictionnaireImmuable + ("Beatrice" to 30)

Elles disposent elles aussi de nombreuses méthodes qui peuvent vous simplifier le développement :

val monDictionnaireImmuable = mapOf("Sandra" to 27, "Alex" to 10)

monDictionnaireImmuable.getOrDefault("Beatrice", 33) // retourne l’âge de Beatrice ou 33 si elle ne figure pas dans la table
monDictionnaireImmuable.all({ it.value > 18 }) // indique si toutes les personnes de la table sont majeures
monDictionnaireImmuable.any({ it.key == "Sandra" }) // Sandra est-elle dans la table ?
monDictionnaireImmuable.contains("Sandra") // idem
monDictionnaireImmuable.filter({ it.value > 18 }) // retourne une table ne gardant que les personnes majeures (ne modifie pas la table originale)
monDictionnaireImmuable.map({ it.value - 10 }) // retourne la liste des âges des personnes, auxquels on aura retranché 10
monDictionnaireImmuable.count() // le nombre d’associations de la table

Encore une fois, n’hésitez pas à aller consulter les documentations officielles de ces deux classes (voir les liens que je vous ai donnés plus haut).

IV-D - Les Sets

L’utilisation des Sets est elle aussi intuitive :

val mesPiecesCapturees = setOf("Cavalier", "Dame", "Fou")
val mesPiecesCaptureesV2 = mesPiecesCapturees - "Dame"

val mesComics = mutableSetOf("Spiderman", "Batman", "Superman")
mesComics += "Green Lantern"

Vous pouvez là aussi retrouver les API pour les classes Set (immuable) et MutableSet (modifiable). Quelques méthodes parmi tant d’autres :

val mesPiecesCapturees = setOf("Cavalier", "Dame", "Fou")
mesPiecesCapturees.count() // nombre d’éléments
mesPiecesCapturees.filter{it.startsWith("C")} // filtre ne retenant que les éléments commençant par « C »;
mesPiecesCapturees.map{it.toLowerCase()} // set où les éléments ont été convertis en minuscules
mesPiecesCapturees.isEmpty() // est-il vide ?
mesPiecesCapturees.isNotEmpty() // a-t-il des éléments ?
mesPiecesCapturees.maxBy{it.length} // quel élément est le plus long ?

V - Les imports / espaces de nommage

Les imports et espaces de nommage fonctionnent dans l’ensemble comme en Java

package com.monprojet.test // définit un espace de nommage

import com.projetimporte.* // importe tout le paquetage
import com.projet2.classe1 as cls1 // importe une classe et lui attribue un alias

À noter :

- un espace de nommage n’a pas pour obligation de respecter le chemin du fichier : cela peut être n’importe quelle valeur acceptable,

- il n’y a pas d’import statique en Kotlin : il faut simplement utiliser la syntaxe standard d’import sur l’objet en question (car – nous le verrons plus tard – il n’y a pas de méthode statique proprement dite en Kotlin, mais une classe de type Singleton, c’est-à-dire une classe de type Object),

- et il est possible d’importer des fonctions globales (ne faisant partie d’aucune classe).

Kotlin importe déjà automatiquement les paquetages suivants :

- kotlin.*

- kotlin.annotation.*

- kotlin.collections.*

- kotlin.comparisons.*

- kotlin.io.*

- kotlin.ranges.*

- kotlin.sequences.*

- kotlin.text.*

- java.lang.* (seulement sur la version JVM de Kotlin)

- kotlin.jvm.* (même remarque)

VI - Interpolation de chaînes

Cette fonctionnalité nous permet de concaténer des chaînes plus facilement. Si en Java nous voulons écrire une séquence du type :

int a = 10;
int b = 6;
String c = "abc";
System.out.println("[" + a + ", " + b*2 + ", " + c + "]");

En Kotlin, on pourra simplement écrire :

val a = 10
val b = 6
val c = "abc"
println("[$a, ${b*2}, $c]")

Ainsi :

- On utilise qu’une seule chaîne de caractères ;

- $a permet de substituer la valeur de la variable a dans la chaîne, quel que soit son type (souvenez-vous qu’en Kotlin, tout est objet) ;

- ${b*2} s’écrit avec accolades étant donné qu’il s’agit d’une expression, et non plus d’une simple référence à une variable.

VII - Inférence de type

Dans de nombreuses situations, le compilateur Kotlin est capable de reconnaître le type d’une variable grâce à l’expression qui a servi à l’initialiser :

val age = 10 // age est déclaré comme Int
val nom = "Toto" // nom est déclaré comme String
var prix = 10.2 // prix est déclaré comme Double

Il va de soi que si une variable n’est pas initialisée de suite (donc de type var), il faut préciser son type, l’inférence de type ne pouvant s’appliquer dans ce cas.

Qui plus est, l’inférence de type fonctionne aussi avec les types complexes, qu’ils soient définis par Kotlin ou par nous-mêmes.

class Personne(val name: String)
val jean = Personne("Jean") // jean est de type Personne.

Nous verrons par la suite d’autres inférences de type.

VIII - Expressions

Toute instruction qui retourne une valeur constitue une expression : simple valeur littérale (3, « Toto »…), expression arithmétique ou booléenne, appel de fonction (même une fonction ne retournant aucune valeur exploitable, c’est-à-dire une fonction définie comme retournant Unit), déclaration d’une fonction anonyme… Ces expressions peuvent donc être utilisées pour initialiser des variables.

val message = if (âge < 18) "C'est un mineur !" else "Il est majeur." // se référer à la section suivante sur les structures de contrôle.

Certaines instructions ne retournent jamais de valeur. C’est le cas pour la boucle while, comme nous le verrons dans une prochaine section du chapitre :

val resultat = while(i < 10) {i += 1} // Strictement interdit !!!

Par ailleurs, les affectations de variables ne sont pas des expressions : il est donc impossible de les enchaîner au sein d’une même instruction :

var b = 0
val a = b = 10 // strictement interdit !!!

IX - Tuples et déconstruction

Kotlin dispose d’un support pour les tuples simples que sont les Paires (Pair) et Triples (Triple). Si un tableau ne peut contenir que des valeurs de types communs, les tuples permettent d’agréger plusieurs types de données : en Kotlin, on peut simplement utiliser les Pair et Triple pour combiner respectivement deux ou trois éléments. On accède alors simplement aux différentes composantes par l’intermédiaire des méthodes component1(), component2(), component3(), où component1() retourne la 1re composante du tuple.

val tuple1 = Pair('a', 25) // de type Pair<Char, Int>
val valeur1_1 = tuple1.component1()
val valeur1_2 = tuple1.component2()
assert(valeur1_1 == 'a')
assert(valeur1_2 == 25)

val tuple2 = Triple('a', 10, "Toto") // de type Triple<Char, Int, String>
val valeur2_1 = tuple2.component1()
val valeur2_2 = tuple2.component2()
val valeur2_3 = tuple2.component3()
assert(valeur2_1 == 'a')
assert(valeur2_2 == 10)
assert(valeur2_3 == "Toto")

Nous pouvons aussi accéder aux différentes composantes d’un tuple par le biais de ce que l’on appelle une déconstruction (destructuring) :

val monTuple = Triple('a', 10, "Toto")
val (a, b, c) = monTuple // déconstruction !
assert(a == 'a')
assert(b == 10)
assert(c == "Toto")

La déconstruction a lieu parce que l’on tente ici d’affecter non pas une simple variable, mais une structure qui dispose de plusieurs variables (la structure (a,b,c)) à une structure qui a la même forme (ici une agrégation de trois valeurs). Remarquez qu’il est interdit de déclarer val Triple(a,b,c) = Triple('a', 10, "Toto") : il faut simplement décrire la forme de la structure à gauche de l’affectation.

Sachez également qu’il est possible d’ignorer certaines valeurs lors de la déconstruction :

val (a, _ , c) = Triple('a', 10, "Toto")
assert (a == 'a')
assert (c == "Toto")

X - Structures de contrôle

Les structures de contrôle sont similaires à celles du java (if, for, while, switch…), à ceci près que la plupart des structures de Kotlin sont des expressions. Leurs résultats peuvent donc être affectés à des variables, ainsi que nous l’avons vu dans la section précédente. L’équivalent de la structure switch en Kotlin est également beaucoup plus simple d’utilisation et beaucoup plus puissant.

X-A - L’instruction while

L’instruction while (ou do… while) en Kotlin, tout comme en Java, ne retourne pas d’expression. Elle s’utilise aussi comme en Java : rien de nouveau.

var i = 0
while (i < 10) {
i += 1
println(i)
}

var j = 0
do {
j += 1
println(j)
} while (j < 10)

Les mots-clés break et continue fonctionnent comme en Java.

X-B - L’instruction if

L’instruction if peut être utilisée aussi bien en tant que structure de contrôle traditionnelle qu’en tant qu’expression. Voici les deux cas de figure en situation :

if (1<2) {
println("ok")
}
else if (2 != 2) {
println("ko 1")
}
else {
println("ko 2")
}

Ça, c’est le cas de figure que nous connaissions déjà.

L’autre cas, c’est l’équivalent de l’opérateur ternaire de Java, sauf que cet opérateur n’existe pas en Kotlin, il a été remplacé par l’expression if-else.

val couleur = if (caseBlanche) "white" else "black"

Ceci nous évite de créer une variable temporaire et permet de créer directement une valeur immuable.

X-C - L’instruction for

L’instruction for, quant à elle, ne peut pas être utilisée en tant qu’expression.

L’utilisation de l’instruction for telle qu’on la connaît en Java (initialisation/condition/mise à jour accompagnée d’un bloc d’instructions) est strictement interdite en Kotlin. En effet, on ne peut utiliser la boucle for que sur des objets « iterable » : traditionnellement des ranges ou certains types de collections. Par « iterable », je veux simplement parler de types pour lesquels on peut obtenir les différentes valeurs d’une variable de ce type par appels successifs à la méthode d’extraction (appelons-la next() pour mieux illustrer).

Ce qui peut par exemple donner :

// i n’existe pas dans cette portée
for (i in 0..10) println(i)

Remarquez bien ici qu’il ne faut utiliser ni le mot-clé val, ni var, afin de déclarer la variable i : peu importe qu’elle existe déjà ou non, il ne faut surtout pas les utiliser.

Un autre exemple avec un simple tableau :

val monTableau = arrayOf(2,3,5,7,11,13)
for (premier in monTableau) {
println("$premier est un nombre premier")
}

Remarquez aussi que je peux m’affranchir des accolades si je n’ai besoin que d’une simple expression pour la boucle for (ce qui est donc possible dans ce deuxième exemple).

Enfin, si l’on souhaite parcourir un tableau tout en conservant les valeurs des différents index, on peut utilise la méthode withIndex() mais avec une syntaxe un peu particulière pour les variables d’index et de valeur :

val monTableau = arrayOf(2,3,5,7,11,13)
for ((index, valeur) in monTableau.withIndex()) {
println("$index: $valeur")
}

Remarquez que la méthode withIndex() retourne une Pair où la première valeur est l’index, donc il faut empaqueter les variables d’index et de valeur entre parenthèses afin de procéder au mécanisme de déconstruction. Mais rien ne nous empêche de procéder en deux étapes :

val monTableau = arrayOf(2,3,5,7,11,13)
for (tupleCourant in monTableau.withIndex()) {
val index = tupleCourant.component1()
val valeur = tupleCourant.component2()
println("$index: $valeur")
}

Bien évidemment, l’instruction for permet aussi de parcourir les tables associatives

val monRepertoireTel = mapOf("Bea" to "0123456789", "Lily" to "08123456", "Max" to "06123456")

for (entree in monRepertoireTel) {
println("${entree.key} => ${entree.value}")
}

// Version utilisant la déconstruction
for ((nom, numero) in monRepertoireTel) {
println("$nom => $numero")
}

Tout comme pour les boucles while (do…while), les mots-clés break et continue fonctionnent comme en Java.

X-D - L’instruction when

L’instruction est une instruction ultra puissante, basée sur une fonctionnalité communément appelée le « pattern-matching » dans un certain nombre de langages. C’est un concept plus ou moins lié au mécanisme de déconstruction que nous avons vu auparavant. D’ailleurs, avant de vous expliquer comment elle fonctionne et à quoi elle peut être utile, je tiens à vous préciser qu’en plus de sa puissance et sa flexibilité, l’instruction when est aussi une expression.

X-D-1 - Cas ultra simple

Pour commencer, un cas ultra simple : tester le signe d’un nombre

fun main() {
val a = 10;
var signeDeA: Int;
when {
a < 0 -> println("negatif")
a == 0 -> println("nul")
else -> println("positif")
}
}

Ici, nous créons d’abord la variable évolutive signeDeA sans lui affecter de valeur, auquel cas il faut obligatoirement préciser un type.

Puis nous effectuons une instruction conditionnelle : ici, la structure when parcourt chacun de ses tests dans l’ordre et s’interrompt (éventuellement) dès que l’un des tests correspond, auquel cas elle exécute auparavant le code associé au test. Le test est situé avant la flèche (signe moins combiné à un signe supérieur), et le code associé après. Remarquez qu’il n’y a pas d’instruction break dans les instructions when !

Évidemment, dans ce cas de figure, c’est l’instruction else — qui correspond à une branche default du switch de java — qui est exécutée. En effet, tous les tests précédents ont échoué.

D’ailleurs cette instruction else n’est pas obligatoire, nous aurions pu négliger le cas du signe positif :

fun main() {
val a = 10;
var signeDeA: Int;
when {
a < 0 -> println("negatif")
a == 0 -> println("nul")
}
}

Et pour être complet sur cet exemple, on peut inverser les trois tests, afin de bien se rendre compte que l’instruction when s’interrompt dès que c’est nécessaire.

fun main() {
val a = 10;
var signeDeA: Int;
when {
a > 0 -> println("positif")
a == 0 -> println("nul")
else -> println("negatif")
}
}

X-D-2 - Test en fonction d’une variable en entrée

On peut aussi baser les tests de l’instruction when sur une seule variable :

fun main() {
val de = java.util.Random()
val valeurDe = de.nextInt(6) + 1
val texteValeur = when (valeurDe)
{
1 -> "Un"
2 -> "Deux"
3 -> "Trois"
4 -> "Quatre"
5 -> "Cinq"
else -> "Six"
}
println(texteValeur);
}

Plusieurs remarques :

1. On vient de fournir la variable valeurDe à l’instruction when, ce qui aura pour conséquence que dans chaque test de l’instruction, valeurDe sera comparée a l’expression du test.

2. Ici, comme nous passons une variable à l’instruction when, celle-ci doit être testée de manière exhaustive. Ce qui est facile avec les énumérations (un type personnalisé avec un nombre de valeurs défini, mais nous en reparlerons plus tard), mais pour un type numérique comme le type Int, il faudra obligatoirement fournir une branche else.

3. Nous avons décidé d’utiliser l’instruction when en tant qu’expression et nous en affectons le résultat à la variable texteValeur.

On aurait aussi pu se passer de créer la variable texteValeur :

val de = java.util.Random()
val valeurDe = de.nextInt(6) + 1
println(when(valeurDe) {
1 -> "Un"
2 -> "Deux"
3 -> "Trois"
4 -> "Quatre"
5 -> "Cinq"
else -> "Six"
})

X-D-3 - Pas d’obligation de tester des constantes

Ajoutons au code précédant un texte à peine plus complexe (nous reviendrons ultérieurement sur la création de fonctions) :

1. fun estPaire(texteValeur: String): Boolean {

2. return when (texteValeur) {

3. "Deux", "Quatre", "Six" -> true

4. else -> false

5. }

6. }

8. fun main(args: Array<String>) {

9. val de = java.util.Random()

10. val valeurDe = de.nextInt(6) + 1

11. val texteValeur = when(valeurDe) {

12. 1 -> "Un"

13. 2 -> "Deux"

14. 3 -> "Trois"

15. 4 -> "Quatre"

16. 5 -> "Cinq"

17. else -> "Six"

18. }

19. println(texteValeur)

21. println(when {

22. estPaire(texteValeur) -> "Paire"

23. else -> "Impaire"

24. })

25. }

La fonction estPaire indique si, pour les six valeurs textes du résultat d’un tirage, la valeur représentée est paire. Nous nous en servons donc pour savoir si la valeur de texteValeur est paire (ligne 21).

X-D-4 - Exécution de bloc

Jusqu’ici, nous n’avons retourné que de simples expressions pour chaque test de l’instruction when. En fait, nous pouvons faire plus que cela :

when (ageGuillaume) {
estMineur(ageGuillaume) -> {
println("Desole, le droit de vote est interdit aux mineurs")
repousserGuillaumeALEntree()
sermonerGuillaume()
}
else -> {
println("Voici les bulletins de vote")
indiquerIsoloirAGuillaume()
}
}

X-D-5 - Regrouper plusieurs valeurs de test

Nous avons vu auparavant que l’instruction break ne peut pas être utilisée avec l’instruction when, et que sur un test réussi, son instruction/expression est traitée et c’est la fin de la structure. Donc si l’on veut tester plusieurs valeurs, ceci — qui était autorisé avec un switch en java — est strictement interdit :

switch (a) {
case 2:
case 3:
case 5:
case 6: println("2,3,5 ou 6");
default: println("autre");
}

On écrira plutôt :

when (a) {
2, 3, 5, 6 -> println("2,3,5 ou 6")
else -> "autre"
}

On peut même tester avec les intervalles de valeurs que nous avons déjà vus auparavant :

when (age) {
in 13..19 -> "adolescent"
else -> "autre"
}

Remarquez l’utilisation du mot-clé in. On peut aussi inverser le test en écrivant !in début..fin.

X-D-6 - Test du type de données

Grâce à l’instruction when, il devient facile de tester le type de donnée d’une variable :

when (age) {
is String -> "Il faut traiter la valeur auparavant"
is Int -> "Ok"
else -> " Impossible d'exploiter cette valeur"
}

Mais aussi, grâce au SmartCast, fonctionnalité dont nous reparlerons sous peu, il n’est pas nécessaire d’effectuer de casting dans les instructions associées aux tests.

when (age) {
is String -> println(age.length) // age est un String donc la propriété length existe
is Int -> println(age.dec()) // affiche age - 1
else -> println("Impossible d'exploiter cette valeur")

XI - La conversion de type

XI-A - La conversion classique

On peut utiliser les mots-clés is et !is pour respectivement tester si un type est ou n’est pas d’un type donné.

if (obj is String)
{
// faire quelque chose
}

if (a!is Int)
{
// faire quelque chose
}

Le mot-clé as permet d’effectuer une conversion de type.

val s:String = valeur as String

Mais si valeur est null, cela lèvera une exception. En effet, le type String ne peut pas prendre la valeur null, contrairement au type String? ; nous en reparlerons sous peu, ainsi que du mot-clé as?)

XI-B - Le SmartCast

Tout d’abord, examinez le code suivant :

if (figureCourante is Cercle)
{
println(figureCourante.rayon)
}
else if (figureCourante is Rectangle)
{
println(figureCourante.largeur)
}

Admettons qu’au préalable, son auteur ait pris soin de définir une hiérarchie de figures (classe Figure), ainsi qu’une classe fille Cercle qui dispose de la propriété rayon, et une classe fille Rectangle qui n’en dispose pas. De même, la classe fille Rectangle dispose de la propriété largeur et pas la classe Cercle. Et donc la classe Figure ne dispose d’aucune de ces deux propriétés.

Ce qui peut choquer de prime abord avec le code ci-dessus, c’est que malgré le fait que figureCourante s’apparente à une instance de Figure, il n’y a pas de cast avant d’accéder aux propriétés rayon (de la classe fille Cercle) et largeur (de la classe fille Rectangle). Mais juste deux branches if-else.

Et pourtant, il s’agit bien d’une fonctionnalité de Kotlin :

- dans chaque test, il est sûr que la variable est d’un type donné

- donc inutile d’y effectuer un cast : il est assez intelligent pour réduire le champ des possibles à l’intérieur du if.

Qui plus est, nous avons vu plus haut une façon bien plus élégante de réaliser cela : when !

when (figureCourante) {
is Cercle -> println(figureCourante.rayon)
is Rectangle -> println(figureCourante.largeur)
else -> println("Echec d'exploitation de la figure")
}

XI-C - La conversion de types numériques

En Kotlin, il n’y a pas de conversion automatique des types numériques. Si l’on veut convertir un Int en Long, il faut passer par une méthode prévue à cet effet :

val a = 10.0 // a est un Double
val b = a.toLong()

val c = 10.0 as Int // la conversion automatique entre types numériques n’existe pas: une ClassCastException sera levée
val d = 10 as Long // de même

Les méthodes s’appellent simplement toInt(), toDouble(), toLong()… et font partie de l’API standard de Kotlin. (Vous pouvez toujours y accéder depuis le site officiel.)

XII - Usage sûr de null

En Kotlin, pour un type donné, il existe deux variantes :

- une qui n’acceptera jamais d’être associée à la valeur null,

- une autre qui le permettra

Par exemple : une variable de type Int ne vaudra jamais null, mais une variable de type Int ? oui.

Cela a donc une conséquence directe : il est interdit d’attribuer une valeur de type Int ? à une valeur de type Int, à moins de la convertir auparavant dans le cas où elle vaudrait null.

On peut utiliser un smartCast pour cela :

val a:Int? = null
val b:Int = when (a) {
null -> 0
else -> a
}

println(b)

Mais on peut aussi utiliser l’opérateur Elvis :

val a:Int? = null
val b:Int = a ?: 0

println(b)

Ainsi la variable b vaudra 0 si a est null, et la valeur de a sinon.

Enfin, il est possible de convertir une valeur « nullable » en une autre (équivalente bien sûr) grâce au mot-clé as?

val s:String? = valeur as? String

Ainsi, si valeur vaut null, s vaudra également null. On aurait pu utiliser le mot-clé as, mais une valeur null aurait alors levé une exception.

XIII - Conclusion et remerciements

Dans cette section, nous avons vu les bases de la syntaxe en Kotlin.

Dans la section suivante, nous verrons les fonctions ainsi que les bases des classes.

Je tiens à remercier -FloT- pour sa relecture orthographique et Mickael Baron pour sa relecture technique.