扩展声明

详细描述了扩展声明是什么、为什么需要它们以及如何使用它们。

介绍

本页详细描述了扩展声明是什么、为什么需要它们以及如何使用它们。

如果你需要扩展的介绍,请阅读此扩展指南

动机

扩展声明旨在在常规字段和扩展之间取得一个平衡。像扩展一样,它们避免创建对字段消息类型的依赖,从而在难以或无法去除未使用消息的环境中,产生更精简的构建图和更小的二进制文件。像常规字段一样,字段名称/编号出现在包含消息中,这使得更容易避免冲突并查看已声明字段的方便列表。

使用扩展声明列出已占用的扩展编号,使用户更容易选择可用的扩展编号并避免冲突。

用法

扩展声明是扩展范围的一个选项。与 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)具有此名称或编号的扩展时,我们强制要求扩展的编号、类型和完整名称与此处前向声明的相匹配。

扩展声明需要两个具有不同包的扩展字段

package my.package;
extend Foo {
  repeated logs.proto.ValidationAnnotations event_annotations = 4;
}
package foo.package;
extend Foo {
  optional int32 bar = 999;
}

保留声明

扩展声明可以标记为 reserved: true,表示它不再活跃使用,并且扩展定义已被删除。请勿删除扩展声明或编辑其 typefull_name

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。对于存储前端的 API 或客户端关心 proto 内容但接收系统不关心的直通系统,这可能是一个不错的选择。

重复使用扩展编号的后果

扩展是在容器消息外部定义的字段;通常在单独的 .proto 文件中。这种定义的分散性使得两个开发者很容易意外地为同一个扩展字段编号创建不同的定义。

更改扩展定义的后果与更改标准字段的后果相同。重复使用字段编号会引入如何从有线格式解码 proto 的歧义。protobuf 有线格式非常精简,无法很好地检测使用一个定义编码但使用另一个定义解码的字段。

这种歧义可能在短时间内显现,例如客户端使用一个扩展定义而服务器使用另一个扩展定义进行通信时。

这种歧义也可能在更长的时间内显现,例如存储使用一个扩展定义编码的数据,然后使用第二个扩展定义检索和解码。如果第一个扩展定义在数据编码和存储后被删除,这种长期情况可能难以诊断。

这可能导致以下结果:

  1. 解析错误(最佳情况)。
  2. PII / SPII 泄露——如果使用一个扩展定义写入 PII 或 SPII,而使用另一个扩展定义读取。
  3. 数据损坏——如果使用“错误”定义读取数据,然后修改并重新写入。

数据定义歧义几乎肯定会至少花费一些调试时间。它还可能导致数据泄露或损坏,需要几个月才能清理。

使用技巧

切勿删除扩展声明

删除扩展声明会为将来意外重复使用打开大门。如果不再处理该扩展并且已删除其定义,则可以将该扩展声明标记为保留

切勿为新的扩展声明使用 reserved 列表中的字段名称或编号

保留编号可能过去曾用于字段或其他扩展。

由于在使用 textproto 时可能存在歧义,不建议使用保留字段的 full_name

切勿更改现有扩展声明的类型

更改扩展字段的类型可能导致数据损坏。

如果扩展字段是枚举或消息类型,并且该枚举或消息类型正在重命名,则更新声明名称是必需且安全的。为避免破坏,类型、扩展字段定义和扩展声明的更新应在一次提交中完成。

重命名扩展字段时请谨慎

尽管重命名扩展字段对于有线格式没有问题,但可能会破坏 JSON 和 TextFormat 解析。