La récursion est une technique utile rencontrée dans les langages fonctionnels et en Kotlin. Cependant, la récursion directe conduit souvent à un débordement de pile (StackOverflowError). Pour lutter contre cela, de nombreux langages introduisent l'optimisation de la récursion terminale (tail call optimization). En Kotlin, cela est réalisé explicitement via le mot-clé tailrec.
La récursion ordinaire crée de nouveaux cadres de pile à chaque appel. Lorsque la profondeur de la récursion est importante, cela entraîne une StackOverflowError. L'optimisation automatique de la récursion terminale n'est pas toujours possible, et la JVM ne la prend pas en charge comme, par exemple, les compilateurs d'autres langages (Scala, Erlang).
Kotlin permet de marquer une fonction avec le mot-clé tailrec. Si la fonction est réellement une récursion terminale ("tail call" - l'appel de soi-même est l'opération finale), le compilateur la remplace par une boucle, ce qui évite le débordement de pile.
Exemple de code :
tailrec fun factorial(n: Int, acc: Int = 1): Int = if (n == 0) acc else factorial(n - 1, acc * n) println(factorial(5)) // 120
Caractéristiques clés :
Que se passe-t-il si tailrec est utilisé, mais l'appel n'est pas en position terminale ?
Le compilateur renverra une erreur et n'appliquera pas l'optimisation.
tailrec fun sum(n: Int, acc: Int = 0): Int { println(n) // après cela, l'optimisation ne peut pas être effectuée return if (n == 0) acc else sum(n - 1, acc + n) } // Erreur de compilation : appel non en position terminale
Peut-on utiliser tailrec dans des fonctions open ou méthodes abstraites ?
Non, le mot-clé ne fonctionne que sur des fonctions final.
open class Base { // Erreur : // tailrec open fun test() {} }
Y a-t-il une différence de performance entre la récursion ordinaire et tailrec ?
Oui, tailrec transforme la fonction en boucle, réduisant les coûts de pile et éliminant StackOverflowError.
Un développeur marque toutes les fonctions récursives avec tailrec sans réfléchir à la possibilité d'optimisation. Les appels ne sont pas toujours en position terminale - au final, les projets ne se compilent pas et la récursion ne fonctionne pas efficacement.
Avantages :
Un développeur analyse la logique, utilise la récursion terminale dans des fonctions comme la recherche dans un arbre ou le calcul de factoriel. tailrec est utilisé là où cela est réellement possible. Cela aboutit à une boucle compilée efficacement sans débordement de pile.
Avantages :