
Fonctions de portée (`let`, `apply`, `run`, `also`) : simplifier le code
Maîtrisez les fonctions de portée `let`, `run`, `with`, `apply`, `also` en Kotlin pour exécuter du code dans le contexte d'un objet, simplifier les null checks et la configuration.
Introduction : Exécuter du code dans le contexte d'un objet
La bibliothèque standard de Kotlin fournit plusieurs fonctions dont le seul but est d'exécuter un bloc de code (une lambda) dans le contexte d'un objet spécifique. Ces fonctions sont appelées fonctions de portée (scope functions) car elles créent une portée temporaire où vous pouvez accéder à l'objet d'une manière particulière (soit comme `this`, soit comme `it`).
Pourquoi utiliser ces fonctions ? Elles permettent d'écrire du code plus concis et plus lisible, en particulier lorsqu'il s'agit de :
- Effectuer des opérations sur un objet nullable seulement s'il n'est pas nul.
- Configurer les propriétés d'un objet après sa création.
- Exécuter une série d'opérations sur un même objet sans répéter son nom.
- Limiter la portée de variables temporaires.
Les cinq fonctions de portée principales sont : `let`, `run`, `with`, `apply`, et `also`. Elles sont toutes similaires mais diffèrent subtilement dans la manière dont l'objet est accessible à l'intérieur de la lambda et dans ce que la fonction retourne. Comprendre ces différences est essentiel pour choisir la fonction la plus appropriée à chaque situation et écrire du code Kotlin véritablement idiomatique.
Les deux distinctions clés
Pour choisir la bonne fonction de portée, il faut considérer deux axes principaux :
1. La référence à l'objet de contexte : Comment accédez-vous à l'objet à l'intérieur de la lambda ? * Comme un récepteur (`this`) : `run`, `with`, `apply`. Cela permet d'appeler les méthodes et d'accéder aux propriétés de l'objet directement, sans qualificateur (comme si vous étiez à l'intérieur de la classe). * Comme un argument de lambda (`it`) : `let`, `also`. L'objet est passé comme argument unique à la lambda, accessible via `it` (ou un nom personnalisé si vous le déclarez explicitement).
2. La valeur de retour : Qu'est-ce que la fonction de portée retourne ? * L'objet de contexte lui-même : `apply`, `also`. Utile pour chaîner des appels ou pour la configuration d'objets où vous avez besoin de l'objet à la fin. * Le résultat de la lambda : `let`, `run`, `with`. Utile lorsque vous voulez calculer une valeur basée sur l'objet de contexte.
Le tableau suivant résume ces différences :
| Fonction | Objet de Contexte | Valeur de Retour | Usage Typique |
|---|---|---|---|
let | it (arg) | Résultat lambda | Null check, variable locale, transformation |
run (extension) | this (récepteur) | Résultat lambda | Null check + accès `this`, configuration + calcul |
run (non-extension) | N/A | Résultat lambda | Exécuter un bloc comme expression |
with | this (récepteur, arg func) | Résultat lambda | Opérations groupées sur objet non-null |
apply | this (récepteur) | Objet de contexte | Configuration d'objet (style builder) |
also | it (arg) | Objet de contexte | Actions/effets de bord sur l'objet (logging, ajout à liste) |
`let` : Agir sur un objet non-null (avec `it`)
Signature : `fun`let` est très souvent utilisée en combinaison avec l'opérateur d'appel sécurisé (`?.`) pour exécuter un bloc de code uniquement si une variable nullable n'est pas nulle. A l'intérieur de la lambda `let`, l'objet non-nul est disponible comme argument `it`. Elle est aussi utile pour introduire une variable locale pour un résultat intermédiaire ou pour améliorer la lisibilité en limitant la portée.
fun main() {
val name: String? = "Kotlin"
val emptyName: String? = null
// 1. Null Check avec let
val length = name?.let {
println("Traitement de '$it'...") // it est non-null ici (String)
it.length // La dernière expression est retournée
}
println("Longueur: $length") // Affiche: Longueur: 6
val emptyLength = emptyName?.let {
println("Ce bloc n'est pas exécuté")
it.length
} ?: 0 // Utilisation de l'Elvis pour une valeur par défaut si emptyName est null
println("Longueur (vide): $emptyLength") // Affiche: Longueur (vide): 0
// 2. Variable locale pour clarté
val numbers = listOf("one", "two", "three", "four")
val resultList = numbers.map { it.length }.filter { it > 3 }.let {
// 'it' ici est la List résultante [5, 5]
"Les longueurs > 3 sont : $it"
}
println(resultList) // Affiche: Les longueurs > 3 sont : [5, 5]
} `run` : Agir sur un objet (avec `this`) ou exécuter un bloc
Signature (extension) : `funLa version *extension* de `run` est similaire à `let`, mais l'objet de contexte est accessible comme `this`. C'est utile pour les null checks (`?.run`) lorsque vous préférez accéder aux membres de l'objet directement (sans `it.`). Elle combine configuration et calcul d'un résultat.
La version *non-extension* de `run` exécute simplement un bloc de code et retourne le résultat de sa dernière expression. Elle est utile pour créer une portée limitée ou pour exécuter plusieurs instructions là où une seule expression est attendue.
data class Person(var name: String, var age: Int)
fun main() {
var person: Person? = Person("Alice", 30)
// 1. Extension 'run' pour null check et accès 'this'
val personDescription = person?.run {
println("Configuration de $name...") // Accès direct à 'name' via 'this'
age += 1
"$name a maintenant $age ans" // Résultat retourné
} ?: "Personne inconnue"
println(personDescription) // Affiche: Alice a maintenant 31 ans
// 2. Non-extension 'run' pour portée limitée / expression
val hexNumber = run {
val a = 10
val b = 5
// Calcul complexe...
(a * b + 15).toString(16) // Retourne la valeur hexadécimale
}
println("Nombre Hex: $hexNumber") // Affiche: Nombre Hex: 41
}`with` : Opérations groupées sur un objet (non-null)
Signature : `fun`with` n'est pas une fonction d'extension. Elle prend l'objet comme premier argument et la lambda comme second. A l'intérieur de la lambda, l'objet est accessible comme `this`. Elle est idéale pour appeler plusieurs méthodes ou accéder à plusieurs propriétés du même objet *non-nullable* sans répéter le nom de l'objet.
import java.util.ArrayList
fun main() {
val list = ArrayList()
// Utilisation de 'with' pour configurer la liste
val description = with(list) {
println("Configuration de la liste...")
add("Apple")
add("Banana")
add("Cherry")
"Liste initialisée avec $size éléments." // Résultat retourné
}
println(description) // Affiche: Liste initialisée avec 3 éléments.
println("Contenu liste: $list") // Affiche: Contenu liste: [Apple, Banana, Cherry]
} Elle est similaire à `run` (extension) mais n'est pas utilisable directement pour les null checks car l'objet doit être passé en premier argument.
`apply` : Configuration d'objet (retourne l'objet)
Signature : `fun`apply` exécute la lambda avec l'objet comme récepteur (`this`), mais retourne toujours l'objet lui-même. C'est la fonction parfaite pour la configuration d'objets après leur création, car vous pouvez définir plusieurs propriétés ou appeler des méthodes de setup dans la lambda, et obtenir l'objet configuré à la fin. Le résultat de la lambda elle-même est ignoré.
data class ServerConfig(var host: String = "localhost", var port: Int = 8080)
fun main() {
// Création et configuration avec 'apply'
val config = ServerConfig().apply {
// 'this' est l'instance de ServerConfig
host = "www.certiquizz.com"
port = 443
println("Configuration appliquée à $this") // Affiche l'objet en cours de config
// Pas besoin de retourner 'this', c'est automatique
}
// 'config' est l'objet ServerConfig configuré
println("Hôte final: ${config.host}, Port final: ${config.port}")
// Affiche: Hôte final: www.certiquizz.com, Port final: 443
}`also` : Actions supplémentaires (retourne l'objet)
Signature : `fun`also` exécute la lambda avec l'objet comme argument (`it`) et retourne l'objet lui-même. Elle est similaire à `apply` dans son retour, mais l'accès via `it` la rend idéale pour effectuer des actions (effets de bord) qui utilisent l'objet sans le modifier ou interférer avec la chaîne d'opérations principale. Pensez à "et faire aussi cela avec l'objet".
fun main() {
val numbers = mutableListOf("one", "two", "three")
val result = numbers
.also { println("Liste initiale: $it") } // Logging de l'état initial
.apply { add("four") } // Ajoute un élément (retourne la liste modifiée)
.also { println("Liste après ajout: $it") } // Logging de l'état après ajout
.map { it.length } // Transforme en longueurs
.also { println("Longueurs calculées: $it") } // Logging des longueurs
println("Résultat final (longueurs): $result")
/* Output:
Liste initiale: [one, two, three]
Liste après ajout: [one, two, three, four]
Longueurs calculées: [3, 3, 5, 4]
Résultat final (longueurs): [3, 3, 5, 4]
*/
}`also` est parfaite pour le logging, la validation intermédiaire, l'ajout de l'objet à une autre collection, etc., sans casser le flux des opérations qui utilisent `apply`, `map`, `filter`, etc.
Choisir la bonne fonction : guide rapide
Voici un petit guide pour vous aider à choisir :
- Besoin d'agir sur un nullable (null check) ?
- Utiliser `it` et retourner un résultat ? -> `?.let`
- Utiliser `this` et retourner un résultat ? -> `?.run`
- Configurer un objet (le retourner à la fin) ? -> `apply` (accès `this`)
- Effectuer une action/effet de bord sur l'objet et le retourner ? -> `also` (accès `it`)
- Exécuter des opérations sur un objet non-null (accès `this`) et retourner un résultat ? -> `with` (ou `run`)
- Calculer une valeur basée sur un objet (accès `it`) et retourner ce résultat ? -> `let`
- Exécuter un bloc comme expression (sans objet contexte spécifique) ? -> `run` (non-extension)
Récapitulatif : la puissance des fonctions de portée
Les fonctions de portée (`let`, `run`, `with`, `apply`, `also`) sont des outils idiomatiques essentiels en Kotlin :
- Elles exécutent un bloc de code dans le contexte d'un objet.
- Elles diffèrent par la manière d'accéder à l'objet (`this` ou `it`) et par leur valeur de retour (objet ou résultat lambda).
- Elles simplifient les null checks (`?.let`, `?.run`).
- Elles facilitent la configuration d'objets (`apply`).
- Elles permettent d'ajouter des actions/effets de bord proprement (`also`).
- Elles améliorent la lisibilité en groupant les opérations sur un objet (`with`, `run`).
Les utiliser à bon escient rend votre code Kotlin plus concis, plus expressif et plus sûr.