Kotlin生成コードガイド

Protocol Buffersコンパイラが、特定のプロトコル定義に対してJava用に生成されるコードに加えて、どのようなKotlinコードを生成するかを正確に説明します。

proto2とproto3の生成コードの違いは強調されています。これらの違いは、このドキュメントで説明されている生成コードにあり、両バージョンで同じ基本メッセージクラス/インターフェースにはありません。このドキュメントを読む前に、proto2言語ガイドおよび/またはproto3言語ガイドを読むことをお勧めします。

コンパイラの呼び出し

Protocol Buffersコンパイラは、Javaコードの上に構築されるKotlinコードを生成します。そのため、--java_out=--kotlin_out=の2つのコマンドラインフラグを指定して呼び出す必要があります。--java_out=オプションのパラメータは、コンパイラがJava出力を書き込むディレクトリであり、--kotlin_out=も同様です。各.protoファイル入力に対して、コンパイラは.protoファイル自体を表すJavaクラスを含むラッパー.javaファイルを作成します。

.protoファイルに以下の行が含まれているかどうかに関わらず

option java_multiple_files = true;

コンパイラは、.protoファイルで宣言された各トップレベルメッセージに対して生成する各クラスとファクトリメソッド用に、個別の.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という名前のメッセージを含んでいる場合、Protocol Buffersコンパイラはファイルbuild/gen/kotlin/com/example/BarKt.ktを生成します。Protocol Buffersコンパイラは、必要に応じてbuild/gen/kotlin/comおよびbuild/gen/kotlin/com/exampleディレクトリを自動的に作成します。ただし、build/gen/kotlinbuild/gen、またはbuildは作成されません。これらはすでに存在している必要があります。単一の呼び出しで複数の.protoファイルを指定でき、すべての出力ファイルが一度に生成されます。

メッセージ

シンプルなメッセージ宣言があるとします。

message FooBar {}

Protocol Buffersコンパイラは、生成されたJavaコードに加えて、FooBarKtというオブジェクトと、以下の構造を持つ2つのトップレベル関数を生成します。

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 Buffersコンパイラは、.protoファイル内のメッセージに定義された各フィールドに対して、DSLにミュータブルなプロパティを生成します。(Kotlinは、Javaによって生成されたゲッターからメッセージオブジェクトの読み取り専用プロパティをすでに推論します。)

プロパティは、.protoファイル内のフィールド名がアンダースコアを含む小文字(推奨されるように)を使用している場合でも、常にキャメルケース命名を使用することに注意してください。ケース変換は次のように機能します。

  1. 名前内の各アンダースコアについて、アンダースコアは削除され、続く文字が大文字になります。
  2. 名前に接頭辞(例えば「clear」)が付く場合、最初の文字は大文字になります。それ以外の場合は、小文字になります。

したがって、フィールドfoo_bar_bazfooBarBazになります。

フィールド名がKotlinの予約語またはprotobufライブラリにすでに定義されているメソッドと競合するいくつかの特殊なケースでは、余分なアンダースコアが追加されます。例えば、inという名前のフィールドのクリアメソッドはclearIn_()です。

単一フィールド (proto2)

これらのフィールド定義のいずれかに対して、

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

コンパイラはDSLに以下のアクセサーを生成します。

  • fun hasFoo(): Boolean: フィールドが設定されている場合にtrueを返します。
  • var foo: Int: フィールドの現在の値。フィールドが設定されていない場合、デフォルト値を返します。
  • fun clearFoo(): フィールドの値をクリアします。これを呼び出した後、hasFoo()falseを返し、getFoo()はデフォルト値を返します。

その他の単純なフィールド型については、スカラ値型テーブルに従って対応するJava型が選択されます。メッセージ型とenum型の場合、値型はメッセージまたはenumクラスに置き換えられます。メッセージ型はJavaで定義されたままであるため、メッセージ内の符号なし型は、JavaおよびKotlinの古いバージョンとの互換性のために、DSLでは標準の対応する符号付き型を使用して表現されます。

組み込みメッセージフィールド

サブメッセージの特別な処理はないことに注意してください。例えば、フィールドがある場合、

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型が選択されます。メッセージ型とenum型の場合、値型はメッセージまたはenumクラスに置き換えられます。メッセージ型はJavaで定義されたままであるため、メッセージ内の符号なし型は、JavaおよびKotlinの古いバージョンとの互換性のために、DSLでは標準の対応する符号付き型を使用して表現されます。

組み込みメッセージフィールド

メッセージフィールド型の場合、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)(指定された0ベースのインデックスにある要素の値を設定する拡張関数)
  • fun DslList<String, FooProxy>.clear()(繰り返しフィールドの内容をクリアする拡張関数)

この特異な構造により、fooListはDSLのスコープ内でミュータブルなリストのように「振る舞う」ことができ、基盤となるビルダーがサポートするメソッドのみをサポートしつつ、ミュータビリティがDSLから「漏れ出す」のを防ぎ、混乱を招く副作用を引き起こす可能性を防ぎます。

その他の単純なフィールド型については、スカラ値型テーブルに従って対応するJava型が選択されます。メッセージ型とenum型の場合、その型はメッセージまたはenumクラスです。

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型が選択されます。メッセージ型とenum型の場合、値型はメッセージまたはenumクラスに置き換えられます。

マップフィールド

このマップフィールド定義の場合、

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のみ)

拡張範囲を持つメッセージがあるとします。

message Foo {
  extensions 100 to 199;
}

Protocol Buffersコンパイラは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 {
  optional int32 bar = 123;
}

Javaは「拡張識別子」barを生成し、これは上記の拡張操作を「キー指定」するために使用されます。