Rekursion ist eine nützliche Technik, die in funktionalen Sprachen und in Kotlin vorkommt. Allerdings führt direkte Rekursion oft zu einem Stacküberlauf (StackOverflowError). Um dem entgegenzuwirken, wurde in vielen Sprachen die Optimierung der Schwanzrekursion (tail call optimization) eingeführt. In Kotlin wird dies explizit durch das Schlüsselwort tailrec realisiert.
Gewöhnliche Rekursion erstellt neue Stackrahmen bei jedem Aufruf. Wenn die Rekursionstiefe groß ist, kann dies zu StackOverflowError führen. Automatische Optimierung der Schwanzrekursion ist nicht immer möglich, und die JVM unterstützt sie nicht wie beispielsweise die Compiler einiger anderer Sprachen (Scala, Erlang).
Kotlin erlaubt es, eine Funktion mit dem Schlüsselwort tailrec zu kennzeichnen. Wenn die Funktion tatsächlich eine Schwanzrekursion ist ("tail call" – der resultierende Aufruf der Funktion selbst ist die letzte Operation), ersetzt der Compiler sie durch eine Schleife, wodurch ein Stacküberlauf vermieden wird.
Beispielcode:
tailrec fun factorial(n: Int, acc: Int = 1): Int = if (n == 0) acc else factorial(n - 1, acc * n) println(factorial(5)) // 120
Schlüsselfeatures:
Was passiert, wenn tailrec steht, der Aufruf jedoch nicht in der Schwanzposition ist?
Der Compiler gibt einen Fehler aus und wendet die Optimierung nicht an.
tailrec fun sum(n: Int, acc: Int = 0): Int { println(n) // danach kann nicht optimiert werden return if (n == 0) acc else sum(n - 1, acc + n) } // Kompilierungsfehler: Aufruf nicht in Schwanzposition
Kann tailrec in open-Funktionen oder abstrakten Methoden verwendet werden?
Nein, das Schlüsselwort funktioniert nur bei finalen Funktionen.
open class Base { // Fehler: // tailrec open fun test() {} }
Gibt es einen Unterschied in der Leistung zwischen gewöhnlicher Rekursion und tailrec?
Ja, tailrec wandelt die Funktion in eine Schleife um, wodurch der Aufwand für den Stack reduziert wird und StackOverflowError vermieden wird.
Ein Entwickler kennzeichnet alle rekursiven Funktionen mit tailrec, ohne zu überlegen, ob die Optimierung angewendet werden kann. Aufrufe stehen nicht immer in der Schwanzposition - am Ende lassen sich die Projekte nicht kompilieren und die Rekursion funktioniert nicht effizient.
Vorteile:
Ein Entwickler analysiert die Logik, wandelt die Schwanzrekursion in Funktionen wie Baum-Suche oder Fakultät um. tailrec wird dort verwendet, wo es tatsächlich möglich ist. Es entsteht eine effizient kompilierte Schleife ohne Stacküberlauf.
Vorteile: