小心未打包的重复字段

最后需要提到的是,虽然这对于我们的 TODO API 并没有帮助,但还是值得一提。在 Protobuf 中,我们有不同的方式来编码重复字段。我们有 打包(packed)未打包(unpacked) 的重复字段。

打包的重复字段

为了理解,我们来看一个打包(packed)重复字段的例子。假设我们有如下的消息定义:

message RepeatedUInt32Values {
  repeated uint32 values = 1;
}

这只是一个包含 uint32 基本类型的简单列表。如果我们使用值 1、2 和 3 来序列化它,我们将得到以下结果:

$ cat repeated_scalar.txt | protoc --encode=RepeatedUInt32Values proto/repeated.proto | hexdump -C
0a 03 01 02 03
00000005

repeated_scalar.txt 文件的内容如下:

values: 1
values: 2
values: 3

这是一个打包的重复字段示例,因为它将多个值 “包装” 在一起。你可能会认为这是正常的,因为这只是一个列表,但我们稍后会看到,这并不总是如此。

为了理解 “包装多个值” 是什么意思,我们需要仔细查看 hexdump 显示的十六进制数据。我们有 5 个字节:0a 03 01 02 03。如我们所知,重复字段是作为长度限定类型进行序列化的。所以:

  • 0a 是类型(varint)和字段标签(1)的组合,

  • 03 表示列表中有三个元素,

  • 剩下的字节就是实际的值(1、2、3)。

未打包的重复字段

然而,序列化的重复字段数据并不总是如此紧凑。让我们来看一个未打包(unpacked)重复字段的例子。假设我们为名为 values 的字段添加了 packed 选项,并将其值设置为 false

message RepeatedUInt32Values {
  repeated uint32 values = 1 [packed = false];
}

现在,如果我们运行相同的命令并使用相同的值,应该会得到以下结果:

$ cat repeated_scalar.txt | protoc --encode=RepeatedUInt32Values proto/repeated.proto | hexdump -C
08 01 08 02 08 03
00000006

我们可以看到,数据的序列化方式完全不同。这一次,我们重复序列化 uint32。在这里,08 表示类型(varint)和标签(1),并且你可以看到它出现了三次,因为我们有三个值。如果我们在重复字段中有超过两个值,这实际上会为每个值添加一个字节。在我们的例子中,整个序列化过程占用了 6 个字节,而不是之前的 5 个字节。

现在,你可能会想:既然这样,为什么不直接不使用 packed 选项,始终使用打包字段呢?对于作用于标量的重复字段来说,你的想法是对的,但对于更复杂的类型则不然。例如,字符串、字节和用户定义的类型将始终以未打包的方式进行序列化,且无法避免。

让我们来看一个带有用户定义类型的例子。假设我们有如下的 Protobuf 代码:

message UserDefined {
  uint32 value = 1;
}
message RepeatedUserDefinedValues {
  repeated UserDefined values = 1;
}

现在,我们可以尝试运行以下命令:

$ cat repeated_ud.txt | protoc --encode=RepeatedUserDefinedValues proto/repeated.proto | hexdump -C
0a 02 08 01 0a 02 08 02 0a 02 08 03
0000000c

repeated_ud.txt 文件包含以下内容:

values: {value: 1}
values: {value: 2}
values: {value: 3}

我们可以看到,这里我们结合了本章前面提到的子消息的开销,此外,我们的重复字段是未打包的。我们有 0a02,它们对应于子消息本身,而 08 + value 则对应于名为 value 的字段。正如你所看到的,这样会浪费更多的字节。

现在,由于在复杂类型上这是无法避免的,所以说我们应该永远不要在这些类型上使用重复字段是不正确的。重复字段是一个非常有用的概念,应该谨慎使用,并且我们应当意识到它的开销。