Kotlin 程式碼產生指南

除了為 Java 產生的程式碼之外,還會詳細描述 protocol buffer 編譯器為任何給定的協定定義產生確切的 Kotlin 程式碼。

會強調 proto2 和 proto3 產生程式碼之間的任何差異 — 請注意,這些差異在此文件中描述的產生程式碼中,而不是在基本訊息類別/介面中,兩者在兩個版本中相同。在閱讀此文件之前,您應該閱讀 proto2 語言指南和/或 proto3 語言指南

編譯器呼叫

protocol buffer 編譯器會產生建構在 Java 程式碼之上的 Kotlin 程式碼。因此,它必須使用兩個命令列標記 --java_out=--kotlin_out= 來呼叫。--java_out= 選項的參數是您想要編譯器寫入 Java 輸出的目錄,--kotlin_out= 也是一樣。對於每個 .proto 檔案輸入,編譯器會建立一個包裝的 .java 檔案,其中包含一個 Java 類別,該類別代表 .proto 檔案本身。

無論您的 .proto 檔案是否包含如下程式碼

option java_multiple_files = true;

編譯器都會為每個類別和工廠方法建立單獨的 .kt 檔案,這些類別和方法將為 .proto 檔案中宣告的每個頂層訊息產生。

每個檔案的 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 的訊息,則 protocol buffer 編譯器會產生檔案 build/gen/kotlin/com/example/BarKt.kt。如果需要,protocol buffer 編譯器會自動建立 build/gen/kotlin/combuild/gen/kotlin/com/example 目錄。但是,它不會建立 build/gen/kotlinbuild/genbuild;它們必須已經存在。您可以在單次呼叫中指定多個 .proto 檔案;所有輸出檔案都會一次產生。

訊息

給定一個簡單的訊息宣告

message FooBar {}

除了產生的 Java 程式碼之外,protocol buffer 編譯器還會產生一個名為 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

欄位

除了上一節中描述的方法之外,protocol buffer 編譯器還會在 DSL 中為 .proto 檔案中訊息內定義的每個欄位產生可變屬性。(Kotlin 已經從 Java 產生的 getter 推斷訊息物件上的唯讀屬性。)

請注意,屬性始終使用駝峰式命名,即使 .proto 檔案中的欄位名稱使用帶底線的小寫 (應該這樣做)。大小寫轉換如下

  1. 對於名稱中的每個底線,會移除底線,並將後面的字母大寫。
  2. 如果名稱會附加前綴(例如「clear」),則第一個字母會大寫。否則,它會是小寫。

因此,欄位 foo_bar_baz 會變成 fooBarBaz

在一些特殊情況下,欄位名稱與 Kotlin 中的保留字或 protobuf 程式庫中已定義的方法衝突時,會附加一個額外的底線。例如,名為 in 的欄位的 clearer 是 clearIn_()

單數欄位 (proto2)

對於任何這些欄位定義

optional int32 foo = 1;
required int32 foo = 1;

編譯器會在 DSL 中產生以下存取子

  • fun hasFoo(): Boolean:如果已設定欄位,則傳回 true
  • var foo: Int:欄位的目前值。如果未設定欄位,則傳回預設值。
  • fun clearFoo():清除欄位的值。在呼叫此函式後,hasFoo() 會傳回 false,而 getFoo() 會傳回預設值。

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

內嵌訊息欄位

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

optional Foo my_foo = 1;

您必須寫入

myFoo = foo {
  ...
}

一般而言,這是因為編譯器不知道 Foo 是否有 Kotlin DSL,或者例如只有產生的 Java API。這表示您不必等待您依賴的訊息來新增 Kotlin 程式碼產生。

單數欄位 (proto3)

對於此欄位定義

int32 foo = 1;

編譯器會在 DSL 中產生以下屬性

  • var foo: Int:傳回欄位的目前值。如果未設定欄位,則傳回欄位型別的預設值。
  • fun clearFoo():清除欄位的值。在呼叫此函式後,getFoo() 會傳回欄位型別的預設值。

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

內嵌訊息欄位

對於訊息欄位型別,會在 DSL 中產生額外的存取子方法

  • boolean hasFoo():如果已設定欄位,則傳回 true

請注意,沒有根據 DSL 設定子訊息的捷徑。例如,如果您有一個欄位

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 (僅限 proto2):如果 oneof 案例是 FOO,則傳回 true
  • val foo: Int:如果 oneof 案例是 FOO,則傳回 oneof_name 的目前值。否則,傳回此欄位的預設值。

對於其他簡單的欄位型別,會根據純量值型別表選擇對應的 Java 型別。對於訊息和列舉型別,值型別會取代為訊息或列舉類別。

Map 欄位

對於此 map 欄位定義

map<int32, int32> weight = 1;

編譯器會在 DSL 類別中產生以下成員

  • class WeightProxy private constructor(): DslProxy(),一個僅在泛型中使用的無法建構的型別
  • val weight: DslMap<Int, Int, WeightProxy>,此為 map 欄位中目前項目的唯讀視圖
  • fun DslMap<Int, Int, WeightProxy>.put(key: Int, value: Int):將項目新增至此 map 欄位
  • 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>):將指定 map 中的所有項目新增至此 map 欄位,覆寫已存在鍵的先前值
  • fun DslMap<Int, Int, WeightProxy>.clear():清除此 map 欄位中的所有項目

擴充 (僅限 proto2)

假設訊息具有擴充範圍

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>:以唯讀 List 取得 DSL 中重複擴充欄位的目前值
  • 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 {
  optional int32 bar = 123;
}

Java 會產生「擴充識別碼」bar,此識別碼用於「鍵控」上述擴充操作。