Kotlin 生成程式碼指南

除了為 Java 生成的程式碼外,本指南還確切描述了協議緩衝區編譯器為任何給定的協議定義生成的 Kotlin 程式碼。

proto2、proto3 和 Editions 生成的程式碼之間的任何差異都將突出顯示——請注意,這些差異存在於本文件描述的生成程式碼中,而不是基礎訊息類/介面(它們在兩個版本中是相同的)。在閱讀本文件之前,您應該閱讀 proto2 語言指南proto3 語言指南 和/或 Editions 指南

編譯器呼叫

協議緩衝區編譯器生成的 Kotlin 程式碼建立在 Java 程式碼之上。因此,它必須使用兩個命令列標誌呼叫:--java_out=--kotlin_out=--java_out= 選項的引數是您希望編譯器將 Java 輸出寫入的目錄,--kotlin_out= 也是如此。對於每個 .proto 檔案輸入,編譯器會建立一個包裝器 .java 檔案,其中包含一個表示 .proto 檔案本身的 Java 類。

無論您的 .proto 檔案是否包含如下行:

option java_multiple_files = true;

編譯器將為每個頂層訊息宣告的類和工廠方法分別建立 .kt 檔案。

每個檔案的 Java 包名與生成的 Java 程式碼使用的名稱相同,具體描述請參閱 Java 生成程式碼參考

輸出檔名是透過連線 --kotlin_out= 的引數、包名(將句點 [.] 替換為斜槓 [/])以及字尾 Kt.kt 來確定的。

因此,舉例來說,假設您像下面這樣呼叫編譯器:

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

如果 foo.proto 的 Java 包是 com.example,並且它包含一個名為 Bar 的訊息,那麼協議緩衝區編譯器將生成檔案 build/gen/kotlin/com/example/BarKt.kt。協議緩衝區編譯器將在需要時自動建立 build/gen/kotlin/combuild/gen/kotlin/com/example 目錄。但是,它不會建立 build/gen/kotlinbuild/genbuild;它們必須已存在。您可以在單次呼叫中指定多個 .proto 檔案;所有輸出檔案將一次性生成。

訊息

給定一個簡單的訊息宣告:

message FooBar {}

協議緩衝區編譯器除了生成 Java 程式碼外,還會生成一個名為 FooBarKt 的物件,以及兩個頂級函式,具有以下結構:

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

巢狀型別

訊息可以宣告在另一個訊息內部。例如:

message Foo {
  message Bar { }
}

在這種情況下,編譯器將 BarKt 物件和 bar 工廠方法巢狀在 FooKt 中,但 copy 方法保持為頂級。

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

欄位

除了上一節中描述的方法之外,協議緩衝區編譯器還在 DSL 中為 .proto 檔案中訊息定義的每個欄位生成可變屬性。(Kotlin 已經從 Java 生成的 getter 中推斷出訊息物件上的只讀屬性。)

請注意,屬性總是使用駝峰命名法,即使 .proto 檔案中的欄位名使用下劃線分隔的小寫字母(正如應該的那樣)。大小寫轉換如下:

  1. 對於名稱中的每個下劃線,下劃線被移除,其後的字母大寫。
  2. 如果名稱將附加字首(例如,“clear”),則第一個字母將大寫。否則,它將小寫。

因此,欄位 foo_bar_baz 將變為 fooBarBaz

在少數特殊情況下,當欄位名與 Kotlin 中的保留字或 protobuf 庫中已定義的方法衝突時,會附加一個額外的下劃線。例如,欄位 in 的清除器是 clearIn_()

單一欄位

對於此欄位定義:

int32 foo = 1;

編譯器將在 DSL 中生成以下訪問器:

  • fun hasFoo(): Boolean:如果設定了該欄位,則返回 true。對於使用隱式存在的欄位,不生成此方法。
  • var foo: Int:欄位的當前值。如果未設定該欄位,則返回預設值。
  • fun clearFoo():清除欄位的值。呼叫此方法後,hasFoo() 將返回 falsegetFoo() 將返回預設值。

對於其他簡單欄位型別,根據 標量值型別表 選擇相應的 Java 型別。對於訊息和列舉型別,值型別將替換為訊息或列舉類。由於訊息型別仍然在 Java 中定義,因此訊息中的無符號型別使用 DSL 中的相應有符號型別表示,以與 Java 和舊版 Kotlin 相容。

內嵌訊息欄位

請注意,子訊息沒有特殊處理。例如,如果您有一個欄位:

optional Foo my_foo = 1;

您必須這樣寫:

myFoo = foo {
  ...
}

總的來說,這是因為編譯器不知道 Foo 是否有 Kotlin DSL,或者例如只有 Java API 被生成。這意味著您不必等待您依賴的訊息新增 Kotlin 程式碼生成。

重複欄位

對於此欄位定義:

repeated string foo = 1;

編譯器將在 DSL 中生成以下成員:

  • class FooProxy: DslProxy,一個不可構造的型別,僅用於泛型。
  • val fooList: DslList<String, FooProxy>,對重複欄位中當前元素的列表的只讀檢視。
  • fun DslList<String, FooProxy>.add(value: String),一個允許將元素新增到重複欄位的擴充套件函式。
  • operator fun DslList<String, FooProxy>.plusAssign(value: String)add 的別名。
  • fun DslList<String, FooProxy>.addAll(values: Iterable<String>),一個允許將元素 Iterable 新增到重複欄位的擴充套件函式。
  • operator fun DslList<String, FooProxy>.plusAssign(values: Iterable<String>)addAll 的別名。
  • operator fun DslList<String, FooProxy>.set(index: Int, value: String),一個設定給定零基索引處元素的擴充套件函式。
  • fun DslList<String, FooProxy>.clear(),一個清除重複欄位內容的擴充套件函式。

這種不尋常的構造允許 fooList 在 DSL 的範圍內“表現得像”一個可變列表,僅支援底層構建器支援的方法,同時防止可變性“逃逸”出 DSL,這可能會導致令人困惑的副作用。

對於其他簡單欄位型別,根據 標量值型別表 選擇相應的 Java 型別。對於訊息和列舉型別,型別是訊息或列舉類。

Oneof 欄位

對於這個 oneof 欄位定義:

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

編譯器將在 DSL 中生成以下訪問器方法:

  • val oneofNameCase: OneofNameCase:獲取 oneof_name 欄位的哪個(如果有)已被設定;有關返回型別,請參閱 Java 程式碼參考
  • fun hasFoo(): Boolean:如果 oneof 的情況是 FOO,則返回 true
  • val foo: Int:如果 oneof 的情況是 FOO,則返回 oneof_name 的當前值。否則,返回此欄位的預設值。

對於其他簡單欄位型別,根據 標量值型別表 選擇相應的 Java 型別。對於訊息和列舉型別,值型別將替換為訊息或列舉類。

對映欄位

對於此 map 欄位定義:

map<int32, int32> weight = 1;

編譯器將在 DSL 類中生成以下成員:

  • class WeightProxy private constructor(): DslProxy(),一個不可構造的型別,僅用於泛型。
  • val weight: DslMap<Int, Int, WeightProxy>,對對映欄位中當前條目的只讀檢視。
  • fun DslMap<Int, Int, WeightProxy>.put(key: Int, value: Int):將條目新增到此對映欄位。
  • operator fun DslMap<Int, Int, WeightProxy>.put(key: Int, value: Int):使用運算子語法進行 put 的別名。
  • fun DslMap<Int, Int, WeightProxy>.remove(key: Int):刪除與 key 關聯的條目(如果存在)。
  • fun DslMap<Int, Int, WeightProxy>.putAll(map: Map<Int, Int>):將指定對映中的所有條目新增到此對映欄位,覆蓋已存在鍵的先前值。
  • fun DslMap<Int, Int, WeightProxy>.clear():清除此對映欄位中的所有條目。

擴充套件

給定一個具有擴充套件範圍的 proto2 或 editions 訊息:

message Foo {
  extensions 100 to 199;
}

協議緩衝區編譯器將向 FooKt.Dsl 新增以下方法:

  • operator fun <T> get(extension: ExtensionLite<Foo, T>): T:獲取 DSL 中擴充套件欄位的當前值。
  • operator fun <T> get(extension: ExtensionLite<Foo, List<T>>): ExtensionList<T, Foo>:將 DSL 中重複擴充套件欄位的當前值作為只讀 List 獲取。
  • operator fun <T : Comparable<T>> set(extension: ExtensionLite<Foo, T>):在 DSL 中設定擴充套件欄位的當前值(對於 Comparable 欄位型別)。
  • operator fun <T : MessageLite> set(extension: ExtensionLite<Foo, T>):在 DSL 中設定擴充套件欄位的當前值(對於訊息欄位型別)。
  • operator fun set(extension: ExtensionLite<Foo, ByteString>):在 DSL 中設定擴充套件欄位的當前值(對於 bytes 欄位)。
  • operator fun contains(extension: ExtensionLite<Foo, *>): Boolean:如果擴充套件欄位有值,則返回 true。
  • fun clear(extension: ExtensionLite<Foo, *>):清除擴充套件欄位。
  • fun <E> ExtensionList<Foo, E>.add(value: E):將值新增到重複的擴充套件欄位。
  • operator fun <E> ExtensionList<Foo, E>.plusAssign(value: E):使用運算子語法進行 add 的別名。
  • operator fun <E> ExtensionList<Foo, E>.addAll(values: Iterable<E>):將多個值新增到重複的擴充套件欄位。
  • operator fun <E> ExtensionList<Foo, E>.plusAssign(values: Iterable<E>):使用運算子語法進行 addAll 的別名。
  • operator fun <E> ExtensionList<Foo, E>.set(index: Int, value: E):設定指定索引處重複擴充套件欄位的元素。
  • inline fun ExtensionList<Foo, *>.clear():清除重複擴充套件欄位的元素。

這裡的泛型很複雜,但效果是 this[extension] = value 對所有擴充套件型別(除了重複擴充套件)都有效,而重複擴充套件具有“自然”的列表語法,其工作方式類似於 非擴充套件重複欄位

給定一個擴充套件定義:

extend Foo {
  int32 bar = 123;
}

Java 會生成“擴充套件識別符號” bar,該識別符號用於為上述擴充套件操作“鍵入”。