扩展声明
简介
本页面详细描述了什么是扩展声明,为什么我们需要它们,以及我们如何使用它们。
注意
Proto3 不支持扩展(除了声明自定义选项)。扩展在 proto2 和版本中得到完全支持。如果您需要扩展的介绍,请阅读此扩展指南
动机
扩展声明旨在在常规字段和扩展之间取得平衡。与扩展类似,它们避免创建对字段消息类型的依赖,因此在难以或不可能剥离未使用消息的环境中,可以生成更精简的构建图和更小的二进制文件。与常规字段类似,字段名称/编号出现在封闭消息中,这使得更容易避免冲突,并查看已声明字段的便捷列表。
通过扩展声明列出已占用的扩展号,使用户更容易选择可用的扩展号并避免冲突。
用法
扩展声明是扩展范围的一种选项。与 C++ 中的前向声明类似,您可以声明扩展字段的字段类型、字段名称和基数(单数或重复),而无需导入包含完整扩展定义的 .proto
文件
syntax = "proto2";
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 关键字分开,并且不需要分解扩展范围。
syntax = "proto2";
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
。对于面向存储的 API 或客户端关心 proto 内容但接收它的系统不关心的直通系统,这可能是一个不错的选择。
重用扩展号的后果
扩展是在容器消息外部定义的字段;通常在单独的 .proto 文件中。这种定义的分布使得两个开发人员很容易意外地为同一扩展字段编号创建不同的定义。
更改扩展定义的后果与扩展和标准字段相同。重用字段编号会在如何从线路格式解码 proto 时引入歧义。protobuf 线路格式很精简,并且没有提供一种好的方法来检测使用一种定义编码和使用另一种定义解码的字段。
这种歧义可能会在短时间内显现出来,例如客户端使用一个扩展定义,而服务器使用另一个扩展定义进行通信。
这种歧义也可能在较长的时间范围内显现出来,例如存储使用一个扩展定义编码的数据,然后在以后检索并使用第二个扩展定义进行解码。如果第一个扩展定义在数据编码和存储后被删除,则这种长期情况可能难以诊断。
其结果可能是
- 解析错误(最佳情况)。
- PII / SPII 泄露 – 如果使用一个扩展定义写入 PII 或 SPII,并使用另一个扩展定义读取。
- 数据损坏 – 如果使用“错误”的定义读取数据,然后修改并重写。
数据定义歧义几乎肯定会花费某人时间进行调试,至少是这样。它还可能导致数据泄漏或损坏,需要数月才能清理干净。
使用技巧
永远不要删除扩展声明
删除扩展声明会为将来意外重用打开大门。如果不再处理扩展并且定义已删除,则可以将扩展声明标记为保留。
永远不要为新的扩展声明使用 reserved
列表中的字段名称或数字
保留的编号可能过去已用于字段或其他扩展。
不建议使用保留字段的 full_name
,因为在使用 textproto 时可能会产生歧义。
永远不要更改现有扩展声明的类型
更改扩展字段的类型可能会导致数据损坏。
如果扩展字段是枚举或消息类型,并且该枚举或消息类型正在重命名,则需要更新声明名称,并且是安全的。为避免中断,类型、扩展字段定义和扩展声明的更新应在一次提交中完成。
重命名扩展字段时要谨慎
虽然重命名扩展字段对于线路格式来说没问题,但它可能会破坏 JSON 和 TextFormat 解析。