Wie hängt es mit Erweiterungsfunktionen zusammen? Warum ist with
eine Funktion kein Schlüsselwort?
Zu diesem Thema scheint es keine explizite Dokumentation zu geben, nur die Annahme von Wissen in Bezug auf Erweiterungen .
Wie hängt es mit Erweiterungsfunktionen zusammen? Warum ist with
eine Funktion kein Schlüsselwort?
Zu diesem Thema scheint es keine explizite Dokumentation zu geben, nur die Annahme von Wissen in Bezug auf Erweiterungen .
Antworten:
Es ist wahr, dass es anscheinend wenig Dokumentation für das Konzept der Empfänger gibt (nur eine kleine Randnotiz zu Erweiterungsfunktionen ), was angesichts der Tatsache überraschend ist:
with
, die keine Kenntnis von Empfängern gegeben könnte wie ein aussehen Stichwort ;Alle diese Themen sind dokumentiert, aber bei Empfängern wird nichts näher erläutert.
Zuerst:
Was ist ein Empfänger?
Jeder Codeblock in Kotlin kann einen (oder sogar mehrere) Typ als Empfänger haben , wodurch Funktionen und Eigenschaften des Empfängers in diesem Codeblock verfügbar werden, ohne ihn zu qualifizieren.
Stellen Sie sich einen Codeblock wie diesen vor:
Macht nicht viel Sinn, oder? Wenn Sie dies einem Funktionstyp von
(Int) -> Long
- woInt
ist der (einzige) Parameter und der Rückgabetyp istLong
- zuweisen, würde dies zu Recht zu einem Kompilierungsfehler führen. Sie können dies beheben, indem Sie den Funktionsaufruf einfach mit dem impliziten Einzelparameter qualifizierenit
. Beim DSL-Aufbau führt dies jedoch zu einer Reihe von Problemen:html { it.body { // how to access extensions of html here? } ... }
Dies kann bei HTML-DSL keine Probleme verursachen, bei anderen Anwendungsfällen jedoch.
it
Aufrufen verunreinigen , insbesondere für Lambdas, die ihren Parameter (der bald Empfänger sein wird) häufig verwenden.Hier kommen die Empfänger ins Spiel.
Durch Zuweisen dieses Codeblocks zu einem Funktionstyp, der
Int
als Empfänger (nicht als Parameter!) Verwendet wird, wird der Code plötzlich kompiliert:val intToLong: Int.() -> Long = { toLong() }
Was ist hier los?
Eine kleine Nebenbemerkung
In diesem Thema wird die Vertrautheit mit Funktionstypen vorausgesetzt , für Empfänger ist jedoch eine kleine Randnotiz erforderlich.
Funktionstypen können auch einen Empfänger haben, indem dem Typ und einem Punkt ein Präfix vorangestellt wird. Beispiele:
Int.() -> Long // taking an integer as receiver producing a long String.(Long) -> String // taking a string as receiver and long as parameter producing a string GUI.() -> Unit // taking an GUI and producing nothing
Bei solchen Funktionstypen wird der Parameterliste der Empfängertyp vorangestellt.
Code mit Empfängern auflösen
Es ist tatsächlich unglaublich einfach zu verstehen, wie Codeblöcke mit Empfängern behandelt werden:
Stellen Sie sich vor, dass der Codeblock ähnlich wie bei Erweiterungsfunktionen innerhalb der Klasse des Empfängertyps ausgewertet wird. Dies wird effektiv durch den Empfängertyp geändert.
In unserem früheren Beispiel führt
val intToLong: Int.() -> Long = { toLong() }
dies effektiv dazu, dass der Codeblock in einem anderen Kontext ausgewertet wird, als ob er in einer Funktion im Inneren platziert wäreInt
. Hier ist ein anderes Beispiel mit handgefertigten Typen, das dies besser veranschaulicht:class Bar class Foo { fun transformToBar(): Bar = TODO() } val myBlockOfCodeWithReceiverFoo: (Foo).() -> Bar = { transformToBar() }
effektiv wird (im Kopf nicht Code-weise - Sie können Klassen auf der JVM nicht wirklich erweitern):
class Bar class Foo { fun transformToBar(): Bar = TODO() fun myBlockOfCode(): Bar { return transformToBar() } } val myBlockOfCodeWithReceiverFoo: (Foo) -> Bar = { it.myBlockOfCode() }
Beachten Sie, dass wir innerhalb einer Klasse keinen
this
Zugriff benötigentransformToBar
- dasselbe passiert in einem Block mit einem Empfänger.Es kommt einfach so vor, dass in der Dokumentation dazu auch erklärt wird, wie ein äußerster Empfänger verwendet wird, wenn der aktuelle Codeblock zwei Empfänger hat, über einen qualifizierten dies .
Warten Sie, mehrere Empfänger?
Ja. Ein Codeblock kann mehrere Empfänger haben, dies hat jedoch derzeit keinen Ausdruck im Typsystem. Der einzige Weg, dies zu erreichen, sind mehrere Funktionen höherer Ordnung , die einen einzigen Empfängerfunktionstyp annehmen. Beispiel:
class Foo class Bar fun Foo.functionInFoo(): Unit = TODO() fun Bar.functionInBar(): Unit = TODO() inline fun higherOrderFunctionTakingFoo(body: (Foo).() -> Unit) = body(Foo()) inline fun higherOrderFunctionTakingBar(body: (Bar).() -> Unit) = body(Bar()) fun example() { higherOrderFunctionTakingFoo { higherOrderFunctionTakingBar { functionInFoo() functionInBar() } } }
Beachten Sie, dass @DslMarker Ihr Freund ist, wenn diese Funktion der Kotlin-Sprache für Ihr DSL ungeeignet erscheint !
Fazit
Warum ist das alles wichtig? Mit diesem Wissen:
toLong()
eine Erweiterungsfunktion auf eine Nummer schreiben können , anstatt die Nummer irgendwie referenzieren zu müssen. Vielleicht sollte Ihre Erweiterungsfunktion keine Erweiterung sein?with
, eine Standard - Bibliothek Funktion und kein Schlüsselwort, existiert - der Akt der den Umfang eines Blocks von Code zur Änderung der auf redudant Eingabe zu speichern ist so verbreitet, die Sprachdesigner es ausdrückte rechts in der Standardbibliothek.quelle
(Foo).() -> Unit
als Funktion, die einenFoo
als Empfänger und keinen Parameter nimmt. Wenn das stimmt, warum rufen Sie es dann mit einem Argument aufFoo()
?Funktionsliterale / Lambda mit Empfänger
Kotlin unterstützt das Konzept der „Funktionsliterale mit Empfängern“. Es ermöglicht den Zugriff auf sichtbare Methoden und Eigenschaften eines Empfängers eines Lambda in seinem Körper ohne zusätzliche Qualifikationsmerkmale . Dies ist sehr ähnlich zu Erweiterungsfunktionen, bei denen auch auf sichtbare Elemente des Empfängerobjekts innerhalb der Erweiterung zugegriffen werden kann.
Ein einfaches Beispiel, auch eine der größten Funktionen in der Kotlin-Standardbibliothek, ist
apply
:public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
Wie Sie sehen können, wird hier ein solches Funktionsliteral mit Empfänger als Argument verwendet
block
. Dieser Block wird einfach ausgeführt und der Empfänger (der eine Instanz von istT
) wird zurückgegeben. In Aktion sieht dies wie folgt aus:val foo: Bar = Bar().apply { color = RED text = "Foo" }
Wir instanziieren ein Objekt von
Bar
und rufenapply
es auf. Die Instanz vonBar
wird zum „Empfänger“. Dasblock
als Argument in{}
(Lambda-Ausdruck) übergebene, muss keine zusätzlichen Qualifikationsmerkmale verwenden, um auf die angezeigten sichtbaren Eigenschaftencolor
und zuzugreifen und diese zu änderntext
.Das Konzept der Lambdas mit Empfänger ist auch das wichtigste Merkmal beim Schreiben von DSLs mit Kotlin.
quelle
{...}
inapply{...}
nur die Lambda-Funktion als Argument fürapply
. Das Lambda ist ein nachfolgendes Lambda , bei dem es nicht in Klammern stehen muss. Es könnte tatsächlich zutreffen ({...}), was für mich beim ersten Lernen weniger verwirrend wäre. kotlinlang.org/docs/reference/…var greet: String.() -> Unit = { println("Hello $this") }
Dies definiert eine Variable vom Typ
String.() -> Unit
, die Ihnen sagtString
ist der Empfänger() -> Unit
ist der FunktionstypWie F. George oben erwähnt, sind alle Methoden dieser Empfänger kann in dem Verfahren Körper bezeichnet werden.
In unserem Beispiel wird
this
also zum Drucken derString
. Die Funktion kann durch Schreiben ...greet("Fitzgerald") // result is "Hello Fitzgerald"
Das obige Code-Snippet stammt aus Kotlin Function Literals with Receiver - Quick Introduction von Simon Wirtz.
quelle
greet
wird als eine Methode definiert, die einenString
Empfänger, aber keine Parameter hat. Ich verstehe also, wie wir anrufen können"Fitzgerald".greet()
, aber wie können wir anrufengreet("Fitzgerald")
?Einfach ausgedrückt (ohne zusätzliche Wörter oder Komplikationen) ist der "Empfänger" der Typ, der in der Erweiterungsfunktion oder im Klassennamen erweitert wird. Verwenden Sie die Beispiele in den obigen Antworten
fun Foo.functionInFoo(): Unit = TODO()
Typ "Foo" ist der "Empfänger"
var greet: String.() -> Unit = { println("Hello $this") }
Typ "String" ist der "Empfänger"
Zusätzlicher Tipp: Achten Sie in der Deklaration "fun" (function) vor dem Punkt (.) Auf die Klasse
fun receiver_class.function_name() { //... }
quelle
Typischerweise haben Sie in Java oder Kotlin Methoden oder Funktionen mit Eingabeparametern vom Typ T. In Kotlin können Sie auch Erweiterungsfunktionen haben, die einen Wert vom Typ T erhalten.
Wenn Sie eine Funktion haben, die einen String-Parameter akzeptiert, zum Beispiel:
fun hasWhitespace(line: String): Boolean { for (ch in line) if (ch.isWhitespace()) return true return false }
Konvertieren des Parameters in einen Empfänger (was Sie mit IntelliJ automatisch tun können):
fun String.hasWhitespace(): Boolean { for (ch in this) if (ch.isWhitespace()) return true return false }
Wir haben jetzt eine Erweiterungsfunktion, die einen String empfängt, und wir können mit auf den Wert zugreifen
this
quelle
Die Objektinstanz vor dem. ist der Empfänger. Dies ist im Wesentlichen der "Bereich", in dem Sie dieses Lambda definieren. Dies ist alles, was Sie wirklich wissen müssen, da die Funktionen und Eigenschaften (Variablen, Begleiter usw.), die Sie im Lambda verwenden, diejenigen sind, die in diesem Bereich bereitgestellt werden.
class Music(){ var track:String="" fun printTrack():Unit{ println(track) } } //Music class is the receiver of this function, in other words, the lambda can be piled after a Music class just like its extension function Since Music is an instance, refer to it by 'this', refer to lambda parameters by 'it', like always val track_name:Music.(String)->Unit={track=it;printTrack()} /*Create an Instance of Music and immediately call its function received by the name 'track_name', and exclusively available to instances of this class*/ Music().track_name("Still Breathing") //Output Still Breathing
Sie definieren diese Variable mit und allen Parametern und Rückgabetypen, die sie haben wird, aber unter allen definierten Konstrukten kann nur die Objektinstanz die Variable aufrufen, genau wie eine Erweiterungsfunktion, und ihr ihre Konstrukte liefern und sie somit "empfangen" . Ein Empfänger würde daher lose als ein Objekt definiert, für das eine Erweiterungsfunktion unter Verwendung des idiomatischen Stils von Lambdas definiert wird.
quelle