扩展声明
简介
本页详细描述了什么是扩展声明、为什么需要它们以及如何使用它们。
注意
Proto3 不支持扩展(除了声明自定义选项)。然而,Proto2 和版本(editions)中完全支持扩展。如果您需要了解扩展的入门知识,请阅读这篇扩展指南。
动机
扩展声明旨在在常规字段和扩展之间取得一个理想的平衡。像扩展一样,它们避免了对字段的消息类型产生依赖,从而在难以或无法剥离未使用消息的环境中,可以获得更精简的构建图和更小的二进制文件。像常规字段一样,字段名称/编号出现在包含它们的消息中,这使得避免冲突和方便地查看已声明的字段列表变得更加容易。
通过扩展声明列出已占用的扩展编号,使用户更容易选择一个可用的扩展编号并避免冲突。
用法
扩展声明是扩展范围的一个选项。类似于 C++ 中的前向声明,您可以在不导入包含完整扩展定义的 .proto
文件的情况下,声明扩展字段的字段类型、字段名称和基数(单一或重复)。
edition = "2023";
message Foo {
extensions 4 to 1000 [
declaration = {
number: 4,
full_name: ".my.package.event_annotations",
type: ".logs.proto.ValidationAnnotations",
repeated: true },
declaration = {
number: 999,
full_name: ".foo.package.bar",
type: "int32"}];
}
此语法具有以下语义:
- 如果范围大小允许,可以在单个扩展范围内定义多个具有不同扩展编号的
declaration
。 - 如果扩展范围有任何声明,那么该范围的*所有*扩展也必须被声明。这可以防止添加未声明的扩展,并强制任何新的扩展都使用该范围的声明。
- 给定的消息类型(
.logs.proto.ValidationAnnotations
)不需要事先定义或导入。我们只检查它是否是一个有效的名称,并且可能在另一个.proto
文件中定义。 - 当这个或另一个
.proto
文件使用此名称或编号定义此消息(Foo
)的扩展时,我们会强制要求扩展的编号、类型和全名与此处前向声明的内容相匹配。
警告
避免对扩展范围组(如extensions 4, 999
)使用声明。目前尚不清楚这些声明适用于哪个扩展范围,并且当前不支持这种用法。扩展声明需要两个具有不同包的扩展字段。
package my.package;
extend Foo {
repeated logs.proto.ValidationAnnotations event_annotations = 4;
}
package foo.package;
extend Foo {
optional int32 bar = 999;
}
保留声明
一个扩展声明可以标记为 reserved: true
,以表明它不再被积极使用,并且扩展定义已被删除。不要删除扩展声明或编辑其 type
或 full_name
值。
这个 reserved
标签与常规字段的 reserved 关键字是分开的,并且**不需要拆分扩展范围**。
edition = "2023";
message Foo {
extensions 4 to 1000 [
declaration = {
number: 500,
full_name: ".my.package.event_annotations",
type: ".logs.proto.ValidationAnnotations",
reserved: true }];
}
使用在声明中被标记为 reserved
的编号的扩展字段定义将无法编译。
在 descriptor.proto 中的表示
扩展声明在 descriptor.proto 中表示为 proto2.ExtensionRangeOptions
中的字段。
message ExtensionRangeOptions {
message Declaration {
optional int32 number = 1;
optional string full_name = 2;
optional string type = 3;
optional bool reserved = 5;
optional bool repeated = 6;
}
repeated Declaration declaration = 2;
}
反射字段查找
扩展声明*不会*从常规的字段查找函数(如 Descriptor::FindFieldByName()
或 Descriptor::FindFieldByNumber()
)中返回。像扩展一样,它们可以通过扩展查找例程(如 DescriptorPool::FindExtensionByName()
)来发现。这是一个特意的选择,反映了声明不是定义,并且没有足够的信息来返回一个完整的 FieldDescriptor
。
从 TextFormat 和 JSON 的角度来看,已声明的扩展仍然像常规扩展一样工作。这也意味着将现有字段迁移到已声明的扩展将需要首先迁移该字段的任何反射性使用。
使用扩展声明分配编号
扩展像普通字段一样使用字段编号,因此为每个扩展分配一个在父消息中唯一的编号非常重要。我们建议使用扩展声明在父消息中声明每个扩展的字段编号和类型。扩展声明充当父消息所有扩展的注册表,protoc 将强制确保没有字段编号冲突。当您添加新的扩展时,选择下一个可用的编号,通常只需将先前添加的扩展编号加一即可。
提示: 对于 MessageSet
有特殊指导,其中提供了一个脚本来帮助选择下一个可用的编号。
每当您删除一个扩展时,请确保将字段编号标记为 reserved
,以消除意外重用它的风险。
这个约定只是一个建议——protobuf 团队没有能力也不希望强迫每个人为每个可扩展的消息都遵守它。如果您作为可扩展 proto 的所有者,不希望通过扩展声明来协调扩展编号,您可以选择通过其他方式提供协调。但是要非常小心,因为意外重用扩展编号可能会导致严重的问题。
避免这个问题的一种方法是完全避免使用扩展,而使用 google.protobuf.Any
。对于前端存储或传递系统(其中客户端关心 proto 的内容但接收它的系统不关心)的 API 来说,这可能是一个不错的选择。
重用扩展编号的后果
扩展是在容器消息之外定义的字段;通常在单独的 .proto 文件中。这种定义的分布使得两个开发者很容易意外地为同一个扩展字段编号创建不同的定义。
更改扩展定义对扩展和标准字段的后果是相同的。重用字段编号会在如何从线路格式解码 proto 时引入歧义。protobuf 线路格式很精简,没有提供很好的方法来检测使用一种定义编码而使用另一种定义解码的字段。
这种歧义可能在很短的时间内表现出来,例如一个客户端使用一种扩展定义,而一个服务器使用另一种定义进行通信。
这种歧义也可能在更长的时间内表现出来,例如使用一种扩展定义编码并存储数据,然后在以后使用第二种扩展定义检索和解码数据。如果第一种扩展定义在数据编码和存储后被删除,这种长期情况可能很难诊断。
这可能导致的结果是:
- 解析错误(最佳情况)。
- PII / SPII 泄露 – 如果使用一种扩展定义写入 PII 或 SPII,而使用另一种扩展定义读取。
- 数据损坏 – 如果使用“错误”的定义读取数据,然后修改并重写。
数据定义的歧义几乎肯定会耗费某人的调试时间。它也可能导致数据泄露或损坏,需要数月时间来清理。
使用技巧
切勿删除扩展声明
删除扩展声明为将来意外重用打开了大门。如果扩展不再被处理且定义被删除,可以将扩展声明标记为保留。
切勿为新的扩展声明使用 reserved
列表中的字段名称或编号
保留编号过去可能已用于字段或其他扩展。
不建议使用保留字段的 full_name
,因为在使用 textproto 时可能会产生歧义。
切勿更改现有扩展声明的类型
更改扩展字段的类型可能导致数据损坏。
如果扩展字段是枚举或消息类型,并且该枚举或消息类型正在被重命名,则更新声明名称是必需且安全的。为避免中断,类型、扩展字段定义和扩展声明的更新都应在单个提交中进行。
重命名扩展字段时请谨慎
虽然重命名扩展字段对于线路格式来说没有问题,但它可能会破坏 JSON 和 TextFormat 的解析。