Was ist Coroutine-Kontext und wie funktioniert es?
Dies ist ein übersetztes Kapitel aus dem Buc Kotlin Coroutines. Wenn Sie mir helfen möchten, die Übersetzung zu verbessern, finden Sie die Quellen auf GitHub.
Wenn Sie sich die Definitionen der Coroutine builders ansehen, werden Sie feststellen, dass ihr erster Parameter vom Typ CoroutineContext
ist.
Der Empfänger und der des letzten Arguments sind beide vom Typ CoroutineScope
1. Dieser CoroutineScope
scheint ein wichtiges Konzept zu sein, also schauen wir uns seine Definition an:
Es scheint lediglich ein Wrapper um CoroutineContext
zu sein. Sie möchten vielleicht darüber nachdenken, wie Continuation
definiert wurde.
Continuation
enthält ebenfalls CoroutineContext
. Dieser Typ wird von den wichtigsten Kotlin Coroutinen Elemente verwendet. Das muss ein wirklich wichtiges Konzept sein, also was ist es?
CoroutineContext
Interface
CoroutineContext
ist ein Interface, das ein Element oder eine Sammlung von Elementen repräsentiert. Es ist konzeptuell ähnlich wie eine Map oder eine Set-Sammlung: es handelt sich um ein indiziertes Set von Element
-Instanzen wie Job
, CoroutineName
, CoroutineDispatcher
, usw. Das Ungewöhnliche ist, dass jedes Element
auch ein CoroutineContext
ist. Also ist jedes Element in einer Sammlung selbst eine Sammlung.
Dieses Konzept ist ziemlich intuitiv. Stellen Sie sich eine Tasse vor. Sie ist ein einzelnes Element, aber sie ist auch eine Sammlung, die ein einzelnes Element enthält. Wenn Sie eine weitere Tasse hinzufügen, haben Sie eine Sammlung mit zwei Elementen.
Um eine bequeme Kontextspezifikation und -modifikation zu ermöglichen, ist jedes CoroutineContext
-Element selbst ein CoroutineContext
, wie im folgenden Beispiel (das Hinzufügen von Kontexten und das Festlegen eines Coroutine-Builders-Kontext wird später erklärt). Es ist viel einfacher, Kontexte zu spezifizieren oder hinzuzufügen, als ein explizites Set zu erstellen.
Jedes Element in dieser Menge hat einen eindeutigen Key
, der zur Identifizierung verwendet wird. Diese Schlüssel werden per Referenz verglichen.
Zum Beispiel: CoroutineName
oder Job
sind Implementierungen von CoroutineContext.Element
, welches das CoroutineContext
Interface umsetzt.
Es ist dasselbe mit SupervisorJob
, CoroutineExceptionHandler
und den Dispatchern vom Dispatchers
Objekt. Das sind die wichtigsten Coroutine-Kontexte. Sie werden in den nächsten Kapiteln erklärt.
Elemente im CoroutineContext finden
Da CoroutineContext
wie eine Sammlung ist, können wir ein Element mit einem konkreten Schlüssel mithilfe von get
finden. Eine andere Möglichkeit besteht darin, eckige Klammern zu verwenden, da in Kotlin die get
Methode ein Operator ist und mithilfe von eckigen Klammern anstelle eines expliziten Funktionsaufrufs aufgerufen werden kann. Genauso wie bei Map
: Wenn ein Element im Kontext ist, wird es zurückgegeben. Ist es nicht vorhanden, wird stattdessen null
zurückgegeben.
CoroutineContext
ist Teil der eingebauten Unterstützung für Kotlin-Coroutinen und wird daher auskotlin.coroutines
importiert, während Kontexte wieJob
oderCoroutineName
Teil der kotlinx.coroutines-Bibliothek sind und daher auskotlinx.coroutines
importiert werden müssen.
Um einen CoroutineName
zu finden, verwenden wir einfach CoroutineName
. Dies ist weder ein Typ noch eine Klasse, sondern ein Companion-Objekt. In Kotlin wirkt ein Klassenname, der für sich allein steht, als Referenz auf sein Companion-Objekt, somit ist ctx[CoroutineName]
einfach eine Abkürzung für ctx[CoroutineName.Key]
.
Es ist üblich in der kotlinx.coroutines Bibliothek, Companion Objects als Schlüssel für Elemente mit dem gleichen Namen zu verwenden. Dies macht es leichter sich zu merken2. Ein Schlüssel könnte auf eine Klasse (wie CoroutineName
) oder auf ein Interface (wie Job
) verweisen, das von vielen Klassen implementiert wird (wie Job
und SupervisorJob
).
Hinzufügen von Kontexten
Was CoroutineContext
wirklich nützlich macht, ist die Fähigkeit, zwei davon zusammenzufügen.
Wenn zwei Elemente mit unterschiedlichen Schlüsseln hinzugefügt werden, reagiert der resultierende Kontext auf beide Schlüssel.
Wenn ein weiteres Element mit demselben Schlüssel hinzugefügt wird, genau wie in einer Map, ersetzt das neue Element das vorherige.
Leerer CoroutineContext
Da CoroutineContext
wie eine Sammlung ist, haben wir auch einen leeren Kontext. Ein solcher Kontext liefert an sich keine Elemente; wenn wir ihn zu einem anderen Kontext hinzufügen, verhält er sich genau wie dieser andere Kontext.
Entfernen von Elementen
Elemente können auch durch ihren Schlüssel aus einem Kontext entfernt werden, indem die Funktion minusKey
verwendet wird.
Der Operator
minus
ist nicht fürCoroutineContext
überladen worden. Ich denke, dies liegt daran, dass seine Bedeutung nicht klar genug wäre, wie in Effective Kotlin Punkt 12: Die Bedeutung eines Operators sollte konsistent mit seinem Funktionsnamen sein erklärt wird.
Faltung des Kontexts
Wenn wir für jedes Element in einem Kontext etwas tun müssen, können wir die fold
Methode nutzen, die ähnlich zur fold
Methode in anderen Sammlungen ist. Sie erfordert:
- einen anfänglichen Akkumulatorwert;
- eine Operation, die den nächsten Zustand des Akkumulators erzeugt, basierend auf dem aktuellen Zustand und dem Element, auf das sie gerade angewendet wird.
Coroutine-Kontext und Ersteller
CoroutineContext
ist also nur eine Möglichkeit Daten zu speichern und zu übergeben. Standardmäßig übergibt das übergeordnete Element seinen Kontext an das untergeordnete Element, was einer der Effekte der Beziehung zwischen übergeordneten und untergeordneten Elementen ist. Man sagt, dass das untergeordnete Element den Kontext von seinem übergeordneten Element erbt.
Für jedes Kind könnte ein spezifischer Kontext im Argument definiert sein. Dieser Kontext überschreibt den der Eltern.
Eine vereinfachte Formel zur Berechnung eines Coroutine-Kontexts lautet:
Da neue Elemente immer alte mit dem gleichem Schlüssel ersetzen, überschreibt der Unterkontext stets Elemente mit dem gleichen Schlüssel aus dem übergeordneten Kontext. Die Standardwerte werden nur für Schlüssel verwendet, die ansonsten nirgendwo spezifiziert sind. Derzeit legen die Standardeinstellungen Dispatchers.Default
fest, wenn kein ContinuationInterceptor
gesetzt ist, und sie setzen nur CoroutineId
, wenn die Anwendung im Debug-Modus ist.
Es gibt einen speziellen Kontext namens Job
, der veränderbar ist und zur Kommunikation zwischen einem Coroutine-Unterkontext und seinem übergeordneten Kontext verwendet wird. Die nächsten Kapitel widmen sich den Auswirkungen dieser Kommunikation.
Zugriff auf den Kontext in einer unterbrechenden Funktion
CoroutineScope
hat eine Eigenschaft coroutineContext
, die verwendet werden kann, um auf den Kontext zuzugreifen. Aber was, wenn wir uns in einer regulären unterbrechenden Funktion befinden? Wie Sie sich vielleicht aus dem Kapitel Coroutinen unter der Haube erinnern, wird der Kontext durch Fortsetzungen referenziert, die an jede unterbrechende Funktion weitergegeben werden. Es ist also möglich, auf den Kontext des Übergeordneten in einer unterbrechenden Funktion zuzugreifen. Dazu verwenden wir die Eigenschaft coroutineContext
, die in jedem unterbrechenden Bereich verfügbar ist.
Erstellen unseres eigenen Kontexts
Es ist nicht üblich, aber wir können unseren eigenen Coroutine-Kontext ziemlich einfach erstellen. Am einfachsten ist es, eine Klasse zu erstellen, die das CoroutineContext.Element
-Interface implementiert. Eine solche Klasse benötigt eine Eigenschaft key
vom Typ CoroutineContext.Key<*>
. Dieser Schlüssel wird als der Schlüssel verwendet, der diesen Kontext identifiziert. Üblicherweise wird das Companion Object dieser Klasse als Schlüssel verwendet. So sieht eine einfache Implementierung eines Coroutine-Kontexts aus:
Ein solcher Kontext wird sich sehr ähnlich wie CoroutineName
verhalten: Er leitet sich von der Eltern- zur Kind-Koroutine weiter, aber jede Kind-Koroutine kann es mit einem anderen Kontext mit demselben Schlüssel überschreiben. Um dies in der Praxis zu sehen, finden Sie unten ein Beispiel für einen Kontext, der dazu dient, aufeinanderfolgende Zahlen auszugeben.
Ich habe gesehen, wie kundenspezifische Kontexte als eine Art von Dependency Injection verwendet werden - um leicht verschiedene Werte in der Produktion als bei Tests einzuspritzen. Ich denke jedoch nicht, dass dies zum Standardverfahren werden wird.
Zusammenfassung
CoroutineContext
ist konzeptionell ähnlich wie eine Map oder eine Set-Sammlung. Es handelt sich um ein indiziertes Set von Element
-Instanzen, wobei jedes Element
auch ein CoroutineContext
ist. Jedes Element darin hat einen eindeutigen Key
, der zur Identifizierung verwendet wird. Auf diese Weise ist CoroutineContext
einfach eine universelle Methode, um Objekte zu gruppieren und an Coroutinen zu übergeben. Diese Objekte werden von den Coroutinen aufbewahrt und können bestimmen, wie diese Coroutinen ausgeführt werden sollten (was ihr Zustand ist, in welchem Thread sie laufen, etc). In den folgenden Kapiteln werden wir uns mit den wichtigsten Kontexten von Coroutinen in der Kotlin-Bibliothek beschäftigen.
Klären wir die Nomenklatur. launch
ist eine Erweiterungsfunktion auf CoroutineScope
, daher ist CoroutineScope
ihr Empfängertyp. Der Empfänger der Erweiterungsfunktion ist das Objekt, auf das wir mit this
verweisen.
Das Begleiter-Objekt unten trägt den Namen Key
. Wir können Begleiter-Objekte benennen, aber das ändert wenig daran, wie sie verwendet werden. Der Standardname für ein Begleiter-Objekt ist Companion
, daher nutzen wir diesen Namen, wenn wir dieses Objekt über Reflexion referenzieren müssen oder wenn wir eine Erweiterungsfunktion darauf definieren. Hier verwenden wir stattdessen Key
.