分类:proto| 发布时间:2025-01-20 21:50:00
介绍如何在项目中使用 Protocol Buffers 2023 修订版
本指南介绍如何使用 protocol buffer 语言来构造 protocol buffer 数据,包括 .proto
文件语法以及如何从 .proto
文件生成数据访问类。
它涵盖了 protocol buffer 的 2023 版。
有关各版本在概念上与 proto2 和 proto3 有何不同,请参阅 Protobuf 版本概述。
有关 proto2 语法的信息,请参阅 Proto2 语言指南。
有关 proto3 语法的信息,请参阅 Proto3 语言指南。
这是一个参考指南 - 有关使用本文档中描述的许多功能的分步示例,请参阅您选择的语言的教程。
首先,让我们来看一个非常简单的例子。 假设您想要定义一种搜索请求消息格式,其中每个搜索请求都包含一个查询字符串、您感兴趣的特定结果页以及每页的结果数量。 以下是您用来定义消息类型的 .proto 文件。
edition = "2023";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
SearchRequest
消息定义指定了三个字段(名称/值对),每个字段对应您想要包含在该类型消息中的一个数据片段。
每个字段都有一个名称和一个类型。在前面的示例中,所有字段都是标量类型:两个整数(page_number
和 results_per_page
)和一个字符串(query
)。
您还可以为您的字段指定枚举类型和复合类型,如其他消息类型。
您必须为消息定义中的每个字段分配一个介于 1 和 536,870,911 之间的编号,并遵守以下限制:
一旦你在消息中为某个字段分配了编号,则这个编号无法更改,因为它在 消息底层传输格式 中标识该字段。 “更改”编号号等同于删除该字段并创建一个具有相同类型但有不同编号的新字段。 有关如何正确执行此操作,请参阅 删除字段。
字段编号绝不应重复使用。 切勿将字段编号从保留列表中取出,然后将其与新的字段定义一起重复使用。 请参阅 字段编号重复使用后果。
对于最常设置的字段,您应该使用 1 到 15 的字段编号。 较低的字段编号值在底层传输格式中占用更少的空间。 例如,1 到 15 范围内的字段编号需要一个字节来编码。 16 到 2047 范围内的字段编号需要两个字节。 您可以在 Protocol Buffer Encoding 中了解更多信息。
重复使用字段编号会使解码消息的底层传输格式是变得有歧义。
protobuf 底层传输格式简洁高效,无法检测到使用一种定义编码而使用另一种定义解码的字段。
使用一种格式编码但是使用另一种格式进行解码字段会导致:
字段编号重复使用的常见原因:
字段编号限制为 29 位而不是 32 位,因为三位用于指定字段的传输格式。 有关更多信息,请参阅 “编码”主题
消息字段可以是以下之一:
field_presence
特性设置为 IMPLICIT
值。
已迁移到 editions 的 Proto2 required
字段也将使用 field_presence
特性,但设置为 LEGACY_REQUIRED
。在 proto editions 中,标量数值类型的 repeated
字段默认使用 packed
编码。
您可以在 Protocol Buffer Encoding 中了解更多关于 packed
编码的信息。
对于 protobuf 消息,“格式良好”一词指的是序列化/反序列化的字节。 protoc 解析器验证给定的 proto 定义文件是否可解析。
单个字段可以在传输格式字节中出现多次。 解析器将接受输入,但只有该字段的最后一个实例可以通过生成的绑定访问。 有关此主题的更多信息,请参阅 Last One Wins
可以在单个 .proto 文件中定义多个消息类型。
如果您要定义多个相关消息,这很有用——例如,如果您想定义与 SearchRequest
消息类型对应的回复消息格式,可以将其添加到同一个 .proto
文件中:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
message SearchResponse {
...
}
合并消息会导致膨胀
虽然可以在单个 .proto
文件中定义多个消息类型(如消息、枚举和服务),但当在单个文件中定义大量具有不同依赖关系的消息时,也会导致依赖关系膨胀。
建议每个 .proto
文件中包含尽可能少的消息类型。
在 .proto 文件中添加注释:
/**
* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response.
*/
message SearchRequest {
string query = 1;
// Which page number do we want?
int32 page_number = 2;
// Number of results to return per page.
int32 results_per_page = 3;
}
如果不正确处理删除字段可能会导致严重问题。
当您不再需要某个字段并且已从客户端代码中删除所有引用时,可以从消息中删除该字段的定义。 但是,必须 保留已删除的字段编号。 如果您不保留字段编号,开发人员将来可能会重用该编号。
您还应该保留字段名称,以允许继续解析消息的 JSON 和 TextFormat 编码。
如果您通过完全删除字段或将其注释掉来 更新 消息类型,则未来的开发人员可以在对类型进行自己的更新时重用该字段编号。
这可能会导致严重的问题,如 字段编号重复使用后果 中所述。
为了确保这种情况不会发生,请将已删除的字段编号添加到 reserved
列表中。
protoc 编译器将在任何未来的开发人员尝试使用这些保留的字段编号时生成错误消息。
message Foo {
reserved 2, 15, 9 to 11;
}
保留字段编号范围是闭区间(9 to 11
与 9, 10, 11 相同)。
稍后重用旧字段名称通常是安全的,但使用 TextProto 或 JSON 编码时除外,因为字段名称会被序列化。
为了避免此风险,您可以将已删除的字段名称添加到 reserved
列表中。
保留名称仅影响 protoc 编译器的行为,而不会影响运行时行为,但有一个例外:TextProto 实现可能会在解析时丢弃具有保留名称的未知字段(而不会像其他未知字段那样引发错误)(目前只有 C++ 和 Go 实现这样做)。 运行时 JSON 解析不受保留名称的影响。
message Foo {
reserved 2, 15, 9 to 11;
reserved foo, bar;
}
注意,不能在同一 reserved
语句中混合字段名和字段号。
.proto
文件中生成了什么?当您在 .proto
文件上运行 protocol buffer 编译器时,编译器会生成您选择的语言中的代码,您将需要使用在文件中描述的消息类型,包括获取和设置字段值、将消息序列化为输出流以及从输入流中解析消息。
您可以通过按照所选语言的教程来了解更多关于使用每个语言的 API 的信息。 有关更多 API 详细信息,请参阅相关的 API 参考。
标量消息字段可以具有以下类型之一,表格显示了在 .proto
文件中指定的类型以及在自动生成的类中的相应类型:
Proto 类型 | 说明 |
---|---|
double | |
float | |
int32 | 使用可变长度编码。此编码对负数效率低下,因此如果您的字段可能具有负值,请使用 sint32 代替。 |
int64 | 使用可变长度编码。此编码对负数效率低下,因此如果您的字段可能具有负值,请使用 sint64 代替。 |
uint32 | 使用可变长度编码。 |
uint64 | 使用可变长度编码。 |
sint32 | 使用可变长度编码。有符号整数。对于负数来说,本类型编码效率比 int32 更高。 |
sint64 | 使用可变长度编码。有符号整数。对于负数来说,本类型编码效率比 int64 更高。 |
fixed32 | 总是四个字节。如果你的值经常大于 228,本类型编码比 uint32 效率更高。 |
fixed64 | 总是八个字节。如果你的值经常大于 256,本类型编码比 uint64 效率更高。 |
sfixed32 | 总是四个字节。 |
sfixed64 | 总是八个字节。 |
bool | |
string | 字符串必须是 UTF-8 编码或者 7-bit ASCII 文本,并且不能长于 232。 |
bytes | 可以包含不长于232的任意字节序列。 |
Proto 类型 | C++ 类型 | Java/Kotlin 类型[1] | Python 类型[3] | Go 类型 | Ruby 类型 | C# 类型 | PHP 类型 | Dart 类型 | Rust 类型 |
---|---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | double | f64 |
float | float | float | float | float32 | Float | float | float | double | f32 |
int32 | int32_t | int | int | int32 | Fixnum or Bignum(as required) | int | integer | int | i32 |
int64 | int64_t | long | int/long[4] | int64 | Bignum | long | interger/string[6] | Int64 | i64 |
uint32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum or Bignum(as required) | int | integer | int | u32 |
uint64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | interger/string[6] | Int64 | u64 |
sint32 | int32_t | int | int | int32 | Fixnum or Bignum(as required) | int | integer | int | i32 |
sint64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
fixed32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum or Bignum(as required) | uint | integer | int | u32 |
fixed64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
sfixed32 | int32_t | int | int | int32 | Fixnum or Bignum(as required) | int | integer | int | i32 |
sfixed64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | bool |
string | string | string | str/unicode[5] | string | String(UTF-8) | string | string | String | ProtoString |
bytes | string | ByteString | str(Python 2), bytes(Python 3) | []byte | String(ASCII-8BIT) | ByteString | string | List | ProtoBytes |
您可以在 Protocol Buffer Encoding 中了解更多关于序列化消息时如何编码这些类型的信息。
当解析消息时,如果编码的消息字节不包含特定字段,则访问解析对象中的该字段将返回该字段的默认值。默认值是特定于类型的:
重复字段: 默认值为空(通常是相应语言中的空列表)。
映射字段: 默认值为空(通常是相应语言中的空映射)。
在 protobuf 版本中,您可以为单一的非消息字段指定显式默认值。
例如,假设您想为 SearchRequest.result_per_page
字段提供 10 的默认值:
int32 result_per_page = 3 [default = 10];
如果发送方未指定 result_per_page,则接收方将观察到以下状态:
result_per_page
字段不存在。也就是说,has_result_per_page()
(hazzer 方法)方法将返回 false。result_per_page
的值(从“getter”返回)为 10。如果发送方确实发送了 result_per_page
的值,则会忽略 10 的默认值,并从 “getter” 返回发送方的值。
有关默认值在生成代码中的工作方式的更多详细信息,请参阅您选择的语言的 generated code guide。
无法为 field_presence
特性设置为 IMPLICIT
的字段指定显式默认值。
当您定义消息类型时,您可能希望其字段仅具有预定义值列表中的一个值。
例如,假设您想为每个 SearchRequest
添加一个 corpus
字段,其中 corpus
可以是 UNIVERSAL
、WEB
、IMAGES
、LOCAL
、NEWS
、PRODUCTS
或 VIDEO
。
您可以通过向消息定义中添加一个枚举,其中包含每个可能值的常量来非常简单地做到这一点。
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
Corpus corpus = 4;
}
SearchRequest.corpus
字段的默认值为 CORPUS_UNSPECIFIED
,因为这是枚举中定义的第一个值。
在 2023 版中,枚举定义中定义的第一个值 必须 具有值零,并且应具有名称 ENUM_TYPE_NAME_UNSPECIFIED
或 ENUM_TYPE_NAME_UNKNOWN
。这是因为:
还建议此第一个默认值没有其他语义含义,只是“此值未指定”。
SearchRequest.corpus
字段等枚举字段的默认值可以像这样显式覆盖:
Corpus corpus = 4 [default = CORPUS_UNIVERSAL];
如果枚举类型是从 proto2 迁移的,并且使用 option features.enum_type = CLOSED;
,则对枚举中的第一个值没有限制。
不建议更改此类枚举的第一个值,因为它将更改使用该枚举类型的任何字段的默认值,而没有显式字段默认值。
您可以通过将相同的值分配给不同的枚举常量来定义别名。
为此,您需要将 allow_alias
选项设置为 true。
否则,协议缓冲区编译器会生成警告消息。
尽管所有别名值对于序列化都是有效的,但只有第一个值在反序列化时使用。
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // Uncommenting this line will cause a warning message.
ENAA_FINISHED = 2;
}
枚举常量必须在 32 位整数的范围内。
由于枚举值在传输格式上使用 varint 编码,负值效率低下,因此不推荐使用。
您可以在消息定义内定义枚举,如前面的示例所示,或者在外部定义 - 这些枚举可以在您的 .proto
文件中的任何消息定义中重复使用。
您还可以使用在一条消息中声明的枚举类型作为另一条消息中字段的类型,使用语法 MessageType.EnumType
当您在使用枚举的 .proto
文件上运行 protocol buffer 编译器时,将为 Java、Kotlin 或 C++ 具有相应的枚举代码,或者为 Python 生成一个特殊的 EnumDescriptor 类,用于在运行时生成的类中创建一组具有整数值的符号常量。
重要 生成的代码可能受到特定于语言的枚举器数量限制(一种语言可能只有几千个)。 查看您计划使用的语言的限制。
在反序列化期间,将保留未识别的枚举值,尽管当消息被反序列化时,其表示方式取决于语言。 在支持具有超出指定符号范围的值的开放式枚举类型的语言(例如 C++ 和 Go)中,未知的枚举值只是简单地存储为其底层的整数表示。 在具有封闭式枚举类型的语言(例如 Java)中,枚举中的一个情况用于表示未识别的值,并且可以使用特殊访问器来访问底层整数。 在任何一种情况下,如果序列化消息,未识别的值仍将与消息一起序列化。
重要 有关枚举应该如何工作以及它们在不同语言中的工作方式的信息,请参阅 generated code guide。
如果您通过完全删除枚举项或将其注释掉来 更新 枚举类型,则未来的用户可以在对类型进行自己的更新时重用数值。
如果他们后来加载相同 .proto
的旧实例,这可能会导致严重问题,包括数据损坏、隐私漏洞等。
确保这种情况不会发生的一种方法是指定已删除项的数值(和/或名称,这也会导致 JSON 序列化问题)是保留的。
如果任何未来的用户尝试使用这些标识符,协议缓冲区编译器将报错。
您可以使用 max
关键字指定保留的数值范围一直到可能的最大值。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved FOO, BAR;
}
注意,你不能在一个 reserved
语句中同时指定要保留的数值以及名称。
您可以使用其他消息类型作为字段类型。
例如,假设您想在每个 SearchResponse
消息中包含 Result
消息 - 为此,您可以在同一个 .proto
文件中定义一个 Result 消息类型,然后在 SearchResponse
中指定一个 Result
类型的字段:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
在前面的例子中,Result
消息类型和 SearchResponse
定义在同一个文件中。
如果你想要使用已经在另一个 .proto
中定义的消息类型作为字段类型,那改怎么做呢?
你可以通过导入其他 .proto
文件来使用其定义。
为了导入其他 .proto
文件的定义,你需要在你的文件顶部使用 import
语句:
import "myproject/other_protos.proto";
默认情况下,你可以通过直接导入 .proto
文件来使用其定义。
然而,某些情况下,你需要将一个 .proto
文件移动到新的路径。
与其直接移动 .proto
文件然后更新所有使用这个文件的地方,你可以在原来的地方放置一个占位的 .proto
文件,然后使用 import public
来前向引用移动到新位置的定义。
请注意,public import 功能在 Java、Kotlin、TypeScript、JavaScript、GCL 以及使用 protobuf 静态反射的 C++ 目标中不可用。
任何导入包含 import public
语句的 proto 的代码都可以传递依赖 import public
依赖项。例如:
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
protocol 编译器会在命令行上使用 -I/--proto_path
标志指定的目录集中搜索导入的文件。
如果没有给出该标志,它会在调用编译器的目录中查找。
通常,你应该将 --proto_path
标志设置为项目的根目录,并对所有导入使用完全限定名。
可以导入 proto2 和 proto3 消息类型然后在 editions 2023 消息中使用,反之亦然。
你可以像下面的例子一样,在其他消息类型内部定义和使用消息类型——这里 Result
消息是在 SearchResponse
消息内部定义的:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果你想再父消息之外的其他地方复用这个消息,你可以使用 Parent.Type
来引用它:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
你可以根据需要任意深度地嵌套消息。
在下面的例子中,请注意两个名为 Inner
的嵌套类型是完全独立的,因为它们是在不同的消息中定义的:
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
如果现有的消息类型不再满足你的所有需求——例如,你希望消息格式有一个额外的字段——但你仍然希望使用用旧格式创建的代码,不用担心! 当使用二进制传输格式时,更新消息类型而不会破坏任何现有代码非常简单。
注意
如果你使用 JSON 或者 proto text format 来存储 protobuf buffer 消息,你能在 proto 定义中进行的修改是不同的。
查看 Proto 最佳实践[译] 并遵循以下规则: Check Proto Best Practices and the following rules:
删除
,只要该字段编号在更新的消息类型中不再使用。
您可能希望重命名该字段,例如添加前缀“OBSOLETE_”,或者 保留 该字段号,以便您的 .proto
的未来用户不会意外地重复使用该编号。int32
、uint32
、int64
、uint64
和 bool
都是兼容的 - 这意味着您可以将字段从这些类型中的一个更改为另一个,而不会破坏向前或向后兼容性。
如果从网络解析的数字不适合相应的类型,您将获得与在 C++ 中将数字强制转换为该类型相同的效果(例如,如果将 64 位数字读取为 int32,它将被截断为 32 位)。sint32
和 sint64
彼此兼容,但与其他整数类型不兼容。string
和 bytes
兼容,只要 bytes 是有效的 UTF-8 编码。bytes
包含该消息的编码实例, 那么 bytes
与嵌入的消息兼容。fixed32
与 sfixed32
兼容,fixed64
与 sfixed64
兼容。string
、bytes
和 message 字段
,单数与 repeated
兼容。
给定 repeated
字段的序列化数据作为输入,期望该字段为单数的客户端将在以下情况下使用最后一个输入值:它是原始类型字段。
如果它是消息类型字段,则合并所有输入元素。
请注意,这通常对于数值类型(包括布尔值和枚举)是不安全的。
数值类型的重复字段默认以打包格式序列化,当期望为单数字段时,将无法正确解析。enum
与 int32
、uint32
、int64
和 uint64
兼容(注意,如果值不适合,将被截断)。
但是,请注意,当消息被反序列化时,客户端代码可能会以不同的方式处理它们:例如,未识别的枚举值将保留在消息中,但是当消息被反序列化时,其表示方式是依赖于语言的。
Int 字段始终只保留其值。optional
字段或 extension
字段更改为新 oneof
的成员是二进制兼容的,但是对于某些语言(尤其是 Go),生成的代码的 API 将以不兼容的方式更改。
因此,如 AIP-180 中所述,Google 不会在其公共 API 中进行此类更改。
考虑到源代码兼容性的相同警告,如果可以确定没有代码同时设置多个字段,则将多个字段移到新的 oneof
中可能是安全的。
将字段移到现有的 oneof
中是不安全的。 同样,将 只有单个字段的 oneof
更改为 optional
字段或 extension
字段是安全的。map<K, V>
和对应的 repeated 消息字段
之间进行更改是二进制兼容的(有关消息布局和其他限制,请参见下面的 映射 )。
但是,更改的安全性取决于应用程序:当反序列化和重新序列化消息时,使用重复字段定义的客户端将产生语义上相同的结果;但是,使用 map 字段定义的客户端可能会重新排序条目并删除具有重复键的条目。未知字段是格式良好的 protocol buffer 序列化数据,表示解析器无法识别的字段。 例如,当旧版二进制文件解析由新版二进制文件发送的数据(该数据包含新字段)时,这些新字段会在旧版二进制文件中成为未知字段。
Editions 消息会保留未知字段,并在解析过程中以及序列化输出中包含它们,这与 proto2 和 proto3 的行为一致。
某些操作可能会导致未知字段丢失。 例如,如果您执行以下操作之一,则未知字段将丢失:
为了避免丢失未知字段,请执行以下操作:
TextFormat 是一个特殊的情况。 序列化为 TextFormat 时,未知字段会使用其字段编号进行打印。 但如果将 TextFormat 数据重新解析为二进制 proto,且数据中包含使用字段编号的条目,则解析会失败。
扩展是一个定义在其容器消息之外的字段;通常在一个与容器消息的 .proto
文件不同的 .proto
文件中定义。
有两个使用扩展的主要原因:
让我们来查看一个扩展示例:
// file kittens/video_ext.proto
import "kittens/video.proto";
import "media/user_content.proto";
package kittens;
// This extension allows kitten videos in a media.UserContent message.
extend media.UserContent {
// Video is a message imported from kittens/video.proto
repeated Video kitten_videos = 126;
}
注意定义了扩展的文件(kittens/video_ext.proto
)导入了容器消息文件(media/user_content.proto
)。
容器消息必须保留字段编号的子集用于扩展:
// file media/user_content.proto
package media;
// A container message to hold stuff that a user has created.
message UserContent {
// Set verification to `DECLARATION` to enforce extension declarations for all
// extensions in this range.
extensions 100 to 199 [verification = DECLARATION];
}
容器消息的文件(media/user_content.proto
)定义了消息 UserContent
,这个消息保留了字段编号 [100 到 199] 用于扩展。
推荐将这些为扩展保留的编号设置 verification = DECLARATION
。
当添加新的扩展(kittens/video_ext.proto
)时,应该在 UserContent
中添加相应的声明,并移除验证。
// A container message to hold stuff that a user has created.
message UserContent {
extensions 100 to 199 [
declaration = {
number: 126,
full_name: ".kittens.kitten_videos",
type: ".kittens.Video",
repeated: true
},
// Ensures all field numbers in this extension range are declarations.
verification = DECLARATION
];
}
UserContent
声明字段编号 126 将用于一个repeated
的扩展字段,具有完全限定的名称 .kittens.kitten_videos
和完全限定的类型 .kittens.Video
。
有关扩展声明的更多信息,请参见 扩展声明。
请注意,容器消息的文件(media/user_content.proto
)没有 导入 kitten_video 扩展定义(kittens/video_ext.proto
)。
扩展字段的底层传输编码与具有相同字段编号、类型和基数的标准字段没有区别。 因此,只要字段编号、类型和基数保持不变,将标准字段移出其容器作为扩展,或者将扩展字段移入容器消息作为标准字段,都是安全的。
然而,由于扩展定义在容器消息之外,因此不会生成专门的访问器来获取和设置特定的扩展字段。
在我们的示例中,protobuf 编译器不会生成 AddKittenVideos()
或 GetKittenVideos()
访问器。
相反,扩展是通过参数化的函数进行访问的,如:HasExtension()
、ClearExtension()
、GetExtension()
、MutableExtension()
和 AddExtension()
。
在 C++ 里面,相关操作类似:
UserContent user_content;
user_content.AddExtension(kittens::kitten_videos, new kittens::Video());
assert(1 == user_content.GetExtensionCount(kittens::kitten_videos));
user_content.GetExtension(kittens::kitten_videos, 0);
如果你是容器消息的所有者,你需要为该消息的扩展定义一个扩展范围。
分配给扩展字段的字段编号不能被标准字段重复使用。
在定义扩展范围后,扩展该范围是安全的。 一个好的默认做法是分配 1000 个相对较小的数字,并通过扩展声明密集地填充该空间:
message ModernExtendableMessage {
// All extensions in this range should use extension declarations.
extensions 1000 to 2000 [verification = DECLARATION];
}
在为扩展声明添加范围之前,你应该添加 verification = DECLARATION
来强制要求该新范围使用声明。
一旦添加了实际的声明,这个占位符可以被移除。
将现有的扩展范围拆分为覆盖相同总范围的独立范围是安全的。 这可能在将遗留消息类型迁移到 扩展声明 时是必要的。 例如,在迁移之前,该范围可能被定义为:
message LegacyMessage {
extensions 1000 to max;
}
迁移后(拆分范围)它可以是:
message LegacyMessage {
// Legacy range that was using an unverified allocation scheme.
extensions 1000 to 524999999 [verification = UNVERIFIED];
// Current range that uses extension declarations.
extensions 525000000 to max [verification = DECLARATION];
}
增加起始字段编号或减少结束字段编号以移动或缩小扩展范围是不安全的。这些更改可能会使现有扩展失效。
建议使用字段编号 1 到 15 为大多数原型实例中填充的标准字段。 并不推荐将这些编号用于扩展。
如果你的编号约定可能涉及扩展使用非常大的字段编号,你可以使用 max 关键字指定扩展范围直到最大可能的字段编号:
message Foo {
extensions 1000 to max;
}
max
的值是 229-1,或者 536,870,911。
扩展只是可以在其容器消息之外指定的字段。 分配字段编号 的所有规则同样适用于扩展字段编号。 字段编号重复使用后果 也同样适用于重用扩展字段编号。
如果容器消息使用 扩展声明,选择唯一的扩展字段编号是很简单的。 在定义新的扩展时,选择高于容器消息中定义的最高扩展范围的所有其他声明的最低字段编号。 例如,如果容器消息定义如下:
message Container {
// Legacy range that was using an unverified allocation scheme
extensions 1000 to 524999999;
// Current range that uses extension declarations. (highest extension range)
extensions 525000000 to max [
declaration = {
number: 525000001,
full_name: ".bar.baz_ext",
type: ".bar.Baz"
}
// 525,000,002 is the lowest field number above all other declarations
];
}
容器
的下一个扩展应该添加一个编号为 525000002
的新声明。
你可以在另一个消息的作用域中声明扩展:
import "common/user_profile.proto";
package puppies;
message Photo {
extend common.UserProfile {
int32 likes_count = 111;
}
...
}
在这种情况下,使用 C++ 代码访问扩展如下:
UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);
换句话说,唯一的效果是 likes_count
在 puppies.Photo
的作用域内定义。
这是一个常见的误区:在消息类型内部声明一个 extend
块并不意味着外部类型与扩展类型之间存在任何关系。
特别是,前面的例子并不意味着 Photo
是 UserProfile
的某种子类。
它的意思只是符号 likes_count
在 Photo
的作用域内声明;它只是一个静态成员。
一种常见的模式是在扩展字段类型的作用域内定义扩展——例如,下面是一个扩展类型为 puppies.Photo
的 media.UserContent
扩展,其中扩展作为 Photo
的一部分定义:
import "media/user_content.proto";
package puppies;
message Photo {
extend media.UserContent {
Photo puppy_photo = 127;
}
...
}
然而,并没有要求具有消息类型的扩展必须在该类型内部定义。你也可以使用标准的定义模式:
import "media/user_content.proto";
package puppies;
message Photo {
...
}
// This can even be in a different file.
extend media.UserContent {
Photo puppy_photo = 127;
}
为了避免混淆,建议使用这种标准(文件级)语法。 嵌套语法通常会被不熟悉扩展的用户误解为子类化。
Any
消息类型允许你将消息用作嵌入类型,而无需其 .proto
定义。
Any
包含一个任意序列化的消息(作为字节),以及一个 URL,作为该消息类型的全局唯一标识符并解析到该类型。
要使用 Any
类型,你需要 导入 google/protobuf/any.proto
。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
给定消息类型的默认类型 URL 是 type.googleapis.com/packagename.messagename
。
不同语言的实现将支持运行时库助手,以类型安全的方式打包和解包 Any 值。
例如,在 Java 中,Any 类型将具有特殊的 pack()
和 unpack()
访问器,而在 C++ 中,则有 PackFrom()
和 UnpackTo()
方法。
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
如果你想限制包含的消息类型数量,并且在向列表中添加新类型之前需要获得许可,考虑使用带有 扩展声明 的 扩展,而不是使用 Any 消息类型。
如果你的消息中有许多单一字段,并且最多只有一个字段会同时被设置,你可以通过使用 oneof 特性来强制这种行为并节省内存。
Oneof 字段类似于单一字段,不同之处在于 oneof 中的所有字段共享内存,并且最多只能同时设置一个字段。
设置 oneof 的任何成员会自动清除所有其他成员。
你可以使用特定的 case()
或 WhichOneof()
方法来检查 oneof 中是否设置了某个值(如果有的话),具体方法取决于你选择的编程语言。
请注意,如果多个值被设置,最后设置的值将覆盖之前的所有值,具体顺序取决于 proto 文件中的顺序。
Oneof 字段的字段编号在包含它们的消息中必须是唯一的。
在你的 .proto
文件中定义 oneof
时,使用 oneof
关键字,后跟 oneof 的名称,在这个例子中是 test_oneof
:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后,你将 oneof 字段添加到 oneof 定义中。
你可以添加任何类型的字段,除了 map
字段和 repeated
字段。
如果你需要将一个 repeated 字段添加到 oneof 中,可以使用一个包含该 repeated 字段的消息。
在生成的代码中,oneof 字段具有与常规字段相同的 getter 和 setter。 你还会得到一个特殊的方法来检查 oneof 中设置了哪个值(如果有的话)。 你可以在相关的 API 文档 中找到关于所选语言的 oneof API 的更多信息。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
// Calling mutable_sub_message() will clear the name field and will set
// sub_message to a new instance of SubMessage with none of its fields set.
message.mutable_sub_message();
CHECK(!message.has_name());
sub_message
已经通过调用 set_name()
方法被删除。SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
交换
两个包含 oneof 的消息,最终每个消息将会拥有另一个消息的 oneof case:在下面的示例中,msg1
将拥有 sub_message
,而 msg2
将拥有 name
。SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());
在添加或移除 oneof 字段时要小心。
如果检查 oneof 的值时返回 None/NOT_SET
,这可能意味着 oneof 尚未被设置,或者它已被设置为 oneof 的不同版本中的某个字段。
无法区分这两种情况,因为没有办法知道底层传输格式中的未知字段是否是 oneof 的成员。
如果你想在数据定义中创建一个关联映射,protocol buffers 提供了一个方便的快捷语法:
map<key_type, value_type> map_field = N;
…其中,key_type
可以是任何整数类型或字符串类型(即,除了浮点类型和字节类型之外的任何标量类型)。
请注意,enum 和 proto 消息类型都不能作为 key_type
。
value_type
可以是任何类型,除了另一个 map 类型。
例如,如果你想创建一个项目映射,其中每个 Project
消息与一个字符串键相关联,你可以这样定义:
map<string, Project> projects = 3;
.proto
生成文本格式时,map 会根据键进行排序。数值键会按数字顺序排序。FooEntry
不能与 map foo
在同一作用域中存在,因为 FooEntry
已经被 map 的实现使用。生成的 map API 当前在所有支持的语言中都可用。 你可以在相关的 API 文档 中找到关于所选语言的 map API 的更多信息。
map 语法在的等价形式如下,因此不支持 map 的 protocol buffers 实现仍然可以处理你的数据:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支持 map 的协议缓冲区实现,必须既能够生成,也能够接收可以被之前定义接受的数据。
你可以在 .proto
文件中添加一个可选的 package
说明符,以防止协议消息类型之间的命名冲突。
package foo.bar;
message Open { ... }
然后,在定义消息类型的字段时,你可以使用包说明符:
message Foo {
...
foo.bar.Open open = 1;
...
}
package 说明符对生成的代码的影响取决于你选择的语言:
Open
会位于命名空间 foo::bar
中。.proto
文件中显式提供了 option java_package
。package
指令会被忽略,因为 Python 模块是根据文件系统中的位置来组织的。.pb.go
文件会位于与对应的 go_proto_library
Bazel 规则命名相同的包中。
对于开源项目,你 必须 提供 option go_package
或设置 Bazel 的 -M
标志。PB_
)。
例如,Open
会位于命名空间 Foo::Bar
中。package
会在转换为 PascalCase 后用作命名空间,除非你在 .proto
文件中显式提供了 option php_namespace
。
例如,Open
会位于命名空间 Foo\Bar
中。.proto
文件中显式提供了 option csharp_namespace
。
例如,Open
会位于命名空间 Foo.Bar
中。请注意,即使 package
指令不会直接影响生成的代码(例如在 Python 中),仍然强烈建议为 .proto 文件指定包,因为否则可能会导致描述符中的命名冲突,并使得 proto 文件无法在其他语言中移植。
protocol buffer 语言中的类型名称解析方式类似于 C++:首先搜索最内层的作用域,然后是下一个内层作用域,依此类推,每个包被视为其父包的“内部”包。
一个前导的点(例如,.foo.bar.Baz
)表示从最外层作用域开始搜索。
protocol buffer 编译器通过解析导入的 .proto
文件来解析所有类型名称。
每种语言的代码生成器知道如何在该语言中引用每个类型,即使它有不同的作用域规则。
如果你想将消息类型与 RPC(远程过程调用)系统一起使用,可以在 .proto
文件中定义一个 RPC 服务接口,protocol buffer 编译器将生成所选语言的服务接口代码和存根。
例如,如果你想定义一个 RPC 服务,其中的方法接受 SearchRequest
并返回 SearchResponse
,你可以在 .proto
文件中按如下方式定义:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
与 protocol buffer 一起使用的最直接的 RPC 系统是 gRPC:这是一个语言和平台中立的开源 RPC 系统,由 Google 开发。
gRPC 与 protocol buffer 特别兼容,并允许你使用特殊的 protocol buffer 编译器插件直接从 .proto
文件生成相关的 RPC 代码。
如果你不想使用 gRPC,也可以将 protocol buffer 与自定义的 RPC 实现一起使用。 你可以在 Proto2 语言指南 中了解更多信息。
此外,还有一些正在进行的第三方项目,用于为 protocol buffer 开发 RPC 实现。 有关我们已知的项目链接列表,请参阅 第三方插件维基页面。
标准的 protobuf 二进制网络传输格式是使用 protobuf 进行系统间通信时首选的序列化格式。 对于与使用 JSON 而非 protobuf 网络传输格式的系统进行通信,Protobuf 支持在 ProtoJSON 中的规范编码。
.proto
文件中的单独声明可以使用许多 option
进行注解。
选项不会改变声明的整体含义,但可能会影响它在特定上下文中的处理方式。
所有可用选项的完整列表定义在 /google/protobuf/descriptor.proto
中。
有些选项是文件级选项,意味着它们应该写在顶层作用域中,而不是在任何消息、枚举或服务定义内部。 有些选项是消息级选项,意味着它们应该写在消息定义内部。 有些选项是字段级选项,意味着它们应该写在字段定义内部。 选项也可以写在枚举类型、枚举值、oneof 字段、服务类型和服务方法上;然而,目前没有任何有用的选项存在于这些位置。
以下是一些最常用的选项:
java_package
(文件选项):你想要为生成的 Java/Kotlin 类使用的包。
如果在 .proto
文件中没有显式给出 java_package
选项,则默认使用 proto 包(在 .proto 文件中使用 package
关键字指定的包)。
然而,proto 包通常不适合作为 Java 包,因为 proto 包通常不会以反向域名的方式开始。
如果没有生成 Java 或 Kotlin 代码,则此选项没有任何效果。option java_package = "com.example.foo";
java_outer_classname
(文件选项): 你想要生成的包装器 Java 类的类名(因此也是文件名)。
如果在 .proto
文件中没有显式指定 java_outer_classname
,则类名将通过将 .proto
文件名转换为驼峰命名法来构造(例如,foo_bar.proto
会变成 FooBar.java
)。
如果禁用了 java_multiple_files
选项,则为 .proto
文件生成的所有其他类/枚举等将作为嵌套类/枚举等生成在这个外部包装器 Java 类中。
如果没有生成 Java 代码,则此选项没有任何效果。option java_outer_classname = "Ponycopter";
java_multiple_files
(文件选项): 如果为 false,则只会为该 .proto
文件生成一个单独的 .java
文件,且为顶层消息、服务和枚举生成的所有 Java 类/枚举等将嵌套在一个外部类中(见 java_outer_classname
)。
如果为 true,则为每个为顶层消息、服务和枚举生成的 Java 类/枚举等生成单独的 .java
文件,并且为该 .proto
文件生成的包装器 Java 类将不包含任何嵌套类/枚举等。
这是一个布尔选项,默认值为 false
。
如果没有生成 Java 代码,则此选项没有任何效果。option java_multiple_files = true;
optimize_for
(文件选项): 可以设置为 SPEED
, CODE_SIZE
, 或者 LITE_RUNTIME
。
这会以以下方式影响 C++ 和 Java 代码生成器(以及可能的第三方生成器):
SPEED
(默认模式):protocol buffer 编译器将生成用于序列化、解析和执行其他常见操作的代码。这些代码经过高度优化。CODE_SIZE
:protocol buffer 编译器将生成最小化的类,并依赖共享的基于反射的代码来实现序列化、解析和各种其他操作。生成的代码因此会比 SPEED
模式下的小得多,但操作会变得较慢。类仍然会实现与 SPEED
模式下完全相同的公共 API。此模式最适用于包含大量 .proto
文件的应用程序,并且不需要所有操作都非常快速。LITE_RUNTIME
:协议缓冲区编译器将生成仅依赖于“lite”运行时库(libprotobuf-lit
e 而不是 libprotobuf
)的类。lite 运行时比完整的库小得多(约小一个数量级),但省略了某些功能,如描述符和反射。这对于在受限平台(如手机)上运行的应用程序特别有用。编译器仍会像在 SPEED
模式下那样生成所有方法的快速实现。生成的类将仅实现每种语言中的 MessageLite
接口,该接口只提供完整 Message
接口的子集方法。option optimize_for = CODE_SIZE;
cc_generic_services
, java_generic_services
, py_generic_services
(文件选项): 通用服务已被弃用。
protocol buffer 编译器是否应分别在 C++、Java 和 Python 中基于 服务定义 生成抽象服务代码。
出于遗留原因,这些选项默认为 true。
然而,从 2.3.0 版本(2010 年 1 月)开始,建议 RPC 实现提供 代码生成器插件,以生成更适合各个系统的代码,而不是依赖于“抽象”服务。// This file relies on plugins to generate service code.
option cc_generic_services = false;
option java_generic_services = false;
option py_generic_services = false;
cc_enable_arenas
(文件选项): 为 C++ 生成 arena allocation 相关代码objc_class_prefix
(文件选项): 设置 Objective-C 类的前缀,该前缀将添加到从此 .proto 生成的所有 Objective-C 类和枚举的前面。
没有默认值。根据 Apple 的建议,你应使用 3 到 5 个大写字母的前缀。
请注意,所有 2 个字母的前缀均由 Apple 保留。packed
(字段选项): 在 protobuf editions, 此选项被锁定为 true
。
要使用未打包的网络传输格式,你可以通过编辑功能覆盖此选项。
这提供了与 2.3.0 版本之前的解析器的兼容性(很少需要),如下例所示:repeated int32 samples = 4 [features.repeated_field_encoding = EXPANDED];
deprecated
(字段选项): 如果设置为 true
,表示该字段已弃用,不应被新代码使用。
在大多数语言中,这没有实际效果。
在 Java 中,这会变成 @Deprecated
注解。
对于 C++,当使用已弃用的字段时,clang-tidy 会生成警告。
未来,其他语言特定的代码生成器可能会在字段的访问器上生成弃用注解,这将导致编译尝试使用该字段的代码时发出警告。
如果该字段未被任何人使用,并且你想防止新用户使用它,可以考虑用 reserved 语句替换字段声明。int32 old_field = 6 [deprecated = true];
支持枚举值选项。你可以使用 deprecated
选项来表示某个值不再使用。
你还可以使用扩展创建自定义选项。
以下示例展示了添加这些选项的语法:
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
string string_name = 123456789;
}
enum Data {
DATA_UNSPECIFIED = 0;
DATA_SEARCH = 1 [deprecated = true];
DATA_DISPLAY = 2 [
(string_name) = "display_value"
];
}
读取 string_name
选项的 C++ 代码可能如下所示:
const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);
请参阅 自定义选项 以了解如何将自定义选项应用于枚举值和字段。
Protocol Buffers 还允许你定义和使用自己的选项。 请注意,这是一个 高级特性,大多数人不需要。 如果你确实认为需要创建自己的选项,请参阅 Proto2 语言指南 了解详细信息。 需要注意的是,创建自定义选项使用了 扩展。
选项具有保留概念,用于控制选项是否在生成的代码中保留。
默认情况下,选项具有运行时保留,这意味着它们会在生成的代码中保留,因此在生成的描述符池中在运行时可见。
然而,你可以设置 retention = RETENTION_SOURCE
来指定某个选项(或选项中的字段)在运行时不应保留。这被称为源保留。
选项保留是一个高级特性,大多数用户无需担心,但如果你希望在不增加二进制文件代码大小的情况下使用某些选项,它可能会很有用。 具有源保留的选项仍然可以在 protoc 和 protoc 插件中看到,因此代码生成器可以使用它们来自定义行为。
保留可以直接设置在选项上,像这样:
extend google.protobuf.FileOptions {
int32 source_retention_option = 1234
[retention = RETENTION_SOURCE];
}
它也可以设置在普通字段上,在这种情况下,只有当该字段出现在选项内部时,才会生效:
message OptionsMessage {
int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}
你可以设置 retention = RETENTION_RUNTIME
,但这没有任何效果,因为这是默认行为。
当一个消息字段标记为 RETENTION_SOURCE
时,其整个内容会被丢弃;其中的字段不能通过尝试设置 RETENTION_RUNTIME
来覆盖这一行为。
注意 从 Protocol Buffers 22.0 开始,选项保留的支持仍在进行中,目前仅支持 C++ 和 Java。 Go 从 1.29.0 版本开始支持此功能。Python 的支持已经完成,但尚未发布。
字段具有一个 targets
选项,用于控制字段作为选项使用时可以应用于哪些类型的实体。
例如,如果一个字段设置了 targets = TARGET_TYPE_MESSAGE
,那么该字段就不能在枚举(或任何其他非消息实体)的自定义选项中设置。
protoc 会强制执行这一点,并在违反目标约束时抛出错误。
乍一看,考虑到每个自定义选项都是某个特定实体的选项消息的扩展,已经将选项约束到该实体,因此这个功能似乎没有必要。 然而,在共享选项消息应用于多个实体类型的情况下,选项目标是有用的,因为你可以控制消息中单个字段的使用。例如:
message MyOptions {
string file_only_option = 1 [targets = TARGET_TYPE_FILE];
int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
targets = TARGET_TYPE_ENUM];
}
extend google.protobuf.FileOptions {
MyOptions file_options = 50000;
}
extend google.protobuf.MessageOptions {
MyOptions message_options = 50000;
}
extend google.protobuf.EnumOptions {
MyOptions enum_options = 50000;
}
// OK: this field is allowed on file options
option (file_options).file_only_option = "abc";
message MyMessage {
// OK: this field is allowed on both message and enum options
option (message_options).message_and_enum_option = 42;
}
enum MyEnum {
MY_ENUM_UNSPECIFIED = 0;
// Error: file_only_option cannot be set on an enum.
option (enum_options).file_only_option = "xyz";
}
要生成与 .proto 文件中定义的消息类型一起使用的 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 代码,你需要在 .proto
文件上运行 protocol buffer 编译器 protoc。
如果你还没有安装编译器,可以 下载安装包 并按照 README 中的说明进行安装。
对于 Go,你还需要为编译器安装一个特殊的代码生成插件;你可以在 GitHub 上的 golang/protobuf 仓库中找到该插件及其安装说明。
协议编译器的调用方式如下:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
IMPORT_PATH
指定了在解析导入指令时查找 .proto
文件的目录。
如果省略此选项,将使用当前目录。
可以通过多次传递 --proto_path
选项来指定多个导入目录;这些目录将按顺序进行搜索。
-I=_IMPORT_PATH_
可以作为 --proto_path
的简写形式。--cpp_out
生成 C++ 代码到 DST_DIR
。有关更多信息,请参阅 C++ 生成代码参考。--java_out
生成 Java 代码到 DST_DIR
。有关更多信息,请参阅 Java 生成代码参考。--kotlin_out
生成额外的 Kotlin 代码到 DST_DIR
。有关更多信息,请参阅 Kotlin 生成代码参考。--python_out
生成 Python 代码到 DST_DIR
。有关更多信息,请参阅 Python 生成代码参考。--go_out
生成 Go 代码到 DST_DIR
。有关更多信息,请参阅 Go 生成代码参考。--ruby_out
生成 Ruby 代码到 DST_DIR
。有关更多信息,请参阅 Ruby 生成代码参考。--objc_out
生成 Objective-C 代码到 DST_DIR
。有关更多信息,请参阅 Objective-C 生成代码参考。--csharp_out
生成 C# 代码到 DST_DIR
。有关更多信息,请参阅 C# 生成代码参考。--php_out
生成 PHP 代码到 DST_DIR
。有关更多信息,请参阅 PHP 生成代码参考。
作为额外的便捷功能,如果 DST_DIR
以 .zip
或 .jar
结尾,编译器将把输出写入一个具有给定名称的单一 ZIP 格式归档文件。
.jar
输出还会附带一个清单文件,这是 Java JAR 规范所要求的。
请注意,如果输出归档文件已经存在,它将被覆盖。.proto
文件作为输入。可以一次指定多个 .proto
文件。
尽管文件是相对于当前目录命名的,但每个文件必须位于一个 IMPORT_PATH
中,以便编译器能够确定其规范名称。最好不要将 .proto
文件放在与其他语言源代码相同的目录中。
可以考虑在项目的根包下为 .proto
文件创建一个子包 proto。
在处理 Java 代码时,将相关的 .proto
文件放在与 Java 源代码相同的目录中是很方便的。
然而,如果其他非 Java 代码也使用相同的 proto 文件,那么路径前缀将不再有意义。
因此,通常应该将 proto 文件放在一个与语言无关的相关目录中,例如 //myteam/mypackage
。
此规则的例外情况是,当明确知道 proto 文件只会在 Java 环境中使用时,例如用于测试。
有关以下内容的信息: