Pourquoi break et continue ne fonctionnent pas dans forEach Kotlin (et comment y remédier)
\n\nSi vous avez déjà programmé en Kotlin et essayé de sortir prématurément d'une boucle forEach avec break, vous avez probablement rencontré un soulignement rouge dans l'IDE et une erreur de compilation. Cela vous dit quelque chose ? Vous n'êtes pas seul – c'est l'une des questions les plus populaires sur Stack Overflow concernant Kotlin, avec des milliers de vues. Dans cet article, nous ne montrerons pas seulement comment contourner cette limitation, mais nous regarderons aussi sous le capot : pourquoi cela a été fait ainsi, comment fonctionne return à l'intérieur d'une lambda et quelles alternatives existent. C'est parti !
Exemple classique : l'erreur du débutant
\n\nImaginez que vous ayez une liste de nombres et que vous souhaitiez afficher uniquement les trois premiers éléments. Intuitivement, on voudrait écrire ceci :
\n\nval numbers = listOf(1, 2, 3, 4, 5)\nnumbers.forEach {\n println(it)\n if (it == 3) break // Erreur de compilation !\n}\n\n\nLe compilateur générera une erreur : 'break' and 'continue' are only allowed inside a loop. Pourquoi ? Pourtant, forEach semble être une boucle ? La réponse réside dans la façon dont forEach est réellement conçu.
Anatomie de forEach : ce n'est pas une boucle, mais un appel de fonction
\n\nOuvrons la bibliothèque standard de Kotlin et regardons la signature de forEach :
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {\n for (element in this) action(element)\n}\n\n\nLe mot-clé ici est inline. Cela signifie qu'à l'étape de compilation, le corps de la fonction est inséré directement à l'endroit de l'appel. Mais forEach reste une fonction d'ordre supérieur qui prend une autre fonction (lambda) comme argument. Et une lambda n'est pas un bloc de code de boucle, c'est un objet-fonction ordinaire. break et continue n'ont de sens qu'à l'intérieur des constructions for, while ou do-while, et non à l'intérieur d'une fonction quelconque.
En d'autres termes, lorsque vous écrivez numbers.forEach { ... }, vous ne créez pas une boucle – vous appelez une méthode qui utilise elle-même une boucle for en interne. Le compilateur ne peut pas « transmettre » un break de votre lambda à la boucle externe, car la lambda est une entité distincte. Ce n'est pas un bug, c'est une conception.
Comment fonctionne return dans forEach (un rebondissement inattendu)
\n\nEn revanche, return à l'intérieur de forEach fonctionne... mais pas comme vous l'attendez. Regardez :
fun main() {\n val numbers = listOf(1, 2, 3, 4, 5)\n numbers.forEach {\n if (it == 3) return // Sortie de main() !\n println(it)\n }\n println("Cette ligne ne sera pas exécutée")\n}\n\n\nLa sortie sera : 1 2. return ici est un non-local return (retour non local). Il termine non seulement la lambda, mais aussi toute la fonction externe (main). Cela fonctionne parce que forEach est déclaré comme inline. Le compilateur intègre le code de la lambda directement dans le corps de main, et return se réfère alors à la fun déclarée la plus proche, c'est-à-dire main.
Si vous souhaitez sortir uniquement de forEach, et non de toute la fonction, utilisez une étiquette (label) :
fun main() {\n val numbers = listOf(1, 2, 3, 4, 5)\n numbers.forEach loop@{\n if (it == 3) return@loop\n println(it)\n }\n println("Cette ligne sera exécutée")\n}\n\n\nSortie : 1 2 et Cette ligne sera exécutée. L'étiquette loop@ indique au compilateur : « sors de la lambda, mais pas de main ». En substance, c'est l'équivalent de continue – nous sautons l'itération actuelle et passons à l'élément suivant.