Dart生成コード

プロトコルバッファコンパイラが、与えられたプロトコル定義に対して生成するDartコードについて説明します。

proto2、proto3、およびエディション生成コード間の違いが強調されています。これらの違いは、このドキュメントで説明されている生成コードにあり、両バージョンで同じ基本APIにはないことに注意してください。このドキュメントを読む前に、proto2言語ガイドproto3言語ガイド、またはエディション言語ガイドを読む必要があります。

コンパイラの呼び出し

プロトコルバッファコンパイラは、Dartコードを生成するためにプラグインを必要とします。 指示に従ってインストールすると、`protoc-gen-dart`バイナリが提供され、`protoc`が`--dart_out`コマンドラインフラグを付けて呼び出されたときに使用されます。`--dart_out`フラグは、コンパイラにDartソースファイルをどこに書き込むかを指示します。`.proto`ファイルを入力として、コンパイラはとりわけ`.pb.dart`ファイルを生成します。

`.pb.dart`ファイルの名前は、`.proto`ファイルの名前を取り、2つの変更を加えることで計算されます。

  • 拡張子 (`.proto`) は `.pb.dart` に置き換えられます。たとえば、`foo.proto` という名前のファイルは、`foo.pb.dart` という名前の出力ファイルになります。
  • プロトパス ( `--proto_path` または `-I` コマンドラインフラグで指定) は、出力パス ( `--dart_out` フラグで指定) に置き換えられます。

例えば、以下のようにコンパイラを呼び出す場合

protoc --proto_path=src --dart_out=build/gen src/foo.proto src/bar/baz.proto

コンパイラは`src/foo.proto`と`src/bar/baz.proto`を読み込み、`build/gen/foo.pb.dart`と`build/gen/bar/baz.pb.dart`を生成します。コンパイラは必要に応じて`build/gen/bar`ディレクトリを自動的に作成しますが、`build`または`build/gen`は作成**しません**。これらはすでに存在している必要があります。

メッセージ

単純なメッセージ宣言を考えます。

message Foo {}

プロトコルバッファコンパイラは、`GeneratedMessage`クラスを継承する`Foo`というクラスを生成します。

`GeneratedMessage` クラスは、メッセージ全体をチェック、操作、読み取り、または書き込みするメソッドを定義します。これらのメソッドに加えて、`Foo` クラスは以下のメソッドとコンストラクタを定義します。

  • `Foo()`: デフォルトコンストラクタ。すべての単一フィールドが未設定で、繰り返しフィールドが空のインスタンスを作成します。
  • `Foo.fromBuffer(...)`: メッセージを表すシリアル化されたプロトコルバッファデータから`Foo`を作成します。
  • `Foo.fromJson(...)`: メッセージをエンコードするJSON文字列から`Foo`を作成します。
  • `Foo clone()`: メッセージ内のフィールドのディープクローンを作成します。
  • `Foo copyWith(void Function(Foo) updates)`: このメッセージの書き込み可能なコピーを作成し、`updates`を適用し、それを返却する前にコピーを読み取り専用にします。
  • `static Foo create()`: 単一の`Foo`を作成するためのファクトリ関数。
  • `static PbList createRepeated()`: `Foo`要素の可変繰り返しフィールドを実装するListを作成するためのファクトリ関数。
  • `static Foo getDefault()`: 新しく構築されたFooのインスタンスと同一の`Foo`のシングルトンインスタンスを返します (したがって、すべての単一フィールドは設定されておらず、すべての繰り返しフィールドは空です)。

ネストされた型

メッセージは別のメッセージ内で宣言できます。例:

message Foo {
  message Bar {
  }
}

この場合、コンパイラは`Foo`と`Foo_Bar`の2つのクラスを生成します。

フィールド

前のセクションで説明したメソッドに加えて、プロトコルバッファコンパイラは、.protoファイル内のメッセージで定義された各フィールドのアクセッサメソッドを生成します。

生成される名前は常にキャメルケース命名を使用することに注意してください。たとえ`.proto`ファイル内のフィールド名がアンダースコア付きの小文字を使用している場合でも (そうあるべきです)。ケース変換は以下のようになります。

  1. 名前の各アンダースコアについて、アンダースコアは削除され、次の文字が大文字になります。
  2. 名前にプレフィックス(例:「has」)が付く場合、最初の文字は大文字になります。それ以外の場合は小文字になります。

したがって、フィールド`foo_bar_baz`の場合、getterは`get fooBarBaz`となり、`has`をプレフィックスとするメソッドは`hasFooBarBaz`となります。

単一のプリミティブフィールド

すべてのフィールドは、Dartの実装において明示的な存在を持ちます。

次のフィールド定義の場合

int32 foo = 1;

コンパイラはメッセージクラスに以下のアクセッサメソッドを生成します。

  • `int get foo`: フィールドの現在の値を返します。フィールドが設定されていない場合、デフォルト値を返します。

  • `bool hasFoo()`: フィールドが設定されている場合は`true`を返します。

  • `set foo(int value)`: フィールドの値を設定します。これを呼び出すと、`hasFoo()`は`true`を返し、`get foo`は`value`を返します。

  • `void clearFoo()`: フィールドの値をクリアします。これを呼び出すと、`hasFoo()`は`false`を返し、`get foo`はデフォルト値を返します。

  • `bool hasFoo()`: フィールドが設定されている場合は`true`を返します。

  • `void clearFoo()`: フィールドの値をクリアします。これを呼び出すと、`hasFoo()`は`false`を返し、`get foo`はデフォルト値を返します。

その他の単純なフィールドタイプの場合、対応するDartタイプはスカラー値タイプテーブルに従って選択されます。メッセージおよびenumタイプの場合、値タイプはメッセージまたはenumクラスに置き換えられます。

単数メッセージフィールド

メッセージ型を考えると:

message Bar {}

`Bar`フィールドを持つメッセージの場合

// proto2
message Baz {
  optional Bar bar = 1;
  // The generated code is the same result if required instead of optional.
}

// proto3 and editions
message Baz {
  Bar bar = 1;
}

コンパイラはメッセージクラスに以下のアクセッサメソッドを生成します。

  • `Bar get bar`: フィールドの現在の値を返します。フィールドが設定されていない場合、デフォルト値を返します。
  • `set bar(Bar value)`: フィールドの値を設定します。これを呼び出すと、`hasBar()`は`true`を返し、`get bar`は`value`を返します。
  • `bool hasBar()`: フィールドが設定されている場合は`true`を返します。
  • `void clearBar()`: フィールドの値をクリアします。これを呼び出すと、`hasBar()`は`false`を返し、`get bar`はデフォルト値を返します。
  • `Bar ensureBar()`: `hasBar()`が`false`を返す場合、`bar`を空のインスタンスに設定し、その後`bar`の値を返します。これを呼び出すと、`hasBar()`は`true`を返します。

繰り返しフィールド

このフィールド定義について:

repeated int32 foo = 1;

コンパイラは以下を生成します。

  • `List get foo`: フィールドをバックアップするリストを返します。フィールドが設定されていない場合、空のリストを返します。リストへの変更はフィールドに反映されます。

Int64フィールド

このフィールド定義について:

int64 bar = 1;

コンパイラは以下を生成します。

  • `Int64 get bar`: フィールド値を含む`Int64`オブジェクトを返します。

Int64はDartのコアライブラリには組み込まれていないことに注意してください。これらのオブジェクトを操作するには、Dartのfixnumライブラリをインポートする必要がある場合があります。

import 'package:fixnum/fixnum.dart';

マップフィールド

このような`map`フィールド定義が与えられた場合

map<int32, int32> map_field = 1;

コンパイラは以下のゲッターを生成します。

  • `Map get mapField`: フィールドをバックアップするDartマップを返します。フィールドが設定されていない場合、空のマップを返します。マップへの変更はフィールドに反映されます。

Any

このような`Any`フィールドが与えられた場合

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  google.protobuf.Any details = 2;
}

生成されたコードでは、`details`フィールドのゲッターは`com.google.protobuf.Any`のインスタンスを返します。これにより、`Any`の値をパックおよびアンパックするための以下の特別なメソッドが提供されます。

    /// Unpacks the message in [value] into [instance].
    ///
    /// Throws a [InvalidProtocolBufferException] if [typeUrl] does not correspond
    /// to the type of [instance].
    ///
    /// A typical usage would be `any.unpackInto(new Message())`.
    ///
    /// Returns [instance].
    T unpackInto<T extends GeneratedMessage>(T instance,
        {ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY});

    /// Returns `true` if the encoded message matches the type of [instance].
    ///
    /// Can be used with a default instance:
    /// `any.canUnpackInto(Message.getDefault())`
    bool canUnpackInto(GeneratedMessage instance);

    /// Creates a new [Any] encoding [message].
    ///
    /// The [typeUrl] will be [typeUrlPrefix]/`fullName` where `fullName` is
    /// the fully qualified name of the type of [message].
    static Any pack(GeneratedMessage message,
        {String typeUrlPrefix = 'type.googleapis.com'});

Oneof

このような`oneof`定義が与えられた場合

message Foo {
  oneof test {
    string name = 1;
    SubMessage sub_message = 2;
  }
}

コンパイラは以下のDart enum型を生成します。

 enum Foo_Test { name, subMessage, notSet }

さらに、これらのメソッドも生成されます。

  • `Foo_Test whichTest()`: どのフィールドが設定されているかを示す列挙型を返します。いずれも設定されていない場合は`Foo_Test.notSet`を返します。
  • `void clearTest()`: 現在設定されているoneofフィールドの値 (存在する場合) をクリアし、oneofケースを`Foo_Test.notSet`に設定します。

oneof 定義内の各フィールドについて、通常のフィールドアクセサメソッドが生成されます。例えば `name` の場合:

  • `String get name`: oneofケースが`Foo_Test.name`の場合、フィールドの現在の値を返します。それ以外の場合は、デフォルト値を返します。
  • `set name(String value)`: フィールドの値を設定し、oneofケースを`Foo_Test.name`に設定します。これを呼び出した後、`get name`は`value`を返し、`whichTest()`は`Foo_Test.name`を返します。
  • `void clearName()`: oneofケースが`Foo_Test.name`でない場合、何も変更されません。それ以外の場合、フィールドの値をクリアします。これを呼び出した後、`get name`はデフォルト値を返し、`whichTest()`は`Foo_Test.notSet`を返します。

列挙型

以下のようなenum定義が与えられた場合

enum Color {
  COLOR_UNSPECIFIED = 0;
  COLOR_RED = 1;
  COLOR_GREEN = 2;
  COLOR_BLUE = 3;
}

プロトコルバッファコンパイラは、`ProtobufEnum`クラスを継承する`Color`というクラスを生成します。このクラスには、4つの値のそれぞれについて`static const Color`と、値を含む`static const List`が含まれます。

static const List<Color> values = <Color> [
  COLOR_UNSPECIFIED,
  COLOR_RED,
  COLOR_GREEN,
  COLOR_BLUE,
];

以下のメソッドも含まれます。

  • `static Color? valueOf(int value)`: 指定された数値に対応する`Color`を返します。

各値は次のプロパティを持ちます。

  • `name`: .protoファイルで指定されたenumの名前。
  • `value`: .protoファイルで指定されたenumの整数値。

`.proto`言語では、複数のenumシンボルが同じ数値を持つことを許可していることに注意してください。同じ数値を持つシンボルは同義語です。例えば

enum Foo {
  BAR = 0;
  BAZ = 0;
}

この場合、`BAZ`は`BAR`の同義語であり、次のように定義されます。

static const Foo BAZ = BAR;

enumはメッセージ型の中にネストして定義することができます。例えば、次のようなenum定義が与えられた場合

message Bar {
  enum Color {
    COLOR_UNSPECIFIED = 0;
    COLOR_RED = 1;
    COLOR_GREEN = 2;
    COLOR_BLUE = 3;
  }
}

プロトコルバッファコンパイラは、`GeneratedMessage`を継承する`Bar`というクラスと、`ProtobufEnum`を継承する`Bar_Color`というクラスを生成します。

拡張機能 (proto3では利用不可)

拡張範囲を持つメッセージとトップレベルの拡張定義を含むファイル`foo_test.proto`が与えられた場合

message Foo {
  extensions 100 to 199;
}

extend Foo {
  optional int32 bar = 101;
}

プロトコルバッファコンパイラは、`Foo`クラスに加えて、ファイル内の各拡張フィールドの`static Extension`と、`ExtensionRegistry`にすべての拡張を登録するためのメソッドを含む`Foo_test`クラスを生成します。

  • static final Extension bar
  • `static void registerAllExtensions(ExtensionRegistry registry)` : 定義されているすべての拡張機能を指定されたレジストリに登録します。

`Foo`の拡張アクセサは次のように使用できます。

Foo foo = Foo();
foo.setExtension(Foo_test.bar, 1);
assert(foo.hasExtension(Foo_test.bar));
assert(foo.getExtension(Foo_test.bar)) == 1);

拡張機能は、別のメッセージの内部にネストして宣言することもできます。

message Baz {
  extend Foo {
    int32 bar = 124;
  }
}

この場合、拡張`bar`は代わりに`Baz`クラスの静的メンバーとして宣言されます。

拡張機能を持つ可能性のあるメッセージを解析する際には、解析できるようにしたいすべての拡張機能を登録した`ExtensionRegistry`を提供する必要があります。そうしないと、それらの拡張機能は未知のフィールドとして扱われます。例えば、

ExtensionRegistry registry = ExtensionRegistry();
registry.add(Baz.bar);
Foo foo = Foo.fromBuffer(input, registry);

すでに未知のフィールドを持つ解析済みメッセージがある場合、`ExtensionRegistry`で`reparseMessage`を使用してメッセージを再解析できます。未知のフィールドのセットにレジストリに存在する拡張機能が含まれている場合、これらの拡張機能は解析され、未知のフィールドセットから削除されます。メッセージにすでに存在する拡張機能は保持されます。

Foo foo = Foo.fromBuffer(input);
ExtensionRegistry registry = ExtensionRegistry();
registry.add(Baz.bar);
Foo reparsed = registry.reparseMessage(foo);

この拡張機能を取得する方法は、全体的にコストが高いことに注意してください。可能な場合は、`GeneratedMessage.fromBuffer`を実行する際に、必要なすべての拡張機能を含む`ExtensionRegistry`を使用することをお勧めします。

サービス

サービス定義が与えられた場合

service Foo {
  rpc Bar(FooRequest) returns(FooResponse);
}

プロトコルバッファコンパイラは、`grpc`オプション(例:`--dart_out=grpc:output_folder`)を指定して呼び出すことができ、その場合、gRPCをサポートするためのコードを生成します。詳細については、gRPC Dart Quickstart guideを参照してください。