Dart 產生程式碼

說明協定緩衝區編譯器針對任何給定協定定義所產生的 Dart 程式碼。

proto2 和 proto3 產生程式碼之間的任何差異都會被突顯 - 請注意,這些差異在本文所述的產生程式碼中,而不是在基礎 API 中,兩者在兩個版本中都是相同的。在閱讀本文之前,您應該先閱讀proto2 語言指南和/或proto3 語言指南

編譯器叫用

協定緩衝區編譯器需要外掛程式來產生 Dart 程式碼。按照指示安裝外掛程式,會提供一個 protoc-gen-dart 二進位檔,當使用 --dart_out 命令列旗標叫用 protoc 時,就會使用該二進位檔。--dart_out 旗標會告知編譯器將 Dart 原始程式碼檔案寫入到何處。對於 .proto 檔案輸入,編譯器會產生一個 .pb.dart 檔案等。

.pb.dart 檔案的名稱是透過取得 .proto 檔案的名稱並進行兩項變更來計算的

  • 副檔名 (.proto) 會取代為 .pb.dart。例如,名為 foo.proto 的檔案會產生一個名為 foo.pb.dart 的輸出檔案。
  • proto 路徑 (使用 --proto_path-I 命令列旗標指定) 會取代為輸出路徑 (使用 --dart_out 旗標指定)。

例如,當您如下叫用編譯器時

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

編譯器會讀取檔案 src/foo.protosrc/bar/baz.proto。它會產生:build/gen/foo.pb.dartbuild/gen/bar/baz.pb.dart。編譯器會自動建立目錄 build/gen/bar (如果需要),但它不會建立 buildbuild/gen;它們必須已經存在。

訊息

假設有一個簡單的訊息宣告

message Foo {}

協定緩衝區編譯器會產生一個名為 Foo 的類別,該類別會擴充類別 GeneratedMessage

類別 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 元素的 mutable 重複欄位。
  • static Foo getDefault():傳回 Foo 的單例執行個體,該執行個體與新建立的 Foo 執行個體相同 (因此所有單數欄位都未設定,且所有重複欄位都為空)。

巢狀類型

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

message Foo {
  message Bar {
  }
}

在此情況下,編譯器會產生兩個類別:FooFoo_Bar

欄位

除了前一節中描述的方法之外,協定緩衝區編譯器還會為 .proto 檔案中訊息內定義的每個欄位產生存取子方法。

請注意,產生的名稱一律使用駝峰式命名,即使 .proto 檔案中的欄位名稱使用底線的小寫字母 ( 應該如此)。大小寫轉換的方式如下

  1. 對於名稱中的每個底線,底線會移除,後面的字母會大寫。
  2. 如果名稱會附加前置字元 (例如 "has"),則第一個字母會大寫。否則,它會是小寫。

因此,對於欄位 foo_bar_baz,getter 會變成 get fooBarBaz,而以 has 為前置字元的方法會是 hasFooBarBaz

單數基本類型欄位 (proto2)

對於任何這些欄位定義

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

編譯器會在訊息類別中產生下列存取子方法

  • int get foo:傳回欄位的目前值。如果未設定欄位,則傳回預設值。
  • bool hasFoo():如果已設定欄位,則傳回 true
  • set foo(int value):設定欄位的值。在呼叫此方法之後,hasFoo() 會傳回 true,而 get foo 會傳回 value
  • void clearFoo():清除欄位的值。在呼叫此方法之後,hasFoo() 會傳回 false,而 get foo 會傳回預設值。

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

單數基本類型欄位 (proto3)

對於此欄位定義

int32 foo = 1;

編譯器會在訊息類別中產生下列存取子方法

  • int get foo:傳回欄位的目前值。如果未設定欄位,則傳回預設值。
  • set foo(int value):設定欄位的值。在呼叫此方法之後,get foo 會傳回 value
  • void clearFoo():清除欄位的值。在呼叫此方法之後,get foo 會傳回預設值。

注意:由於 Dart proto3 實作中的一個怪癖,即使 optional 修飾詞 (用於請求存在語意) 不在 proto 定義中,也會產生以下方法。

  • bool hasFoo():如果已設定欄位,則傳回 true
  • void clearFoo():清除欄位的值。在呼叫此方法之後,hasFoo() 會傳回 false,而 get foo 會傳回預設值。

單數訊息欄位

假設訊息類型為

message Bar {}

對於具有 Bar 欄位的訊息

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

// proto3
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<int> get foo:傳回支援欄位的清單。如果未設定欄位,則傳回空的清單。對清單的修改會反映在欄位中。

Int64 欄位

對於此欄位定義

// proto2
optional int64 bar = 1;

// proto3
int64 bar = 1;

編譯器會產生

  • Int64 get bar:傳回包含欄位值的 Int64 物件。

請注意,Int64 並未內建於 Dart 核心程式庫中。若要使用這些物件,您可能需要匯入 Dart fixnum 程式庫

import 'package:fixnum/fixnum.dart';

Map 欄位

假設map欄位定義如下

map<int32, int32> map_field = 1;

編譯器會產生下列 getter

  • Map<int, int> get mapField:傳回支援欄位的 Dart map。如果未設定欄位,則傳回空的 map。對 map 的修改會反映在欄位中。

Any

假設Any 欄位如下

import "google/protobuf/any.proto";

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

在我們產生的程式碼中,details 欄位的 getter 會傳回 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 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 Color {
  COLOR_UNSPECIFIED = 0;
  COLOR_RED = 1;
  COLOR_GREEN = 2;
  COLOR_BLUE = 3;
}

協定緩衝區編譯器將會產生一個名為 Color 的類別,該類別繼承自 ProtobufEnum 類別。該類別將會為四個值中的每一個值包含一個 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 檔案中所指定。
  • value:列舉的整數值,如 .proto 檔案中所指定。

請注意,.proto 語言允許有多個列舉符號具有相同的數值。具有相同數值的符號是同義詞。例如:

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

在這種情況下,BAZBAR 的同義詞,並將被定義如下:

static const Foo BAZ = BAR;

列舉可以定義在訊息類型中。例如,假設有如下的列舉定義:

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

協定緩衝區編譯器將會產生一個名為 Bar 的類別,該類別繼承自 GeneratedMessage,以及一個名為 Bar_Color 的類別,該類別繼承自 ProtobufEnum

擴充 (僅限 proto2)

假設有一個檔案 foo_test.proto,其中包含帶有 擴充範圍 的訊息和頂層擴充定義:

message Foo {
  extensions 100 to 199;
}

extend Foo {
  optional int32 bar = 101;
}

協定緩衝區編譯器除了產生 Foo 類別之外,還會產生一個 Foo_test 類別,其中將會為檔案中的每個擴充欄位包含一個 static Extension,以及一個用於在 ExtensionRegistry 中註冊所有擴充的方法。

  • 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 {
    optional 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 快速入門指南 以了解更多詳細資訊。