Was ist ein "Empfänger" in Kotlin?

78

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 .

F. George
quelle
1
Bestehende Antworten haben bereits gezeigt, was ein Empfänger ist, aber es kann hilfreich sein zu verstehen, woher das Wort kommt: In objektorientierten Sprachen wie Smalltalk, die Nachrichten weitergeben, werden Methodenaufrufe als an ein Objekt übergebene Nachrichten verstanden. Das Objekt, für das die Methode aufgerufen wird, ist der "Empfänger" der Nachricht.
LarsH

Antworten:

122

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:

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:

{ toLong() }

Macht nicht viel Sinn, oder? Wenn Sie dies einem Funktionstyp von (Int) -> Long- wo Intist der (einzige) Parameter und der Rückgabetyp ist Long- 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 qualifizieren it. Beim DSL-Aufbau führt dies jedoch zu einer Reihe von Problemen:

  • Bei verschachtelten DSL-Blöcken werden die oberen Ebenen schattiert:
    html { it.body { // how to access extensions of html here? } ... }
    Dies kann bei HTML-DSL keine Probleme verursachen, bei anderen Anwendungsfällen jedoch.
  • Es kann den Code mit itAufrufen 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 Intals 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äre Int. 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 thisZugriff benötigen transformToBar- 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:

  • Sie verstehen jetzt, warum Sie toLong()eine Erweiterungsfunktion auf eine Nummer schreiben können , anstatt die Nummer irgendwie referenzieren zu müssen. Vielleicht sollte Ihre Erweiterungsfunktion keine Erweiterung sein?
  • Sie können ein DSL für Ihre bevorzugte Markup-Sprache erstellen und dabei helfen, die eine oder andere zu analysieren ( wer benötigt reguläre Ausdrücke ? !).
  • Sie verstehen , warum 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.
  • (vielleicht) haben Sie beim Ableger etwas über Funktionstypen gelernt.
F. George
quelle
Frage: Ich lese (Foo).() -> Unitals Funktion, die einen Fooals Empfänger und keinen Parameter nimmt. Wenn das stimmt, warum rufen Sie es dann mit einem Argument auf Foo()?
Abhijit Sarkar
1
@AbhijitSarkar Funktionstypen mit einem Empfänger wird dem Empfänger die Parameterliste vorangestellt. Dies sollte im Hauptteil des Beitrags sein, Bearbeitung in ..
F. George
Sie können auch mehrere Empfänger haben, wenn Sie die Erweiterung innerhalb der Klasse definieren
Igor Wojda
@Panel Sie sprechen einen gültigen Punkt an, aber wenn jemand zu diesem Beitrag kommt, um zu verstehen, was ein Empfänger ist, sind die Unterschiede zwischen statischem und virtuellem Versand möglicherweise zu hoch ... zum Teufel, manchmal stolpert er sogar über mich.
F. George
1
Hier finden Sie detaillierte Erklärungen zu verschiedenen Empfängertypen. blog.kotlin-academy.com/…
Igor Wojda
13

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 ist T) wird zurückgegeben. In Aktion sieht dies wie folgt aus:

val foo: Bar = Bar().apply {
    color = RED
    text = "Foo"
}

Wir instanziieren ein Objekt von Barund rufen applyes auf. Die Instanz von Barwird zum „Empfänger“. Das blockals Argument in {}(Lambda-Ausdruck) übergebene, muss keine zusätzlichen Qualifikationsmerkmale verwenden, um auf die angezeigten sichtbaren Eigenschaften colorund zuzugreifen und diese zu ändern text.

Das Konzept der Lambdas mit Empfänger ist auch das wichtigste Merkmal beim Schreiben von DSLs mit Kotlin.

s1m0nw1
quelle
Da dies möglicherweise das erste Mal ist, dass Sie diese Syntax sehen, ist das {...}in apply{...}nur die Lambda-Funktion als Argument für apply. 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/…
Ben Butterworth
12
var greet: String.() -> Unit = { println("Hello $this") }

Dies definiert eine Variable vom Typ String.() -> Unit , die Ihnen sagt

  • Stringist der Empfänger
  • () -> Unit ist der Funktionstyp

Wie F. George oben erwähnt, sind alle Methoden dieser Empfänger kann in dem Verfahren Körper bezeichnet werden.

In unserem Beispiel wird thisalso zum Drucken der String. 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.

wooldridgetm
quelle
2
In diesem Fall können wir verschiedene Arten von Anrufen tätigen: grüßen ("mein Text"), was den gleichen Effekt hat wie "mein Text" .greet ()
Ultraon
Ich verstehe es nicht greetwird als eine Methode definiert, die einen StringEmpfänger, aber keine Parameter hat. Ich verstehe also, wie wir anrufen können "Fitzgerald".greet(), aber wie können wir anrufen greet("Fitzgerald")?
LarsH
(Zu
Ihrer Information,
9

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() {
   //...
}
GraSim
quelle
1
die einzig angemessene Antwort
Waldmann
1

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

Lou Morda
quelle
0

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.

Douglas Mbogz
quelle