Dart 生成的代码
本文重点说明了 proto2、proto3 和 editions 生成的代码之间的任何差异——请注意,这些差异存在于本文档所描述的生成代码中,而不是基础 API 中,基础 API 在两个版本中是相同的。在阅读本文档之前,您应该先阅读 proto2 语言指南、proto3 语言指南或 editions 语言指南。
编译器调用
protocol buffer 编译器需要一个插件来生成 Dart 代码。按照说明进行安装后,会提供一个 protoc-gen-dart 二进制文件,当 protoc 使用 --dart_out 命令行标志调用时会使用它。--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.proto 和 src/bar/baz.proto。它会生成:build/gen/foo.pb.dart 和 build/gen/bar/baz.pb.dart。编译器会自动创建目录 build/gen/bar(如果需要),但它*不会*创建 build 或 build/gen;它们必须已经存在。
消息
给定一个简单的消息声明:
message Foo {}
protocol buffer 编译器会生成一个名为 Foo 的类,该类扩展了 GeneratedMessage 类。
GeneratedMessage 类定义了允许您检查、操作、读取或写入整个消息的方法。除了这些方法之外,Foo 类还定义了以下方法和构造函数:
Foo(): 默认构造函数。创建一个实例,其中所有单一字段都未设置,重复字段为空。Foo.fromBuffer(...): 从表示该消息的序列化 protocol buffer 数据中创建一个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。
字段
除了上一节中描述的方法外,protocol buffer 编译器还为 .proto 文件中消息内定义的每个字段生成了访问器方法。
请注意,生成的名称始终使用驼峰命名法,即使 .proto 文件中的字段名使用带下划线的小写字母(按规范应该如此)。大小写转换规则如下:
- 对于名称中的每个下划线,下划线被移除,其后的字母大写。
- 如果名称将附加前缀(例如 "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将返回默认值。注意
由于 Dart proto3 实现的一个特殊性,即使配置了隐式存在性,以下方法仍会被生成。bool hasFoo(): 如果字段已设置,则返回true。注意
如果 proto 是在支持隐式存在性的另一种语言(例如 Java)中序列化的,那么这个值可能并不真正可信。尽管 Dart 会跟踪存在性,但其他语言不会,因此往返一个值为零的隐式存在性字段将使其从 Dart 的角度来看“消失”。void clearFoo(): 清除字段的值。调用此方法后,hasFoo()将返回false,get foo将返回默认值。
对于其他简单字段类型,相应的 Dart 类型是根据标量值类型表选择的。对于消息和枚举类型,值类型将被替换为相应的消息或枚举类。
奇异消息字段
给定消息类型:
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<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;
编译器将生成以下 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;
}
protocol buffer 编译器将生成一个名为 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;
}
在这种情况下,BAZ 是 BAR 的同义词,并将按如下方式定义:
static const Foo BAZ = BAR;
枚举可以嵌套在消息类型中定义。例如,给定一个如下的枚举定义:
message Bar {
enum Color {
COLOR_UNSPECIFIED = 0;
COLOR_RED = 1;
COLOR_GREEN = 2;
COLOR_BLUE = 3;
}
}
protocol buffer 编译器将生成一个名为 Bar 的类,它扩展了 GeneratedMessage,以及一个名为 Bar_Color 的类,它扩展了 ProtobufEnum。
扩展(proto3 中不可用)
给定一个文件 foo_test.proto,其中包含一个带有扩展范围的消息和一个顶层扩展定义:
message Foo {
extensions 100 to 199;
}
extend Foo {
optional int32 bar = 101;
}
protocol buffer 编译器除了生成 Foo 类之外,还会生成一个名为 Foo_test 的类,该类将为文件中的每个扩展字段包含一个 static Extension,以及一个用于在 ExtensionRegistry 中注册所有扩展的方法:
static final Extension barstatic 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。
服务(Services)
给定一个服务定义:
service Foo {
rpc Bar(FooRequest) returns(FooResponse);
}
protocol buffer 编译器可以通过 `grpc` 选项(例如 --dart_out=grpc:output_folder)来调用,这种情况下它将生成支持 gRPC 的代码。更多详情请参阅 gRPC Dart 快速入门指南。