Kotlin generierter Code-Leitfaden

Beschreibt genau, welcher Kotlin-Code vom Protocol Buffer Compiler zusätzlich zum Java-Code generiert wird.

Alle Unterschiede zwischen proto2, proto3 und Edges generiertem Code werden hervorgehoben – beachten Sie, dass diese Unterschiede im generierten Code liegen, wie in diesem Dokument beschrieben, nicht in den zugrunde liegenden Nachrichtenklassen/-schnittstellen, die in beiden Versionen gleich sind. Sie sollten den proto2 Sprachleitfaden, proto3 Sprachleitfaden und/oder den Editionsleitfaden lesen, bevor Sie dieses Dokument lesen.

Compiler-Aufruf

Der Protocol Buffer Compiler erzeugt Kotlin-Code, der auf Java-Code aufbaut. Daher muss er mit zwei Kommandozeilenflags aufgerufen werden: --java_out= und --kotlin_out=. Der Parameter für die Option --java_out= ist das Verzeichnis, in dem der Compiler Ihren Java-Output schreiben soll, und dasselbe gilt für --kotlin_out=. Für jede eingegebene .proto-Datei erstellt der Compiler eine Wrapper-.java-Datei mit einer Java-Klasse, die die .proto-Datei selbst darstellt.

Unabhängig davon, ob Ihre .proto-Datei eine Zeile wie die folgende enthält oder nicht

option java_multiple_files = true;

Der Compiler erstellt separate .kt-Dateien für jede der Klassen und Factory-Methoden, die er für jede in der .proto-Datei deklarierte Top-Level-Nachricht generieren wird.

Der Java-Paketname für jede Datei ist derselbe wie der des generierten Java-Codes, wie im Java generierter Code Referenz beschrieben.

Die Ausgabedatei wird durch die Verkettung des Parameters für --kotlin_out=, des Paketnamens (mit durch Schrägstriche [/] ersetzten Punkten [.]) und der Dateiendung Kt.kt ausgewählt.

Nehmen wir also zum Beispiel an, Sie rufen den Compiler wie folgt auf:

protoc --proto_path=src --java_out=build/gen/java --kotlin_out=build/gen/kotlin src/foo.proto

Wenn das Java-Paket von foo.proto com.example ist und eine Nachricht namens Bar enthält, generiert der Protocol Buffer Compiler die Datei build/gen/kotlin/com/example/BarKt.kt. Der Protocol Buffer Compiler erstellt automatisch die Verzeichnisse build/gen/kotlin/com und build/gen/kotlin/com/example, falls erforderlich. Er erstellt jedoch nicht build/gen/kotlin, build/gen oder build; diese müssen bereits existieren. Sie können mehrere .proto-Dateien in einem einzigen Aufruf angeben; alle Ausgabedateien werden gleichzeitig generiert.

Nachrichten

Angesichts einer einfachen Nachrichten Deklaration:

message FooBar {}

Der Protocol Buffer Compiler generiert zusätzlich zum generierten Java-Code ein Objekt namens FooBarKt sowie zwei Top-Level-Funktionen mit folgender Struktur

object FooBarKt {
  class Dsl private constructor { ... }
}
inline fun fooBar(block: FooBarKt.Dsl.() -> Unit): FooBar
inline fun FooBar.copy(block: FooBarKt.Dsl.() -> Unit): FooBar

Verschachtelte Typen

Eine Nachricht kann innerhalb einer anderen Nachricht deklariert werden. Zum Beispiel:

message Foo {
  message Bar { }
}

In diesem Fall verschachtelt der Compiler das BarKt-Objekt und die bar-Factory-Methode innerhalb von FooKt, obwohl die copy-Methode auf Top-Level-Ebene verbleibt.

object FooKt {
  class Dsl { ... }
  object BarKt {
    class Dsl private constructor { ... }
  }
  inline fun bar(block: FooKt.BarKt.Dsl.() -> Unit): Foo.Bar
}
inline fun foo(block: FooKt.Dsl.() -> Unit): Foo
inline fun Foo.copy(block: FooKt.Dsl.() -> Unit): Foo
inline fun Foo.Bar.copy(block: FooKt.BarKt.Dsl.() -> Unit): Foo.Bar

Felder

Zusätzlich zu den im vorherigen Abschnitt beschriebenen Methoden generiert der Protocol Buffer Compiler mutable Eigenschaften in der DSL für jedes Feld, das innerhalb der Nachricht in der .proto-Datei definiert ist. (Kotlin leitet bereits Lese-/Schreibe-Eigenschaften aus den von Java generierten Gettern für das Nachrichtenobjekt ab.)

Beachten Sie, dass Eigenschaften immer im Camelcase-Stil benannt werden, auch wenn der Feldname in der .proto-Datei kleingeschrieben mit Unterstrichen ist (wie er sein sollte). Die Fallumwandlung funktioniert wie folgt

  1. Für jeden Unterstrich im Namen wird der Unterstrich entfernt und der folgende Buchstabe großgeschrieben.
  2. Wenn dem Namen ein Präfix hinzugefügt wird (z. B. "clear"), wird der erste Buchstabe großgeschrieben. Andernfalls wird er kleingeschrieben.

Somit wird das Feld foo_bar_baz zu fooBarBaz.

In einigen Sonderfällen, in denen ein Feldname mit reservierten Wörtern in Kotlin oder bereits im Protobuf-Bibliothek definierten Methoden kollidiert, wird ein zusätzlicher Unterstrich angehängt. Beispielsweise ist der "clearer" für ein Feld namens in clearIn_().

Singuläre Felder

Für diese Felddefinition:

int32 foo = 1;

Der Compiler generiert die folgenden Accessoren in der DSL

  • fun hasFoo(): Boolean: Gibt true zurück, wenn das Feld gesetzt ist. Dies wird nicht für Felder mit impliziter Anwesenheit generiert.
  • var foo: Int: Der aktuelle Wert des Feldes. Wenn das Feld nicht gesetzt ist, wird der Standardwert zurückgegeben.
  • fun clearFoo(): Löscht den Wert des Feldes. Nach dem Aufruf dieser Methode gibt hasFoo() false zurück und getFoo() gibt den Standardwert zurück.

Für andere einfache Feldtypen wird der entsprechende Java-Typ gemäß der Tabelle der skalaren Werttypen gewählt. Für Nachrichten- und Enum-Typen wird der Werttyp durch die Nachrichten- oder Enum-Klasse ersetzt. Da der Nachrichtentyp weiterhin in Java definiert ist, werden unsignierte Typen in der Nachricht zur Kompatibilität mit Java und älteren Versionen von Kotlin mithilfe der standardmäßigen entsprechenden signierten Typen in der DSL dargestellt.

Eingebettete Nachrichtenfelder

Beachten Sie, dass es keine spezielle Behandlung von Subnachrichten gibt. Wenn Sie beispielsweise ein Feld haben

optional Foo my_foo = 1;

müssen Sie Folgendes schreiben

myFoo = foo {
  ...
}

Dies liegt im Allgemeinen daran, dass der Compiler nicht weiß, ob Foo überhaupt eine Kotlin-DSL hat oder z. B. nur die Java-APIs generiert hat. Das bedeutet, dass Sie nicht darauf warten müssen, dass Nachrichten, von denen Sie abhängen, die Kotlin-Code-Generierung hinzufügen.

Wiederholte Felder

Für diese Felddefinition:

repeated string foo = 1;

Der Compiler generiert die folgenden Member in der DSL

  • class FooProxy: DslProxy, ein nicht instanziierbarer Typ, der nur in Generika verwendet wird
  • val fooList: DslList<String, FooProxy>, eine schreibgeschützte Ansicht der Liste der aktuellen Elemente im wiederholten Feld
  • fun DslList<String, FooProxy>.add(value: String), eine Erweiterungsfunktion, die das Hinzufügen von Elementen zum wiederholten Feld ermöglicht
  • operator fun DslList<String, FooProxy>.plusAssign(value: String), ein Alias für add
  • fun DslList<String, FooProxy>.addAll(values: Iterable<String>), eine Erweiterungsfunktion, die das Hinzufügen eines Iterable von Elementen zum wiederholten Feld ermöglicht
  • operator fun DslList<String, FooProxy>.plusAssign(values: Iterable<String>), ein Alias für addAll
  • operator fun DslList<String, FooProxy>.set(index: Int, value: String), eine Erweiterungsfunktion zum Festlegen des Werts des Elements am angegebenen nullbasierten Index
  • fun DslList<String, FooProxy>.clear(), eine Erweiterungsfunktion zum Löschen des Inhalts des wiederholten Feldes

Diese ungewöhnliche Konstruktion ermöglicht es fooList, sich "wie" eine mutable Liste im Geltungsbereich der DSL zu verhalten, unterstützt nur die vom zugrunde liegenden Builder unterstützten Methoden, verhindert aber, dass die Mutabilität die DSL "verlässt", was zu verwirrenden Nebeneffekten führen könnte.

Für andere einfache Feldtypen wird der entsprechende Java-Typ gemäß der Tabelle der skalaren Werttypen gewählt. Für Nachrichten- und Enum-Typen ist der Typ die Nachrichten- oder Enum-Klasse.

Oneof-Felder

Für diese Oneof-Felddatendefinition

oneof oneof_name {
    int32 foo = 1;
    ...
}

Der Compiler generiert die folgenden Zugriffsmethoden in der DSL

  • val oneofNameCase: OneofNameCase: gibt an, welches, falls vorhanden, der Felder von oneof_name gesetzt ist; siehe die Java-Code-Referenz für den Rückgabetyp
  • fun hasFoo(): Boolean: Gibt true zurück, wenn der Oneof-Fall FOO ist.
  • val foo: Int: Gibt den aktuellen Wert von oneof_name zurück, wenn der Oneof-Fall FOO ist. Andernfalls gibt er den Standardwert dieses Feldes zurück.

Für andere einfache Feldtypen wird der entsprechende Java-Typ gemäß der Tabelle der skalaren Werttypen gewählt. Für Nachrichten- und Enum-Typen wird der Werttyp durch die Nachrichten- oder Enum-Klasse ersetzt.

Map-Felder

Für diese Map-Felddefinition

map<int32, int32> weight = 1;

Der Compiler generiert die folgenden Member in der DSL-Klasse

  • class WeightProxy private constructor(): DslProxy(), ein nicht instanziierbarer Typ, der nur in Generika verwendet wird
  • val weight: DslMap<Int, Int, WeightProxy>, eine schreibgeschützte Ansicht der aktuellen Einträge im Map-Feld
  • fun DslMap<Int, Int, WeightProxy>.put(key: Int, value: Int): fügt den Eintrag zu diesem Map-Feld hinzu
  • operator fun DslMap<Int, Int, WeightProxy>.put(key: Int, value: Int): Alias für put unter Verwendung der Operator-Syntax
  • fun DslMap<Int, Int, WeightProxy>.remove(key: Int): entfernt den Eintrag, der mit key assoziiert ist, falls vorhanden
  • fun DslMap<Int, Int, WeightProxy>.putAll(map: Map<Int, Int>): fügt alle Einträge aus der angegebenen Map zu diesem Map-Feld hinzu und überschreibt frühere Werte für bereits vorhandene Schlüssel
  • fun DslMap<Int, Int, WeightProxy>.clear(): löscht alle Einträge aus diesem Map-Feld

Erweiterungen (Extensions)

Gegeben eine proto2- oder Edges-Nachricht mit einem Erweiterungsbereich

message Foo {
  extensions 100 to 199;
}

Der Protocol Buffer Compiler fügt der FooKt.Dsl die folgenden Methoden hinzu

  • operator fun <T> get(extension: ExtensionLite<Foo, T>): T: Ruft den aktuellen Wert des Erweiterungsfeldes in der DSL ab
  • operator fun <T> get(extension: ExtensionLite<Foo, List<T>>): ExtensionList<T, Foo>: Ruft den aktuellen Wert des wiederholten Erweiterungsfeldes in der DSL als schreibgeschützte List ab
  • operator fun <T : Comparable<T>> set(extension: ExtensionLite<Foo, T>): Setzt den aktuellen Wert des Erweiterungsfeldes in der DSL (für Comparable Feldtypen)
  • operator fun <T : MessageLite> set(extension: ExtensionLite<Foo, T>): Setzt den aktuellen Wert des Erweiterungsfeldes in der DSL (für Nachrichtenfeldtypen)
  • operator fun set(extension: ExtensionLite<Foo, ByteString>): Setzt den aktuellen Wert des Erweiterungsfeldes in der DSL (für bytes Felder)
  • operator fun contains(extension: ExtensionLite<Foo, *>): Boolean: Gibt true zurück, wenn das Erweiterungsfeld einen Wert hat
  • fun clear(extension: ExtensionLite<Foo, *>): Löscht das Erweiterungsfeld
  • fun <E> ExtensionList<Foo, E>.add(value: E): Fügt einen Wert zum wiederholten Erweiterungsfeld hinzu
  • operator fun <E> ExtensionList<Foo, E>.plusAssign(value: E): Alias für add unter Verwendung der Operator-Syntax
  • operator fun <E> ExtensionList<Foo, E>.addAll(values: Iterable<E>): Fügt mehrere Werte zum wiederholten Erweiterungsfeld hinzu
  • operator fun <E> ExtensionList<Foo, E>.plusAssign(values: Iterable<E>): Alias für addAll unter Verwendung der Operator-Syntax
  • operator fun <E> ExtensionList<Foo, E>.set(index: Int, value: E): Setzt das Element des wiederholten Erweiterungsfeldes am angegebenen Index
  • inline fun ExtensionList<Foo, *>.clear(): Löscht die Elemente des wiederholten Erweiterungsfeldes

Die Generika sind hier komplex, aber das Ergebnis ist, dass this[extension] = value für jeden Erweiterungstyp außer wiederholten Erweiterungen funktioniert, und wiederholte Erweiterungen haben eine "natürliche" Listen-Syntax, die ähnlich wie bei nicht-erweiterten wiederholten Feldern funktioniert.

Gegeben eine Erweiterungsdefinition

extend Foo {
  int32 bar = 123;
}

Java generiert die "Erweiterungs-ID" bar, die verwendet wird, um die obigen Erweiterungsoperationen zu "schlüsseln".