Formation Kotlin pour les programmeurs 5.1 : Extensions

Cet atelier de programmation fait partie du cours Kotlin Bootcamp for Programmers. Vous tirerez pleinement parti de ce cours en suivant les ateliers de programmation dans l'ordre. En fonction de vos connaissances, vous pourrez peut-être survoler certaines sections. Ce cours s'adresse aux programmeurs qui connaissent un langage orienté objet et qui souhaitent apprendre Kotlin.

Introduction

Dans cet atelier de programmation, vous allez découvrir plusieurs fonctionnalités utiles en Kotlin, y compris les paires, les collections et les fonctions d'extension.

Plutôt que de créer une seule application exemple, les leçons de ce cours sont conçues pour développer vos connaissances, mais sont semi-indépendantes les unes des autres afin que vous puissiez parcourir les sections que vous connaissez. Pour les relier, de nombreux exemples utilisent un thème d'aquarium. Si vous souhaitez en savoir plus sur l'histoire 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
  • Utiliser le REPL (Read-Eval-Print Loop) de Kotlin dans IntelliJ IDEA
  • Créer une classe dans IntelliJ IDEA et exécuter un programme

Points abordés

  • Utiliser des paires et des triplets
  • En savoir plus sur les collections
  • Définir et utiliser des constantes
  • Écrire des fonctions d'extension

Objectifs de l'atelier

  • Découvrir les paires, les triplets et les tables de hachage dans le REPL
  • Découvrez différentes façons d'organiser les constantes.
  • Écrire une fonction d'extension et une propriété d'extension

Dans cette tâche, vous allez découvrir les paires et les triplets, et comment les déstructurer. Les paires et les triplets sont des classes de données prédéfinies pour deux ou trois éléments génériques. Cela peut, par exemple, être utile pour qu'une fonction renvoie plusieurs valeurs.

Supposons que vous ayez un List de poissons et une fonction isFreshWater() pour vérifier si le poisson est un poisson d'eau douce ou d'eau salée. List.partition() renvoie deux listes : l'une avec les éléments pour lesquels la condition est true et l'autre pour les éléments pour lesquels la condition est false.

val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")

Étape 1 : Faites des paires et des triples

  1. Ouvrez le REPL (Tools > Kotlin > Kotlin REPL).
  2. Créez une paire associant un équipement à son utilisation, puis imprimez les valeurs. Vous pouvez créer une paire en créant une expression qui relie deux valeurs, telles que deux chaînes, avec le mot clé to, puis en utilisant .first ou .second pour faire référence à chaque valeur.
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. Créez un triplet et imprimez-le avec toString(), puis convertissez-le en liste avec toList(). Vous créez un triplet à l'aide de Triple() avec trois valeurs. Utilisez .first, .second et .third pour faire référence à chaque valeur.
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42)
[6, 9, 42]

Les exemples ci-dessus utilisent le même type pour toutes les parties de la paire ou du triplet, mais ce n'est pas obligatoire. Les parties peuvent être une chaîne, un nombre ou une liste, par exemple, voire une autre paire ou un autre triplet.

  1. Créez une paire dont la première partie est elle-même une paire.
val equipment2 = ("fish net" to "catching fish") to "equipment"
println("${equipment2.first} is ${equipment2.second}\n")
println("${equipment2.first.second}")
⇒ (fish net, catching fish) is equipment
⇒ catching fish

Étape 2 : Déstructurer certaines paires et certains triplets

La déstructuration consiste à séparer les paires et les triples en leurs éléments constitutifs. Attribuez la paire ou le triple au nombre approprié de variables, et Kotlin attribuera la valeur de chaque partie dans l'ordre.

  1. Déstructurez une paire et imprimez les valeurs.
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
  1. Déstructurez un triple et affichez les valeurs.
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

Notez que la déstructuration des paires et des triplets fonctionne de la même manière qu'avec les classes de données, comme indiqué dans un précédent atelier de programmation.

Dans cette tâche, vous en apprendrez davantage sur les collections, y compris les listes, et sur un nouveau type de collection, les tables de hachage.

Étape 1 : En savoir plus sur les listes

  1. Les listes et les listes modifiables ont été présentées dans une leçon précédente. Les listes sont une structure de données très utile. Kotlin fournit donc un certain nombre de fonctions intégrées pour les listes. Consultez cette liste partielle de fonctions pour les listes. Vous trouverez des listes complètes dans la documentation Kotlin pour List et MutableList.

Fonction

Purpose

add(element: E)

Ajoutez un élément à la liste modifiable.

remove(element: E)

Supprimez un élément d'une liste modifiable.

reversed()

Renvoie une copie de la liste avec les éléments dans l'ordre inverse.

contains(element: E)

Renvoie true si la liste contient l'élément.

subList(fromIndex: Int, toIndex: Int)

Renvoie une partie de la liste, du premier index jusqu'au deuxième index (non inclus).

  1. Toujours dans le REPL, créez une liste de nombres et appelez sum() dessus. Cela résume tous les éléments.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. Créez une liste de chaînes et additionnez-la.
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
  1. Si l'élément n'est pas quelque chose que List sait comment additionner directement, comme une chaîne, vous pouvez spécifier comment l'additionner à l'aide de .sumBy() avec une fonction lambda, par exemple, pour additionner par la longueur de chaque chaîne. Le nom par défaut d'un argument lambda est it. Ici, it fait référence à chaque élément de la liste lors de son parcours.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. Vous pouvez faire beaucoup plus avec les listes. Pour voir les fonctionnalités disponibles, vous pouvez créer une liste dans IntelliJ IDEA, ajouter le point, puis consulter la liste d'autocomplétion dans l'info-bulle. Cela fonctionne pour n'importe quel objet. Essayez avec une liste.

  1. Choisissez listIterator() dans la liste, puis parcourez la liste avec une instruction for et affichez tous les éléments séparés par des espaces.
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

Étape 2 : Essayez les tables de hachage

En Kotlin, vous pouvez mapper à peu près n'importe quoi à n'importe quoi d'autre à l'aide de hashMapOf(). Les tables de hachage sont un peu comme une liste de paires, où la première valeur sert de clé.

  1. Créez un tableau associatif qui fait correspondre les symptômes (clés) aux maladies des poissons (valeurs).
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. Vous pouvez ensuite récupérer la valeur de la maladie en fonction de la clé du symptôme, en utilisant get() ou, plus simplement, des crochets [].
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. Essayez de spécifier un symptôme qui ne figure pas sur la carte.
println(cures["scale loss"])
⇒ null

Si une clé n'est pas dans la carte, la tentative de renvoi de la maladie correspondante renvoie null. En fonction des données cartographiques, il est possible qu'aucune clé ne corresponde. Pour ce type de situations, Kotlin fournit la fonction getOrDefault().

  1. Essayez de rechercher une clé qui ne correspond à rien, en utilisant getOrDefault().
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know

Si vous devez faire plus que renvoyer une valeur, Kotlin fournit la fonction getOrElse().

  1. Modifiez votre code pour utiliser getOrElse() au lieu de getOrDefault().
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this

Au lieu de renvoyer une simple valeur par défaut, le code entre accolades {} est exécuté. Dans l'exemple, else renvoie simplement une chaîne, mais il pourrait s'agir d'une fonction plus sophistiquée, comme la recherche d'une page Web contenant un remède et son renvoi.

Tout comme pour mutableListOf, vous pouvez également créer un mutableMapOf. Une carte mutable vous permet d'ajouter et de supprimer des éléments. "Mutable" signifie "modifiable", tandis qu'"immuable" signifie "non modifiable".

  1. Créez une carte d'inventaire modifiable, en mappant une chaîne d'équipement au nombre d'articles. Créez-le avec une épuisette, puis ajoutez trois éponges de nettoyage dans l'inventaire avec put() et retirez l'épuisette avec remove().
val inventory = mutableMapOf("fish net" to 1)
inventory.put("tank scrubber", 3)
println(inventory.toString())
inventory.remove("fish net")
println(inventory.toString())
⇒ {fish net=1, tank scrubber=3}{tank scrubber=3}

Dans cette tâche, vous allez découvrir les constantes en Kotlin et différentes façons de les organiser.

Étape 1 : Découvrez la différence entre "const" et "val"

  1. Dans le REPL, essayez de créer une constante numérique. En Kotlin, vous pouvez créer des constantes de premier niveau et leur attribuer une valeur au moment de la compilation à l'aide de const val.
const val rocks = 3

La valeur est attribuée et ne peut pas être modifiée, ce qui ressemble beaucoup à la déclaration d'un val normal. Quelle est donc la différence entre const val et val ? La valeur de const val est déterminée au moment de la compilation, tandis que celle de val est déterminée lors de l'exécution du programme. Cela signifie que val peut être attribuée par une fonction au moment de l'exécution.

Cela signifie que val peut se voir attribuer une valeur à partir d'une fonction, mais pas const val.

val value1 = complexFunctionCall() // OK
const val CONSTANT1 = complexFunctionCall() // NOT ok

De plus, const val ne fonctionne qu'au niveau supérieur et dans les classes singleton déclarées avec object, et non avec les classes standards. Vous pouvez l'utiliser pour créer un fichier ou un objet singleton qui ne contient que des constantes, et les importer selon vos besoins.

object Constants {
    const val CONSTANT2 = "object constant"
}
val foo = Constants.CONSTANT2

Étape 2 : Créez un objet associé

Kotlin ne comporte pas de constantes au niveau de la classe.

Pour définir des constantes dans une classe, vous devez les encapsuler dans des objets compagnons déclarés avec le mot clé companion. L'objet compagnon est essentiellement un objet singleton dans la classe.

  1. Créez une classe avec un objet compagnon contenant une constante de chaîne.
class MyClass {
    companion object {
        const val CONSTANT3 = "constant in companion"
    }
}

La différence fondamentale entre les objets compagnons et les objets standards est la suivante :

  • Les objets compagnons sont initialisés à partir du constructeur statique de la classe conteneur, c'est-à-dire qu'ils sont créés lorsque l'objet est créé.
  • Les objets standards sont initialisés de manière différée lors du premier accès à cet objet, c'est-à-dire lors de leur première utilisation.

Il y a plus à savoir, mais pour l'instant, il vous suffit de savoir que vous devez encapsuler les constantes dans des classes dans un objet compagnon.

Dans cette tâche, vous allez apprendre à étendre le comportement des classes. Il est très courant d'écrire des fonctions utilitaires pour étendre le comportement d'une classe. Kotlin fournit une syntaxe pratique pour déclarer ces fonctions utilitaires : les fonctions d'extension.

Les fonctions d'extension vous permettent d'ajouter des fonctions à une classe existante sans avoir à accéder à son code source. Par exemple, vous pouvez les déclarer dans un fichier Extensions.kt qui fait partie de votre package. Cela ne modifie pas la classe, mais vous permet d'utiliser la notation par points pour appeler la fonction sur les objets de cette classe.

Étape 1 : Écrire une fonction d'extension

  1. Toujours dans le REPL, écrivez une fonction d'extension simple, hasSpaces(), pour vérifier si une chaîne contient des espaces. Le nom de la fonction est précédé de la classe sur laquelle elle agit. Dans la fonction, this fait référence à l'objet sur lequel il est appelé, et it fait référence à l'itérateur dans l'appel find().
fun String.hasSpaces(): Boolean {
    val found = this.find { it == ' ' }
    return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
  1. Vous pouvez simplifier la fonction hasSpaces(). Le this n'est pas explicitement nécessaire. La fonction peut être réduite à une seule expression et renvoyée. Les accolades {} qui l'entourent ne sont donc pas non plus nécessaires.
fun String.hasSpaces() = find { it == ' ' } != null

Étape 2 : Découvrez les limites des extensions

Les fonctions d'extension n'ont accès qu'à l'API publique de la classe qu'elles étendent. Les variables private ne sont pas accessibles.

  1. Essayez d'ajouter des fonctions d'extension à une propriété marquée private.
class AquariumPlant(val color: String, private val size: Int)

fun AquariumPlant.isRed() = color == "red"    // OK
fun AquariumPlant.isBig() = size > 50         // gives error
⇒ error: cannot access 'size': it is private in 'AquariumPlant'
  1. Examinez le code ci-dessous et déterminez ce qu'il imprimera.
open class AquariumPlant(val color: String, private val size: Int)

class GreenLeafyPlant(size: Int) : AquariumPlant("green", size)

fun AquariumPlant.print() = println("AquariumPlant")
fun GreenLeafyPlant.print() = println("GreenLeafyPlant")

val plant = GreenLeafyPlant(size = 10)
plant.print()
println("\n")
val aquariumPlant: AquariumPlant = plant
aquariumPlant.print()  // what will it print?
⇒ GreenLeafyPlant
AquariumPlant

plant.print() tirages GreenLeafyPlant. Vous vous attendez peut-être à ce que aquariumPlant.print() affiche également GreenLeafyPlant, car la valeur de plant lui a été attribuée. Toutefois, le type est résolu au moment de la compilation, donc AquariumPlant est imprimé.

Étape 3 : Ajouter une propriété d'extension

En plus des fonctions d'extension, Kotlin vous permet également d'ajouter des propriétés d'extension. Comme pour les fonctions d'extension, vous spécifiez la classe que vous étendez, suivie d'un point, puis du nom de la propriété.

  1. Toujours dans le REPL, ajoutez une propriété d'extension isGreen à AquariumPlant, qui est true si la couleur est verte.
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

La propriété isGreen est accessible comme une propriété normale. Lorsqu'elle est consultée, le getter de isGreen est appelé pour obtenir la valeur.

  1. Affichez la propriété isGreen de la variable aquariumPlant et observez le résultat.
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

Étape 4 : En savoir plus sur les récepteurs pouvant être nuls

La classe que vous étendez est appelée récepteur, et il est possible de rendre cette classe nullable. Dans ce cas, la variable this utilisée dans le corps peut être null. Assurez-vous donc de la tester. Vous devez accepter un récepteur nullable si vous vous attendez à ce que les appelants souhaitent appeler votre méthode d'extension sur des variables nullables ou si vous souhaitez fournir un comportement par défaut lorsque votre fonction est appliquée à null.

  1. Toujours dans le REPL, définissez une méthode pull() qui accepte un récepteur pouvant être nul. Cela est indiqué par un point d'interrogation ? après le type, avant le point. Dans le corps, vous pouvez tester si this n'est pas null en utilisant questionmark-dot-apply ?.apply..
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. Dans ce cas, aucune sortie n'est générée lorsque vous exécutez le programme. Étant donné que plant est null, le println() interne n'est pas appelé.

Les fonctions d'extension sont très puissantes, et la majeure partie de la bibliothèque standard Kotlin est implémentée en tant que fonctions d'extension.

Dans cette leçon, vous avez découvert les collections et les constantes, et vous avez pu constater la puissance des fonctions et des propriétés d'extension.

  • Les paires et les triplets peuvent être utilisés pour renvoyer plusieurs valeurs à partir d'une fonction. Exemples :
    val twoLists = fish.partition { isFreshWater(it) }
  • Kotlin propose de nombreuses fonctions utiles pour List, telles que reversed(), contains() et subList().
  • Un HashMap peut être utilisé pour mapper des clés à des valeurs. Par exemple :
    val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  • Déclarez les constantes au moment de la compilation à l'aide du mot clé const. Vous pouvez les placer au niveau supérieur, les organiser dans un objet singleton ou les placer dans un objet compagnon.
  • Un objet compagnon est un objet singleton dans une définition de classe, défini avec le mot clé companion.
  • Les fonctions et propriétés d'extension peuvent ajouter des fonctionnalités à une classe. Exemples :
    fun String.hasSpaces() = find { it == ' ' } != null
  • Un récepteur nullable vous permet de créer des extensions sur une classe qui peut être null. L'opérateur ?. peut être associé à apply pour vérifier la présence de null avant d'exécuter le code. Par exemple :
    this?.apply { println("removing $this") }

Documentation Kotlin

Si vous souhaitez en savoir plus sur un sujet abordé dans ce cours ou si vous êtes bloqué, https://kotlinlang.org est le meilleur point de départ.

Tutoriels Kotlin

Le site Web https://try.kotlinlang.org inclut des tutoriels complets appelés Kotlin Koans, un interpréteur Web et un ensemble complet de documentation de référence avec des exemples.

Cours Udacity

Pour consulter le cours Udacity sur ce sujet, reportez-vous à la formation Kotlin pour les programmeurs.

IntelliJ IDEA

La documentation d'IntelliJ IDEA est disponible sur le site Web de JetBrains.

Cette section répertorie les devoirs possibles pour les élèves qui suivent cet atelier de programmation dans le cadre d'un cours animé par un enseignant. Il revient à l'enseignant d'effectuer les opérations suivantes :

  • Attribuer des devoirs si nécessaire
  • Indiquer aux élèves comment rendre leurs devoirs
  • Noter les devoirs

Les enseignants peuvent utiliser ces suggestions autant qu'ils le souhaitent, et ne doivent pas hésiter à attribuer d'autres devoirs aux élèves s'ils le jugent nécessaire.

Si vous suivez cet atelier de programmation par vous-même, n'hésitez pas à utiliser ces devoirs pour tester vos connaissances.

Répondre aux questions suivantes

Question 1

Laquelle des propositions suivantes renvoie une copie d'une liste ?

▢ add()

▢ remove()

▢ reversed()

▢ contains()

Question 2

Quelle fonction d'extension sur class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean) générera une erreur de compilation ?

▢ fun AquariumPlant.isRed() = color == "red"

▢ fun AquariumPlant.isBig() = size > 45

▢ fun AquariumPlant.isExpensive() = cost > 10.00

▢ fun AquariumPlant.isNotLeafy() = leafy == false

Question 3

Parmi les propositions suivantes, laquelle n'est pas un endroit où vous pouvez définir des constantes avec const val ?

▢ au premier niveau d'un fichier

▢ dans les cours réguliers

▢ dans les objets singleton

▢ dans les objets associés

Passez à la leçon suivante : 5.2 Génériques

Pour obtenir un aperçu du cours, y compris des liens vers d'autres ateliers de programmation, consultez Formation Kotlin pour les programmeurs : bienvenue dans le cours.