Dartが生成するコード

Protocol Bufferコンパイラが与えられたプロトコル定義に対して生成するDartコードについて記述します。

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

コンパイラの呼び出し

Protocol Bufferコンパイラが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<Foo> createRepeated(): Foo要素の変更可能な繰り返しフィールドを実装するListを作成するためのファクトリ関数。
  • static Foo getDefault(): Fooのシングルトンインスタンスを返します。これは新しく構築されたFooインスタンスと同じです(したがって、すべての単一フィールドは未設定で、すべての繰り返しフィールドは空です)。

ネストされた型

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

message Foo {
  message Bar {
  }
}

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

フィールド

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

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

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

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

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

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

以下のフィールド定義の場合、

int32 foo = 1;

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

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

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

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

  • 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 barvalueを返します。
  • bool hasBar(): フィールドが設定されている場合はtrueを返します。
  • void clearBar(): フィールドの値をクリアします。これを呼び出すと、hasBar()falseを返し、get barはデフォルト値を返します。
  • Bar ensureBar(): hasBar()falseを返す場合、barを空のインスタンスに設定し、barの値を返します。これを呼び出すと、hasBar()trueを返します。

繰り返しフィールド

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

repeated int32 foo = 1;

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

  • List<int> 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<int, int> 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 namevalueを返し、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<Color>`が含まれます。

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;
}

Protocol Bufferコンパイラは、`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クイックスタートガイドを参照してください。