Advertisement
  1. Code
  2. Kotlin

Kotlin von Grund auf neu: Erweiterte Funktionen

by
Read Time:14 minsLanguages:
This post is part of a series called Kotlin From Scratch.
Kotlin From Scratch: More Fun With Functions
Kotlin From Scratch: Classes and Objects

German (Deutsch) translation by Katharina Grigorovich-Nevolina (you can also view the original English article)

Kotlin ist eine funktionale Sprache, und das bedeutet, dass Funktionen im Vordergrund stehen. Die Sprache ist vollgepackt mit Funktionen, die Codierungsfunktionen einfach und ausdrucksstark machen. In diesem Beitrag erfahren Sie mehr über Erweiterungsfunktionen, Funktionen höherer Ordnung, Schließungen und Inline-Funktionen in Kotlin.

Im vorherigen Artikel haben Sie Informationen zu Funktionen der obersten Ebene, Lambda-Ausdrücken, anonymen Funktionen, lokalen Funktionen, Infix-Funktionen und schließlich zu Mitgliedsfunktionen in Kotlin erhalten. In diesem Tutorial erfahren Sie mehr über Funktionen in Kotlin, indem wir uns mit folgenden Themen befassen:

  • Erweiterungsfunktionen
  • Funktionen höherer Ordnung
  • Verschlüsse
  • Inline-Funktionen

1. Erweiterungsfunktionen

Wäre es nicht schön, wenn der String-Typ in Java eine Methode zum Großschreiben des ersten Buchstabens in einem String hätte - wie ucfirst() in PHP? Wir könnten diese Methode UpperCaseFirstLetter() aufrufen.

Um dies zu realisieren, können Sie eine String-Unterklasse erstellen, die den String-Typ in Java erweitert. Denken Sie jedoch daran, dass die String-Klasse in Java endgültig ist. Dies bedeutet, dass Sie sie nicht erweitern können. Eine mögliche Lösung für Kotlin wäre das Erstellen von Hilfsfunktionen oder Funktionen der obersten Ebene. Dies ist jedoch möglicherweise nicht ideal, da wir die Funktion zur automatischen Vervollständigung der IDE nicht verwenden können, um die Liste der für den String-Typ verfügbaren Methoden anzuzeigen. Was wirklich schön wäre, wäre, einer Klasse irgendwie eine Funktion hinzuzufügen, ohne von dieser Klasse erben zu müssen.

Nun, Kotlin hat uns mit einer weiteren großartigen Funktion ausgestattet: Erweiterungsfunktionen. Diese geben uns die Möglichkeit, eine Klasse mit neuen Funktionen zu erweitern, ohne von dieser Klasse erben zu müssen. Mit anderen Worten, wir müssen keinen neuen Subtyp erstellen oder den ursprünglichen Typ ändern.

Eine Erweiterungsfunktion wird außerhalb der Klasse deklariert, die erweitert werden soll. Mit anderen Worten, es handelt sich auch um eine Funktion der obersten Ebene (wenn Sie eine Auffrischung der Funktionen der obersten Ebene in Kotlin wünschen, besuchen Sie das Tutorial Mehr Spaß mit Funktionen in dieser Reihe).

Neben Erweiterungsfunktionen unterstützt Kotlin auch Erweiterungseigenschaften. In diesem Beitrag werden wir Erweiterungsfunktionen diskutieren und bis zu einem zukünftigen Beitrag warten, um die Erweiterungseigenschaften zusammen mit Klassen in Kotlin zu diskutieren.

Erweiterungsfunktion erstellen

Wie Sie im folgenden Code sehen können, haben wir wie gewohnt eine Funktion der obersten Ebene definiert, um eine Erweiterungsfunktion zu deklarieren. Diese Erweiterungsfunktion befindet sich in einem Paket namens com.chike.kotlin.strings.

Um eine Erweiterungsfunktion zu erstellen, müssen Sie den Namen der Klasse, die Sie erweitern, vor den Funktionsnamen stellen. Der Klassenname oder der Typ, für den die Erweiterung definiert ist, wird als Empfängertyp bezeichnet, und das Empfängerobjekt ist die Klasseninstanz oder der Wert, für den die Erweiterungsfunktion aufgerufen wird.

Beachten Sie, dass this Schlüsselwort this im Funktionskörper auf das Empfängerobjekt oder die Instanz verweist.

Aufrufen einer Erweiterungsfunktion

Nach dem Erstellen Ihrer Erweiterungsfunktion müssen Sie die Erweiterungsfunktion zunächst in andere Pakete oder Dateien importieren, die in dieser Datei oder diesem Paket verwendet werden sollen. Das Aufrufen der Funktion entspricht dann dem Aufrufen einer anderen Methode der Empfängertypklasse.

Im obigen Beispiel ist der Empfängertyp die String-Klasse und das Empfängerobjekt ist "chike". Wenn Sie eine IDE wie IntelliJ IDEA verwenden, die über die IntelliSense-Funktion verfügt, wird Ihre neue Erweiterungsfunktion in der Liste der anderen Funktionen in einem String-Typ vorgeschlagen.

IntelliJ IDEA intellisense featureIntelliJ IDEA intellisense featureIntelliJ IDEA intellisense feature

Java-Interoperabilität

Beachten Sie, dass Kotlin hinter den Kulissen eine statische Methode erstellt. Das erste Argument dieser statischen Methode ist das Empfängerobjekt. Daher ist es für Java-Aufrufer einfach, diese statische Methode aufzurufen und dann das Empfängerobjekt als Argument zu übergeben.

Wenn unsere Erweiterungsfunktion beispielsweise in einer StringUtils.kt-Datei deklariert wurde, erstellt der Kotlin-Compiler eine Java-Klasse StringUtilsKt mit einer statischen Methode UpperCaseFirstLetter().

Dies bedeutet, dass Java-Aufrufer die Methode wie bei jeder anderen statischen Methode einfach aufrufen können, indem sie auf ihre generierte Klasse verweisen.

Denken Sie daran, dass dieser Java-Interop-Mechanismus der Funktionsweise von Funktionen der obersten Ebene in Kotlin ähnelt, wie wir im Beitrag Mehr Spaß mit Funktionen beschrieben haben!

Erweiterungsfunktionen vs. Mitgliedsfunktionen

Beachten Sie, dass Erweiterungsfunktionen Funktionen, die bereits in einer Klasse oder Schnittstelle deklariert sind, nicht überschreiben können - sogenannte Elementfunktionen (wenn Sie eine Auffrischung der Elementfunktionen in Kotlin wünschen, lesen Sie das vorherige Tutorial in dieser Reihe). Wenn Sie also eine Erweiterungsfunktion mit genau derselben Funktionssignatur definiert haben - denselben Funktionsnamen und dieselbe Anzahl, denselben Typ und dieselbe Reihenfolge von Argumenten, unabhängig vom Rückgabetyp -, wird sie vom Kotlin-Compiler nicht aufgerufen. Beim Kompilieren sucht der Kotlin-Compiler beim Aufrufen einer Funktion zunächst nach einer Übereinstimmung in den im Instanztyp oder in seinen Oberklassen definierten Elementfunktionen. Wenn es eine Übereinstimmung gibt, wird diese Mitgliedsfunktion aufgerufen oder gebunden. Wenn keine Übereinstimmung vorliegt, ruft der Compiler eine Erweiterungsfunktion dieses Typs auf.

Zusammengefasst also: Mitgliedsfunktionen gewinnen immer.

Sehen wir uns ein praktisches Beispiel an.

Im obigen Code haben wir einen Typ namens Student mit zwei Elementfunktionen definiert: printResult() und expel(). Wir haben dann zwei Erweiterungsfunktionen definiert, die dieselben Namen wie die Elementfunktionen haben.

Rufen wir die Funktion printResult() auf und sehen das Ergebnis.

Wie Sie sehen können, war die aufgerufene oder gebundene Funktion die Elementfunktion und nicht die Erweiterungsfunktion mit derselben Funktionssignatur (obwohl IntelliJ IDEA Ihnen dennoch einen Hinweis dazu geben würde).

Der Aufruf der Mitgliedsfunktion expel() und der Erweiterungsfunktion expel(reason: String) führt jedoch zu unterschiedlichen Ergebnissen, da die Funktionssignaturen unterschiedlich sind.

Funktionen zur Erweiterung von Mitgliedern

Sie werden eine Erweiterungsfunktion die meiste Zeit als Funktion der obersten Ebene deklarieren. Beachten Sie jedoch, dass Sie sie auch als Elementfunktionen deklarieren können.

Im obigen Code haben wir eine Erweiterungsfunktion exFunction() vom Typ ClassB in einer anderen Klasse ClassA deklariert. Der Versandempfänger ist die Instanz der Klasse, in der die Erweiterung deklariert ist, und die Instanz des Empfängertyps der Erweiterungsmethode wird als Erweiterungsempfänger bezeichnet. Beachten Sie, dass der Compiler den Nebenstellenempfänger auswählt, wenn zwischen dem Versandempfänger und dem Nebenstellenempfänger ein Namenskonflikt oder eine Schattenbildung auftritt.

Im obigen Codebeispiel ist der Erweiterungsempfänger eine Instanz von ClassB. Dies bedeutet, dass die toString()-Methode vom Typ ClassB ist, wenn sie innerhalb der Erweiterungsfunktion exFunction() aufgerufen wird. Damit wir stattdessen die toString()-Methode des Versandempfängers ClassA aufrufen können, müssen wir ein qualifiziertes this verwenden:

2. Funktionen höherer Ordnung

Eine Funktion höherer Ordnung ist nur eine Funktion, die eine andere Funktion (oder einen Lambda-Ausdruck) als Parameter verwendet, eine Funktion zurückgibt oder beides ausführt. Die Auflistungsfunktion last() ist ein Beispiel für eine Funktion höherer Ordnung aus der Standardbibliothek.

Hier haben wir ein Lambda an die last-Funktion übergeben, die als Prädikat für die Suche innerhalb einer Teilmenge von Elementen dient. Wir werden uns nun mit der Erstellung eigener Funktionen höherer Ordnung in Kotlin befassen.

Erstellen einer Funktion höherer Ordnung

Wenn Sie sich die Funktion circleOperation() unten ansehen, hat sie zwei Parameter. Der erste, radius, akzeptiert ein Double, und der zweite, op, ist eine Funktion, die ein Double als Eingabe akzeptiert und auch ein Double als Ausgabe zurückgibt - wir können prägnanter sagen, dass der zweite Parameter "eine Funktion von Double zu Double" ist. .

Beachten Sie, dass die Parametertypen der op-Funktion für die Funktion in Klammern () eingeschlossen sind und der Ausgabetyp durch einen Pfeil getrennt ist. Die Funktion circleOperation() ist ein typisches Beispiel für eine Funktion höherer Ordnung, die eine Funktion als Parameter akzeptiert.

Aufrufen einer Funktion höherer Ordnung

Beim Aufruf dieser Funktion circleOperation() übergeben wir ihr eine weitere Funktion, calArea(). (Beachten Sie, dass der Funktionsaufruf nicht kompiliert wird, wenn die Methodensignatur der übergebenen Funktion nicht mit der deklariert, die die Funktion höherer Ordnung deklariert.)

Um die Funktion calArea() als Parameter an circleOperation() zu übergeben, müssen wir :: voranstellen und die Klammern () weglassen.

Wenn Sie Funktionen höherer Ordnung mit Bedacht einsetzen, kann unser Code leichter lesbar und verständlicher werden.

Lambdas und Funktionen höherer Ordnung

Wir können ein Lambda (oder Funktionsliteral) auch direkt an eine Funktion höherer Ordnung übergeben, wenn wir die Funktion aufrufen:

Denken Sie daran, damit wir vermeiden, das Argument explizit zu benennen, können wir den für uns automatisch generierten it-Argumentnamen nur verwenden, wenn das Lambda ein Argument hat. (Wenn Sie eine Auffrischung zu Lambda in Kotlin wünschen, besuchen Sie das Tutorial Mehr Spaß mit Funktionen in dieser Reihe.)

Rückgabe einer Funktion

Denken Sie daran, dass Funktionen höherer Ordnung nicht nur eine Funktion als Parameter akzeptieren, sondern auch eine Funktion an Aufrufer zurückgeben können.

Hier gibt die Funktion multiplier() eine Funktion zurück, die den angegebenen Faktor auf eine beliebige übergebene Zahl anwendet. Diese zurückgegebene Funktion ist ein Lambda (oder Funktionsliteral) von double bis double (was bedeutet, dass der Eingabeparameter der zurückgegebenen Funktion ein doppelter Typ ist und das Ausgabeergebnis auch ein doppelter Typ ist).

Um dies zu testen, haben wir den Faktor zwei übergeben und die zurückgegebene Funktion dem Variablenverdoppler zugewiesen. Wir können dies wie eine normale Funktion aufrufen, und jeder Wert, den wir übergeben, wird verdoppelt.

3. Verschlüsse

Ein Abschluss ist eine Funktion, die Zugriff auf Variablen und Parameter hat, die in einem äußeren Bereich definiert sind.

Im obigen Code verwendet das an die Sammlungsfunktion filter() übergebene Lambda den Parameter length der äußeren Funktion printFilteredNamesByLength(). Beachten Sie, dass dieser Parameter außerhalb des Bereichs des Lambda definiert ist, das Lambda jedoch weiterhin auf die length zugreifen kann. Dieser Mechanismus ist ein Beispiel für das Schließen in der funktionalen Programmierung.

4. Inline-Funktionen

In Mehr Spaß mit Funktionen erwähnte ich, dass der Kotlin-Compiler in früheren Java-Versionen hinter den Kulissen beim Erstellen von Lambda-Ausdrücken eine anonyme Klasse erstellt.

Leider führt dieser Mechanismus zu Overhead, da jedes Mal, wenn wir ein Lambda erstellen, eine anonyme Klasse unter der Haube erstellt wird. Außerdem fügt ein Lambda, das den äußeren Funktionsparameter oder die lokale Variable mit einem Abschluss verwendet, seinen eigenen Speicherzuweisungsaufwand hinzu, da dem Heap bei jedem Aufruf ein neues Objekt zugewiesen wird.

Vergleichen von Inline-Funktionen mit normalen Funktionen

Um diesen Aufwand zu vermeiden, hat uns das Kotlin-Team den inline-Modifikator für Funktionen zur Verfügung gestellt. Eine Funktion höherer Ordnung mit dem inline-Modifikator wird während der Codekompilierung eingefügt. Mit anderen Worten, der Compiler kopiert das Lambda (oder Funktionsliteral) sowie den Funktionskörper höherer Ordnung und fügt sie an der Aufrufstelle ein.

Schauen wir uns ein praktisches Beispiel an.

Im obigen Code haben wir eine Funktion circleOperation() höherer Ordnung, die den inline-Modifikator nicht hat. Lassen Sie uns nun den Kotlin-Bytecode sehen, der beim Kompilieren und Dekompilieren des Codes generiert wird, und ihn dann mit einem Code vergleichen, der den inline-Modifikator enthält.

Im obigen generierten Java-Bytecode sehen Sie, dass der Compiler die Funktion circleOperation() innerhalb der main()-Methode aufgerufen hat.

Geben wir nun stattdessen die Funktion höherer Ordnung als inline an und sehen auch den generierten Bytecode.

Um eine Funktion höherer Ordnung inline zu machen, müssen wir den inline-Modifikator vor dem Schlüsselwort fun einfügen, genau wie im obigen Code. Überprüfen wir auch den für diese Inline-Funktion generierten Bytecode.

Wenn Sie sich den generierten Bytecode für die Inline-Funktion innerhalb der main()-Funktion ansehen, können Sie feststellen, dass sie statt des Aufrufs der circleOperation()-Funktion nun den circleOperation()-Funktionskörper einschließlich des Lambda-Bodys kopiert und an ihrer Aufrufstelle eingefügt hat.

Mit diesem Mechanismus wurde unser Code erheblich optimiert - keine anonymen Klassen mehr erstellt oder zusätzliche Speicherzuweisungen vorgenommen. Aber seien Sie sich sehr bewusst, dass wir hinter den Kulissen einen größeren Bytecode haben würden als zuvor. Aus diesem Grund wird dringend empfohlen, nur kleinere Funktionen höherer Ordnung zu integrieren, die Lambda als Parameter akzeptieren.

Viele der Funktionen höherer Ordnung der Standardbibliothek in Kotlin verfügen über den Inline-Modifikator. Wenn Sie beispielsweise einen Blick auf die Funktionen der Erfassungsoperation filter() und first() werfen, werden Sie feststellen, dass sie über den inline-Modifikator verfügen und auch klein sind.

Denken Sie daran, keine normalen Funktionen zu integrieren, die kein Lambda als Parameter akzeptieren! Sie werden kompiliert, aber es würde keine signifikante Leistungsverbesserung geben (IntelliJ IDEA würde sogar einen Hinweis darauf geben).

Der noinline-Modifikator

Wenn eine Funktion mehr als zwei Lambda-Parameter enthält, können Sie mithilfe des Modifikators noinline für den Parameter entscheiden, welches Lambda nicht inline geschaltet werden soll. Diese Funktionalität ist besonders nützlich für einen Lambda-Parameter, der viel Code aufnimmt. Mit anderen Worten, der Kotlin-Compiler kopiert und fügt das Lambda, in dem es aufgerufen wird, nicht ein und erstellt stattdessen eine anonyme Klasse hinter den Kulissen.

Hier haben wir den noinline-Modifikator in den zweiten Lambda-Parameter eingefügt. Beachten Sie, dass dieser Modifikator nur gültig ist, wenn die Funktion über den inline-Modifikator verfügt.

Stapelverfolgung in Inline-Funktionen

Beachten Sie, dass sich der Methodenaufrufstapel in der Stapelverfolgung von einer normalen Funktion ohne den Inline-Modifikator unterscheidet, wenn eine Ausnahme innerhalb einer inline-Funktion ausgelöst wird. Dies liegt an dem Kopier- und Einfügemechanismus, den der Compiler für Inline-Funktionen verwendet. Das Coole ist, dass IntelliJ IDEA uns hilft, den Methodenaufruf-Stack im Stack-Trace für eine Inline-Funktion einfach zu navigieren. Sehen wir uns ein Beispiel an.

Im obigen Code wird absichtlich eine Ausnahme in der Inline-Funktion myFunc() ausgelöst. Lassen Sie uns nun den Stack-Trace in IntelliJ IDEA sehen, wenn der Code ausgeführt wird. Wenn Sie sich den Screenshot unten ansehen, sehen Sie, dass wir zwei Navigationsoptionen zur Auswahl haben: den Inline-Funktionskörper oder die Inline-Funktionsaufrufseite. Wenn Sie Ersteres auswählen, gelangen Sie zu dem Punkt, an dem die Ausnahme im Funktionskörper ausgelöst wurde, während Letzteres uns zu dem Punkt führt, an dem die Methode aufgerufen wurde.

IntelliJ IDEA stack trace for inline functionIntelliJ IDEA stack trace for inline functionIntelliJ IDEA stack trace for inline function

Wenn die Funktion keine Inline-Funktion wäre, würde unsere Stapelverfolgung derjenigen entsprechen, mit der Sie möglicherweise bereits vertraut sind:

IntelliJ IDEA stack trace for normal functionIntelliJ IDEA stack trace for normal functionIntelliJ IDEA stack trace for normal function

Schlussfolgerung

In diesem Tutorial haben Sie noch mehr gelernt, was Sie mit Funktionen in Kotlin tun können. Wir haben Folgendes behandelt:

  • Erweiterungsfunktionen
  • Funktionen höherer Ordnung
  • Verschlüsse
  • Inline-Funktionen

Im nächsten Tutorial der Kotlin From Scratch-Reihe werden wir uns mit objektorientierter Programmierung befassen und lernen, wie Klassen in Kotlin funktionieren. Bis bald!

Um mehr über die Kotlin-Sprache zu erfahren, empfehle ich, die Kotlin-Dokumentation zu besuchen. Oder sehen Sie sich einige unserer anderen Beiträge zur Entwicklung von Android-Apps hier auf Envato Tuts+ an!

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.