Cet atelier de programmation fait partie du cours Kotlin Bootcamp for Programmers (Formation Kotlin pour les programmeurs). Vous tirerez le meilleur parti de ce cours si vous suivez les ateliers de programmation dans l'ordre. En fonction de vos connaissances, vous pouvez parcourir certaines sections. Ce cours s'adresse aux programmeurs qui connaissent un langage orienté objet et souhaitent apprendre l'Kotlin.
Introduction
Dans cet atelier de programmation, vous avez découvert des classes, des fonctions et des méthodes génériques, et comment elles fonctionnent en langage Kotlin.
Plutôt que de créer un exemple d'application, les leçons de ce cours sont conçues pour développer vos connaissances, mais elles ne doivent pas être indépendantes les unes des autres pour vous permettre de parcourir les sections que vous connaissez déjà. Pour les assembler, de nombreux exemples utilisent un thème d'aquarium. Et si vous voulez voir l'histoire complète de l'aquarium, consultez le cours Udacity Kotlin Bootcamp for Programmers.
Ce que vous devez déjà savoir
- Syntaxe des fonctions, classes et méthodes Kotlin
- Créer une classe dans IntelliJ IDEA et exécuter un programme
Points abordés
- Utiliser des classes, des méthodes et des fonctions génériques
Objectifs de l'atelier
- Créer une classe générique et ajouter des contraintes
- Créer les types
in
etout
- Créer des fonctions génériques, des méthodes et des extensions
Présentation des génériques
Tout comme de nombreux langages de programmation, Kotlin utilise des types génériques. Un type générique vous permet de rendre une classe générique, et ainsi de gagner en flexibilité.
Imaginez que vous implémentez une classe MyList
contenant une liste d'éléments. Sans génériques, vous devrez implémenter une nouvelle version de MyList
pour chaque type: une pour Double
, une pour String
et une pour Fish
. Vous pouvez rendre la liste générique afin qu'elle puisse contenir n'importe quel type d'objet. C'est l'exemple d'un caractère générique adapté à de nombreux types.
Pour définir un type générique, ajoutez T entre chevrons <T>
après le nom de la classe. Vous pouvez utiliser une autre lettre ou un nom plus long, mais la convention pour un type générique est la lettre "T".
class MyList<T> {
fun get(pos: Int): T {
TODO("implement")
}
fun addItem(item: T) {}
}
Vous pouvez référencer T
comme s'il s'agissait d'un type normal. Le type renvoyé pour get()
est T
, et le paramètre sur addItem()
est de type T
. Bien entendu, les listes génériques étant très utiles, la classe List
est intégrée à Kotlin.
Étape 1: Définissez une hiérarchie de types
Au cours de cette étape, vous allez créer des classes à utiliser à l'étape suivante. Nous avons étudié la sous-classe dans un atelier de programmation précédent, mais voici un résumé.
- Pour pouvoir libérer l'exemple, créez un package sous src et appelez-le
generics
. - Dans le package generics, créez un fichier
Aquarium.kt
. Cela vous permet de redéfinir des éléments avec les mêmes noms sans conflit. Le reste du code de cet atelier de programmation est donc stocké dans ce fichier. - Définissez une hiérarchie des types d'approvisionnement en eau. Commencez par créer une classe
open
pourWaterSupply
afin qu'elle puisse être sous-classée. - Ajoutez un paramètre booléen
var
,needsProcessing
. Cela crée automatiquement une propriété modifiable avec un getter et un setter. - Créez une sous-classe
TapWater
qui étendWaterSupply
et transmettrue
pourneedsProcessing
, car l'eau du robinet contient des additifs dangereux pour le poisson. - Dans
TapWater
, définissez une fonction appeléeaddChemicalCleaners()
qui définitneedsProcessing
surfalse
après avoir nettoyé l'eau. La propriéténeedsProcessing
peut être définie depuisTapWater
, car elle estpublic
par défaut et accessible aux sous-classes. Voici le code final.
package generics
open class WaterSupply(var needsProcessing: Boolean)
class TapWater : WaterSupply(true) {
fun addChemicalCleaners() {
needsProcessing = false
}
}
- Créez deux autres sous-classes de
WaterSupply
, appeléesFishStoreWater
etLakeWater
.FishStoreWater
n'a pas besoin d'être traitée, maisLakeWater
doit être filtré à l'aide de la méthodefilter()
. Après le filtrage, il n'est pas nécessaire de le traiter à nouveau. Dansfilter()
, définissezneedsProcessing = false
.
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}
Pour en savoir plus, consultez la leçon précédente sur l'héritage en langage Kotlin.
Étape 2: Créer un cours générique
Au cours de cette étape, vous allez modifier la classe Aquarium
pour qu'elle accepte différents types d'eau.
- Dans Aquarium.kt, définissez une classe
Aquarium
en saisissant<T>
entre crochets. - Ajoutez une propriété immuable
waterSupply
de typeT
àAquarium
.
class Aquarium<T>(val waterSupply: T)
- Écrivez une fonction appelée
genericsExample()
. Il ne fait pas partie d'une classe. Il peut donc se trouver au premier niveau du fichier, comme la fonctionmain()
ou les définitions de classe. Dans la fonction, créez unAquarium
et transmettez-lui unWaterSupply
. Le paramètrewaterSupply
étant générique, vous devez spécifier le type entre chevrons<>
.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
}
- Dans
genericsExample()
, votre code peut accéder auwaterSupply
de l'aquarium. Comme il s'agit du typeTapWater
, vous pouvez appeleraddChemicalCleaners()
sans aucun type.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- Lors de la création de l'objet
Aquarium
, vous pouvez supprimer les chevrons et les éléments entre eux, car Kotlin possède une inférence de type. Il n'y a donc pas de raison de direTapWater
deux fois lorsque vous créez l'instance. Le type peut être déduit par l'argument àAquarium
. La valeurAquarium
sera quand même de typeTapWater
.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- Pour voir ce qui se passe, imprimez
needsProcessing
avant et après avoir appeléaddChemicalCleaners()
. La fonction ci-dessous est terminée.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
aquarium.waterSupply.addChemicalCleaners()
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
}
- Ajoutez une fonction
main()
pour appelergenericsExample()
, puis exécutez votre programme et observez le résultat.
fun main() {
genericsExample()
}
⇒ water needs processing: true water needs processing: false
Étape 3: Soyez plus précis
Un code générique vous permet de transmettre presque tous les problèmes, ce qui constitue parfois un problème. Dans cette étape, vous allez rendre la classe Aquarium
plus spécifique pour ce qu'elle peut contenir.
- Dans
genericsExample()
, créez unAquarium
en transmettant une chaîne pour lewaterSupply
, puis imprimez la propriétéwaterSupply
de l'aquarium.
fun genericsExample() {
val aquarium2 = Aquarium("string")
println(aquarium2.waterSupply)
}
- Exécutez le programme pour observer le résultat.
⇒ string
Le résultat correspond à la chaîne que vous avez transmise, car Aquarium
n'applique aucune restriction à T.
Tous les types, y compris String
, peuvent être transmis.
- Dans
genericsExample()
, créez un autreAquarium
en transmettantnull
pourwaterSupply
. Si la valeur dewaterSupply
est nulle, imprimez le"waterSupply is null"
.
fun genericsExample() {
val aquarium3 = Aquarium(null)
if (aquarium3.waterSupply == null) {
println("waterSupply is null")
}
}
- Exécutez votre programme et observez le résultat.
⇒ waterSupply is null
Pourquoi transmettre des null
lorsque vous créez un Aquarium
? Cela est possible, car par défaut, T
représente le type Any?
pouvant être nul (type en haut de la hiérarchie des types). Les données suivantes sont équivalentes à celles que vous avez saisies précédemment.
class Aquarium<T: Any?>(val waterSupply: T)
- Pour ne pas autoriser la transmission de
null
, définissez explicitementT
surAny
en supprimant?
aprèsAny
.
class Aquarium<T: Any>(val waterSupply: T)
Dans ce contexte, Any
est appelé une contrainte générique. Cela signifie que n'importe quel type peut être transmis pour T
tant qu'il n'est pas null
.
- Assurez-vous que seul un élément
WaterSupply
(ou l'une de ses sous-classes) peut être transmis pourT
. RemplacezAny
parWaterSupply
pour définir une contrainte générique plus spécifique.
class Aquarium<T: WaterSupply>(val waterSupply: T)
Étape 4: Ajoutez des vérifications
Cette étape consiste à vous familiariser avec la fonction check()
afin de vous assurer que votre code se comporte comme prévu. La fonction check()
est une fonction de bibliothèque standard en langage Kotlin. Il agit en tant qu'assertion et renvoie une erreur IllegalStateException
si son argument a la valeur false
.
- Ajoutez une méthode
addWater()
à la classeAquarium
pour ajouter de l'eau, avec uncheck()
qui vous évite d'avoir à traiter l'eau en premier.
class Aquarium<T: WaterSupply>(val waterSupply: T) {
fun addWater() {
check(!waterSupply.needsProcessing) { "water supply needs processing first" }
println("adding water from $waterSupply")
}
}
Dans ce cas, si needsProcessing
est défini sur "true", check()
renvoie une exception.
- Dans
genericsExample()
, ajoutez du code pour créer unAquarium
avecLakeWater
, puis ajoutez-y de l'eau.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.addWater()
}
- Exécutez votre programme, et vous recevrez une exception, car l'eau doit d'abord être filtrée.
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)
- Ajoutez un appel pour filtrer l'eau avant de l'ajouter au
Aquarium
. Désormais, lorsque vous exécutez votre programme, aucune exception n'est générée.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.waterSupply.filter()
aquarium4.addWater()
}
⇒ adding water from generics.LakeWater@880ec60
La section ci-dessus aborde les bases des génériques. Les tâches suivantes en couvrent davantage, mais le concept important est de déclarer et d'utiliser une classe générique avec une contrainte générique.
Dans cette tâche, vous allez découvrir les types d'entrées et de sorties à l'aide de génériques. Un type in
est un type qui ne peut être transmis que dans une classe, et non renvoyé. Un type out
est un type qui ne peut être renvoyé qu'à partir d'une classe.
Examinez la classe Aquarium
. Vous constaterez que le type générique n'est renvoyé que lors de l'obtention de la propriété waterSupply
. Il n'existe aucune méthode utilisant une valeur de type T
comme paramètre (sauf pour la définir dans le constructeur). Kotlin vous permet de définir les types out
dans ce cas précis, et il peut déduire des informations supplémentaires sur les endroits où les types peuvent être utilisés en toute sécurité. De même, vous pouvez définir des types in
pour les types génériques qui ne sont transmis que dans les méthodes, et non renvoyés. Cela permet à Kotlin d'effectuer des vérifications supplémentaires pour contrôler la sécurité du code.
Les types in
et out
sont des instructions concernant le système de types Kotlin. L'ensemble du système de types n'est pas expliqué dans ce tutoriel (le programme est assez impliqué). Toutefois, le compilateur signale les types qui ne sont pas correctement associés aux libellés in
et out
. Vous devez donc les connaître.
Étape 1: Définissez un type de sortie
- Dans la classe
Aquarium
, remplacezT: WaterSupply
par le typeout
.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}
- Dans le même fichier, en dehors de la classe, déclarez une fonction
addItemTo()
qui attend unAquarium
deWaterSupply
.
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")
- Appelez
addItemTo()
depuisgenericsExample()
et exécutez votre programme.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
addItemTo(aquarium)
}
⇒ item added
Kotlin peut garantir que addItemTo()
n'effectue aucune action non sécurisée avec le code générique WaterSupply
, car il a été déclaré en tant que type out
.
- Si vous supprimez le mot clé
out
, le compilateur génère une erreur lors de l'appel deaddItemTo()
, car Kotlin ne peut pas garantir que le contenu n'est pas dangereux.
Étape 2: Définissez un type
Le type in
est semblable au type out
, mais pour les types génériques qui ne sont transmis que dans des fonctions et qui ne sont pas renvoyés. Si vous essayez de renvoyer un type in
, une erreur de compilation s'affichera. Dans cet exemple, vous allez définir un type in
dans une interface.
- Dans Aquarium.kt, définissez une interface
Cleaner
qui utilise unT
générique limité àWaterSupply
. Il n'est utilisé qu'en tant qu'argument declean()
. Vous pouvez donc en faire un paramètrein
.
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}
- Pour utiliser l'interface
Cleaner
, créez une classeTapWaterCleaner
qui met en œuvreCleaner
pour nettoyerTapWater
en ajoutant des produits chimiques.
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}
- Dans la classe
Aquarium
, mettez à jour leaddWater()
pour prendre unCleaner
de typeT
, puis nettoyez l'eau avant de l'ajouter.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
fun addWater(cleaner: Cleaner<T>) {
if (waterSupply.needsProcessing) {
cleaner.clean(waterSupply)
}
println("water added")
}
}
- Mettez à jour l'exemple de code
genericsExample()
pour créer unTapWaterCleaner
avec unAquarium
avecTapWater
, puis ajoutez de l'eau à l'aide du nettoyeur. Il utilisera le produit plus propre si nécessaire.
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}
Kotlin utilisera les informations de type in
et out
pour s'assurer que votre code utilise les informations génériques de manière sécurisée. Out
et in
sont faciles à retenir: les types out
peuvent être transmis en tant que valeurs de retour, les types in
en tant qu'arguments.
Si vous souhaitez en savoir plus sur les problèmes de type et de résolution, consultez la documentation pour en savoir plus.
Dans cette tâche, vous allez découvrir les fonctions génériques et leur utilisation. En règle générale, nous vous recommandons d'utiliser une fonction générique chaque fois qu'elle utilise un argument d'une classe de type générique.
Étape 1: Créez une fonction générique
- Dans generics/Aquarium.kt, créez une fonction
isWaterClean()
qui utilise unAquarium
. Vous devez spécifier le type générique du paramètre. Vous avez la possibilité d'utiliserWaterSupply
.
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
println("aquarium water is clean: ${aquarium.waterSupply.needsProcessing}")
}
Cela signifie que Aquarium
doit disposer d'un paramètre de type out
pour que cet appel soit appelé. Parfois, out
ou in
sont trop restrictifs, car vous devez utiliser un type à la fois pour les entrées et les sorties. Vous pouvez supprimer l'exigence out
en rendant la fonction générique.
- Pour que la fonction soit générique, ajoutez des chevrons après le mot clé
fun
avec un type génériqueT
et toute contrainte, dans ce casWaterSupply
. ModifiezAquarium
de sorte que la contrainte soit appliquée parT
et non parWaterSupply
.
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}
T
est un paramètre de type isWaterClean()
qui permet de spécifier le type générique de l'aquarium. Ce schéma est très courant et il est utile de prendre un moment pour y parvenir.
- Appelez la fonction
isWaterClean()
en spécifiant le type entre chevrons juste après le nom de la fonction et avant les parenthèses.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean<TapWater>(aquarium)
}
- L'inférence de type provenant de l'argument
aquarium
est nécessaire pour le supprimer. Vous devez donc le supprimer. Exécutez votre programme et observez le résultat.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean(aquarium)
}
⇒ aquarium water is clean: false
Étape 2: Créez une méthode générique avec un type rectifié
Vous pouvez également utiliser des fonctions génériques pour des méthodes, même dans les classes ayant leur propre type générique. Dans cette étape, vous allez ajouter à Aquarium
une méthode générique pour vérifier si elle est de type WaterSupply
.
- Dans la classe
Aquarium
, déclarez une méthodehasWaterSupplyOfType()
qui utilise un paramètre génériqueR
(T
est déjà utilisé) limité àWaterSupply
, et renvoietrue
siwaterSupply
est de typeR
. Il s'agit de la fonction que vous avez déclarée précédemment, mais dans la classeAquarium
.
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
- Notez que l'élément
R
final est souligné en rouge. Passez la souris dessus pour découvrir l'erreur. - Pour effectuer une vérification
is
, vous devez indiquer à Kotlin que le type est réifié (ou réel) et peut être utilisé dans la fonction. Pour ce faire, ajoutezinline
devant le mot cléfun
etreified
devant le type génériqueR
.
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
Une fois un type redéfini, vous pouvez l'utiliser comme un type normal, car il est réel après intégration. Cela signifie que vous pouvez effectuer des vérifications is
à l'aide du type choisi.
Si vous n'utilisez pas reified
ici, le type ne sera pas suffisant pour que Kotlin puisse autoriser les vérifications is
. C'est parce que les types non unifiés ne sont disponibles qu'au moment de la compilation et ne peuvent pas être utilisés au moment de l'exécution par votre programme. Ce point est abordé plus en détail dans la section suivante.
- Transmettre
TapWater
comme type Comme pour appeler des fonctions génériques, appelez des méthodes génériques en utilisant des chevrons dont le type est situé après le nom de la fonction. Exécutez votre programme et observez le résultat.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>()) // true
}
⇒ true
Étape 3: Créez des fonctions d'extension
Vous pouvez également utiliser les types rectifiés pour les fonctions standards et les fonctions d'extension.
- En dehors de la classe
Aquarium
, définissez une fonction d'extension surWaterSupply
appeléeisOfType()
, qui vérifie siWaterSupply
est de type spécifique, par exempleTapWater
.
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T
- Appelez la fonction d'extension comme une méthode.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.waterSupply.isOfType<TapWater>())
}
⇒ true
Avec ces fonctions d'extension, peu importe le type de Aquarium
(Aquarium
, TowerTank
ou une autre sous-classe), tant qu'il s'agit d'une propriété Aquarium
. La syntaxe star-projection est un moyen pratique de spécifier différentes correspondances. Et lorsque vous utilisez une projection étoile, Kotlin veillera à ce que vous ne fassiez rien d'autre.
- Pour utiliser une projection étoile, ajoutez
<*>
aprèsAquarium
. DéplacezhasWaterSupplyOfType()
pour en faire une fonction d'extension, car elle ne fait pas vraiment partie de l'API principale deAquarium
.
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R
- Remplacez l'appel par
hasWaterSupplyOfType()
et exécutez votre programme.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>())
}
⇒ true
Dans l'exemple précédent, vous deviez marquer le type générique comme reified
et définir la fonction inline
, car Kotlin doit les connaître au moment de l'exécution, et pas seulement du temps de compilation.
Tous les types génériques ne sont utilisés qu'au moment de la compilation par Kotlin. Cela permet au compilateur de s'assurer que vous faites tout cela en toute sécurité. Lors de l'exécution, tous les types génériques sont effacés, d'où le message d'erreur précédent sur la vérification d'un type effacé.
Le compilateur peut créer un code correct sans conserver les types génériques jusqu'à l'exécution. Mais cela implique parfois d'effectuer certaines opérations, comme vérifier des types génériques sur is
, que le compilateur ne peut pas prendre en charge. C'est pourquoi Kotlin a ajouté des types concrets ou réels.
Pour en savoir plus sur les types unifiés et l'effacement des types, consultez la documentation Kotlin.
Dans cette leçon, nous nous sommes concentrés sur les termes génériques, qui sont importants pour rendre le code plus flexible et plus facile à réutiliser.
- Créez des classes génériques pour rendre le code plus flexible.
- Ajoutez des contraintes génériques pour limiter les types utilisés avec des caractères génériques.
- Utilisez les types
in
etout
avec des caractères génériques pour améliorer la vérification du type afin de limiter les types qui sont transmis ou renvoyés par les classes. - Créer des fonctions et des méthodes génériques pour utiliser des types génériques Exemples :
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... }
- Les fonctions d'extension génériques permettent d'ajouter des fonctionnalités qui ne font pas partie de la suite principale à une classe.
- Les types redéfinis sont parfois nécessaires en raison de l'effacement des types. Contrairement aux types génériques, les types améliorés sont conservés pendant l'exécution.
- Utilisez la fonction
check()
pour vérifier que votre code s'exécute comme prévu. Exemple:check(!waterSupply.needsProcessing) { "water supply needs processing first" }
Documentation Kotlin
Si vous souhaitez en savoir plus sur un sujet de ce cours ou si vous rencontrez des difficultés, https://kotlinlang.org constitue le meilleur point de départ.
- Générique
- Contraintes génériques
- Projections étoiles
- Types
In
etout
- Paramètres unifiés
- Effacement du texte
- Fonction
check()
Tutoriels Kotlin
Le site Web https://try.kotlinlang.org propose des tutoriels enrichis appelés Kotlin Koans, un interpréteur Web ainsi qu'une documentation de référence complète avec des exemples.
Cours Udacity
Pour afficher le cours Udacity sur ce sujet, consultez Kotlin Bootcamp for Programmers (Formation Kotlin pour les programmeurs).
IntelliJ IDEA
Consultez la documentation sur IntelliJ IDEA sur le site Web de JetBrains.
Cette section répertorie les devoirs possibles pour les élèves qui effectuent cet atelier de programmation dans le cadre d'un cours animé par un enseignant. C'est à l'enseignant de procéder comme suit:
- Si nécessaire, rendez-les.
- Communiquez aux élèves comment rendre leurs devoirs.
- Notez les devoirs.
Les enseignants peuvent utiliser ces suggestions autant qu'ils le souhaitent, et ils n'ont pas besoin d'attribuer les devoirs de leur choix.
Si vous suivez vous-même cet atelier de programmation, n'hésitez pas à utiliser ces devoirs pour tester vos connaissances.
Répondez à ces questions
Question 1
Quelle proposition correspond à la convention pour nommer un type générique ?
▢ <Gen>
▢ <Generic>
▢ <T>
▢ <X>
Question 2
Une restriction applicable aux types autorisés pour un type générique est appelée:
▢ une restriction générique
▢ une contrainte générique
Désambigulation de ▢
▢ une limite de type générique
Question 3
"Signifié" signifie:
▢ L'impact réel de l'exécution d'un objet a été calculé.
▢ L'index d'entrée à accès limité a été défini sur la classe.
▢ Le type générique est devenu un type réel.
▢ Un indicateur d'erreur à distance s'est déclenché.
Passez à la leçon suivante:
Pour une présentation du cours, y compris des liens vers d'autres ateliers de programmation, consultez le "Kotlin Bootcamp for Programmers: Welcome to the course."