应用说明:字段存在
背景
字段存在是指 protobuf 字段是否具有值的概念。protobuf 的存在有两种不同的体现:隐式存在,生成的 message API 只存储字段值;以及显式存在,API 还存储字段是否已被设置。
注意
我们建议始终为 proto3 基本类型添加optional
标签。这为版本迁移提供了一条更平滑的路径,因为版本默认使用显式存在。存在规则
存在规则定义了 API 表示和序列化表示之间转换的语义。隐式存在规则在(反)序列化时依靠字段值本身来做出决策,而显式存在规则则依靠显式跟踪状态。
标签-值流(线格式)序列化中的存在
线格式是带标签的、自定界值的流。根据定义,线格式表示一系列存在的值。换句话说,序列化中找到的每个值都代表一个存在的字段;此外,序列化不包含关于不存在的值的信息。
为 proto message 生成的 API 包含(反)序列化定义,这些定义在 API 类型和定义上存在的(标签,值)对流之间进行转换。这种转换设计为在消息定义发生更改时保持前向和后向兼容;然而,这种兼容性在反序列化线格式消息时引入了一些(可能令人惊讶的)注意事项
- 序列化时,具有隐式存在的字段如果包含其默认值则不会被序列化。
- 对于数字类型,默认值为 0。
- 对于枚举,默认值是值为零的枚举器。
- 对于字符串、字节和重复字段,默认值是零长度值。
- “空”的长度定界值(例如空字符串)可以在序列化值中有效表示:该字段是“存在”的,因为它出现在线格式中。但是,如果生成的 API 不跟踪存在,则这些值可能不会被重新序列化;即,空字段在序列化往返后可能“不存在”。
- 反序列化时,重复的字段值可能会根据字段定义以不同的方式处理。
- 重复的
repeated
字段通常被附加到字段的 API 表示中。(请注意,序列化packed重复字段在线格式中只产生一个长度定界值。) - 重复的
optional
字段值遵循“最后一个获胜”的规则。
- 重复的
oneof
字段暴露出 API 级别的不变性,即一次只能设置一个字段。然而,线格式可能包含多个概念上属于oneof
的(标签,值)对。与optional
字段类似,生成的 API 遵循“最后一个获胜”的规则。- 在生成的 proto2 API 中,对于枚举字段不会返回超出范围的值。但是,即使线格式标签被识别,超出范围的值仍可能在 API 中作为未知字段存储。
命名字段映射格式中的存在
Protobuf 可以以人类可读的文本形式表示。两种值得注意的格式是 TextFormat(由生成的 message DebugString
方法产生的输出格式)和 JSON。
这些格式有其自身的正确性要求,并且通常比带标签值流格式更严格。然而,TextFormat 更接近于模仿线格式的语义,并且在某些情况下提供类似的语义(例如,将重复的名称-值映射附加到重复字段)。特别是,与线格式类似,TextFormat 只包含存在的字段。
然而,JSON 是一种更严格的格式,无法有效表示线格式或 TextFormat 的某些语义。
- 值得注意的是,JSON 元素在语义上是无序的,并且每个成员必须具有唯一的名称。这与 TextFormat 中重复字段的规则不同。
- 与针对其他格式的隐式存在规则不同,JSON 可能包含“不存在”的字段
- JSON 定义了一个
null
值,该值可用于表示一个已定义但不存在的字段。 - 重复字段值可以包含在格式化输出中,即使它们等于默认值(一个空列表)。
- JSON 定义了一个
- 由于 JSON 元素是无序的,因此无法明确解释“最后一个获胜”的规则。
- 在大多数情况下,这没问题:JSON 元素必须具有唯一的名称:重复的字段值不是有效的 JSON,因此无需像 TextFormat 中那样解析它们。
- 然而,这意味着可能无法明确解释
oneof
字段:如果存在多个 case,它们是无序的。
理论上,JSON 可以以保留语义的方式表示存在。然而在实践中,存在的正确性可能因实现选择而异,尤其是在选择 JSON 作为与不使用 protobuf 的客户端交互的方式时。
Proto2 API 中的存在
此表概述了 proto2 API 中字段是否存在被跟踪(包括生成 API 和使用动态反射)
字段类型 | 显式存在 |
---|---|
单一数字(整数或浮点数) | ✔️ |
单一枚举 | ✔️ |
单一字符串或字节 | ✔️ |
单一消息 | ✔️ |
重复字段 | |
Oneof 字段 | ✔️ |
Map 字段 |
单一字段(所有类型)在生成的 API 中显式跟踪存在。生成的 message 接口包含查询字段存在的方法。例如,字段 foo
对应一个 has_foo
方法。(具体的名称遵循与字段访问器相同的特定语言命名约定。)这些方法有时在 protobuf 实现中被称为“hazzers”。
与单一字段类似,oneof
字段显式跟踪其中哪个成员(如果有)包含值。例如,考虑这个 oneof
示例
oneof foo {
int32 a = 1;
float b = 2;
}
根据目标语言,生成的 API 通常会包含几种方法
- oneof 的 hazzer:
has_foo
- 一个 oneof case 方法:
foo
- 成员的 Hazzers:
has_a
,has_b
- 成员的 Getters:
a
,b
重复字段和 map 字段不跟踪存在:空重复字段与不存在的重复字段之间没有区别。
Proto3 API 中的存在
此表概述了 proto3 API 中字段是否存在被跟踪(包括生成 API 和使用动态反射)
字段类型 | optional | 显式存在 |
---|---|---|
单一数字(整数或浮点数) | 否 | |
单一数字(整数或浮点数) | 是 | ✔️ |
单一枚举 | 否 | |
单一枚举 | 是 | ✔️ |
单一字符串或字节 | 否 | |
单一字符串或字节 | 是 | ✔️ |
单一消息 | 否 | ✔️ |
单一消息 | 是 | ✔️ |
重复字段 | 不适用 | |
Oneof 字段 | 不适用 | ✔️ |
Map 字段 | 不适用 |
与 proto2 API 类似,proto3 不会显式跟踪重复字段的存在。如果没有 optional
标签,proto3 API 也不会跟踪基本类型(数字、字符串、字节和枚举)的存在。Oneof 字段明确暴露存在,尽管生成的 hazzer 方法集可能与 proto2 API 不同。
在没有 optional
标签的情况下不跟踪存在的这一默认行为与 proto2 的行为不同。我们建议在使用 proto3 时使用 optional
标签,除非您有特定的理由不这样做。
在隐式存在规则下,默认值在序列化目的上等同于“不存在”。为了概念上“清除”一个字段(使其不被序列化),API 用户会将其设置为默认值。
在隐式存在下,枚举类型字段的默认值是对应的值为 0 的枚举器。根据 proto3 语法规则,所有枚举类型都必须有一个映射到 0 的枚举器值。按照惯例,这是一个 UNKNOWN
或类似名称的枚举器。如果零值在概念上超出应用程序的有效值范围,此行为可以被视为等同于显式存在。
版本 API 中的存在
此表概述了版本 API 中字段是否存在被跟踪(包括生成 API 和使用动态反射)
字段类型 | 显式存在 |
---|---|
单一数字(整数或浮点数) | ✔️ |
单一枚举 | ✔️ |
单一字符串或字节 | ✔️ |
单一消息† | ✔️ |
重复字段 | |
Oneof 字段† | ✔️ |
Map 字段 |
† 消息和 oneof 字段从未有过隐式存在,并且版本不允许将 field_presence
设置为 IMPLICIT
。
基于版本的 API 显式跟踪字段存在,类似于 proto2,除非将 features.field_presence
设置为 IMPLICIT
。与 proto2 API 类似,基于版本的 API 不会显式跟踪重复字段的存在。
语义差异
当设置了默认值时,隐式存在序列化规则会导致与显式存在跟踪规则明显的差异。对于具有数字、枚举或字符串类型的单一字段
- 隐式存在规则
- 默认值不被序列化。
- 默认值不被合并进来。
- 要“清除”一个字段,将其设置为默认值。
- 默认值可能意味着
- 该字段被显式设置为其默认值,这在应用程序特定值域中是有效的;
- 该字段通过设置其默认值而概念上被“清除”;或
- 该字段从未被设置。
- 不生成
has_
方法(但请参阅此列表后的注释)
- 显式存在规则
- 显式设置的值始终被序列化,包括默认值。
- 未设置的字段永远不会被合并进来。
- 显式设置的字段——包括默认值——会被合并进来。
- 生成的
has_foo
方法指示字段foo
是否已被设置(且未被清除)。 - 必须使用生成的
clear_foo
方法来清除(即取消设置)该值。
注意
在大多数情况下,不会为隐式成员生成Has_
方法。此行为的例外是 Dart,它会为带有 proto3 proto 模式文件的字段生成 has_
方法。合并时的注意事项
在隐式存在规则下,目标字段实际上不可能从其默认值合并(使用 protobuf 的 API 合并函数)。这是因为默认值会被跳过,类似于隐式存在序列化规则。合并只使用来自更新(合并来源)消息的非跳过值来更新目标(合并到)消息。
合并行为的差异对依赖于部分“补丁”更新的协议有进一步的影响。如果未跟踪字段存在,则仅靠更新补丁无法表示对默认值的更新,因为只有非默认值会被合并进来。
在这种情况下,更新以设置默认值需要一些外部机制,例如 FieldMask
。然而,如果跟踪了存在,则所有显式设置的值——即使是默认值——都将合并到目标中。
变更兼容性注意事项
在线格式中,将字段从显式存在更改为隐式存在是序列化值的二进制兼容更改。然而,消息的序列化表示可能会有所不同,具体取决于用于序列化的消息定义版本。特别地,当“发送者”将字段显式设置为其默认值时
- 遵循隐式存在规则的序列化值不包含默认值,即使它已被显式设置。
- 遵循显式存在规则的序列化值包含每个“存在”的字段,即使它包含默认值。
此更改可能安全也可能不安全,取决于应用程序的语义。例如,考虑两个客户端使用不同版本的消息定义的情况。
客户端 A 使用消息的此定义,该定义对字段 foo
遵循显式存在序列化规则
syntax = "proto3";
message Msg {
optional int32 foo = 1;
}
客户端 B 使用同一消息的定义,只是它遵循无存在规则
syntax = "proto3";
message Msg {
int32 foo = 1;
}
现在,考虑这样一个场景:客户端 A 观察 foo
的存在性,而客户端通过反序列化和重新序列化重复交换“相同”的消息
// Client A:
Msg m_a;
m_a.set_foo(1); // non-default value
assert(m_a.has_foo()); // OK
Send(m_a.SerializeAsString()); // to client B
// Client B:
Msg m_b;
m_b.ParseFromString(Receive()); // from client A
assert(m_b.foo() == 1); // OK
Send(m_b.SerializeAsString()); // to client A
// Client A:
m_a.ParseFromString(Receive()); // from client B
assert(m_a.foo() == 1); // OK
assert(m_a.has_foo()); // OK
m_a.set_foo(0); // default value
Send(m_a.SerializeAsString()); // to client B
// Client B:
Msg m_b;
m_b.ParseFromString(Receive()); // from client A
assert(m_b.foo() == 0); // OK
Send(m_b.SerializeAsString()); // to client A
// Client A:
m_a.ParseFromString(Receive()); // from client B
assert(m_a.foo() == 0); // OK
assert(m_a.has_foo()); // FAIL
如果客户端 A 依赖 foo
的显式存在,那么经过客户端 B 的“往返”对于客户端 A 来说将是信息丢失的。在此示例中,这不是一个安全的更改:客户端 A 要求(通过 assert
)该字段存在;即使没有通过 API 进行任何修改,该要求在取决于值和对等方的情况下仍然失败。
如何在 Proto3 中启用显式存在
以下是使用 proto3 字段跟踪支持的一般步骤
- 将
optional
字段添加到.proto
文件中。 - 运行
protoc
(至少 v3.15 版本,或使用--experimental_allow_proto3_optional
标志的 v3.12 版本)。 - 在应用程序代码中使用生成的“hazzer”方法和“clear”方法,而不是比较或设置默认值。
.proto
文件更改
这是一个 proto3 消息的示例,其中字段遵循无存在和显式存在两种语义
syntax = "proto3";
package example;
message MyMessage {
// implicit presence:
int32 not_tracked = 1;
// Explicit presence:
optional int32 tracked = 2;
}
protoc
调用
自 v3.15.0 版本以来,proto3 消息的字段跟踪默认启用,此前直到 v3.12.0 版本,在使用 protoc 进行存在跟踪时需要 --experimental_allow_proto3_optional
标志。
使用生成代码
具有显式存在(optional
标签)的 proto3 字段生成的代码将与 proto2 文件中的相同。
以下是“隐式存在”示例中使用的定义
syntax = "proto3";
package example;
message Msg {
int32 foo = 1;
}
以下是“显式存在”示例中使用的定义
syntax = "proto3";
package example;
message Msg {
optional int32 foo = 1;
}
在示例中,函数 GetProto
构造并返回一个类型为 Msg
、内容未指定的消息。
C++ 示例
隐式存在
Msg m = GetProto();
if (m.foo() != 0) {
// "Clear" the field:
m.set_foo(0);
} else {
// Default value: field may not have been present.
m.set_foo(1);
}
显式存在
Msg m = GetProto();
if (m.has_foo()) {
// Clear the field:
m.clear_foo();
} else {
// Field is not present, so set it.
m.set_foo(1);
}
C# 示例
隐式存在
var m = GetProto();
if (m.Foo != 0) {
// "Clear" the field:
m.Foo = 0;
} else {
// Default value: field may not have been present.
m.Foo = 1;
}
显式存在
var m = GetProto();
if (m.HasFoo) {
// Clear the field:
m.ClearFoo();
} else {
// Field is not present, so set it.
m.Foo = 1;
}
Go 示例
隐式存在
m := GetProto()
if m.Foo != 0 {
// "Clear" the field:
m.Foo = 0
} else {
// Default value: field may not have been present.
m.Foo = 1
}
显式存在
m := GetProto()
if m.Foo != nil {
// Clear the field:
m.Foo = nil
} else {
// Field is not present, so set it.
m.Foo = proto.Int32(1)
}
Java 示例
这些示例使用 Builder
来演示清除操作。简单地检查存在性并从 Builder
获取值遵循与消息类型相同的 API。
隐式存在
Msg.Builder m = GetProto().toBuilder();
if (m.getFoo() != 0) {
// "Clear" the field:
m.setFoo(0);
} else {
// Default value: field may not have been present.
m.setFoo(1);
}
显式存在
Msg.Builder m = GetProto().toBuilder();
if (m.hasFoo()) {
// Clear the field:
m.clearFoo()
} else {
// Field is not present, so set it.
m.setFoo(1);
}
Python 示例
隐式存在
m = example.Msg()
if m.foo != 0:
# "Clear" the field:
m.foo = 0
else:
# Default value: field may not have been present.
m.foo = 1
显式存在
m = example.Msg()
if m.HasField('foo'):
# Clear the field:
m.ClearField('foo')
else:
# Field is not present, so set it.
m.foo = 1
Ruby 示例
隐式存在
m = Msg.new
if m.foo != 0
# "Clear" the field:
m.foo = 0
else
# Default value: field may not have been present.
m.foo = 1
end
显式存在
m = Msg.new
if m.has_foo?
# Clear the field:
m.clear_foo
else
# Field is not present, so set it.
m.foo = 1
end
Javascript 示例
隐式存在
var m = new Msg();
if (m.getFoo() != 0) {
// "Clear" the field:
m.setFoo(0);
} else {
// Default value: field may not have been present.
m.setFoo(1);
}
显式存在
var m = new Msg();
if (m.hasFoo()) {
// Clear the field:
m.clearFoo()
} else {
// Field is not present, so set it.
m.setFoo(1);
}
Objective-C 示例
隐式存在
Msg *m = [Msg message];
if (m.foo != 0) {
// "Clear" the field:
m.foo = 0;
} else {
// Default value: field may not have been present.
m.foo = 1;
}
显式存在
Msg *m = [Msg message];
if ([m hasFoo]) {
// Clear the field:
[m clearFoo];
} else {
// Field is not present, so set it.
m.foo = 1;
}
速查表
Proto2
是否跟踪字段存在?
字段类型 | 是否跟踪? |
---|---|
单一字段 | 是 |
单一消息字段 | 是 |
oneof 中的字段 | 是 |
重复字段和 map | 否 |
Proto3
是否跟踪字段存在?
字段类型 | 是否跟踪? |
---|---|
其他单一字段 | 如果定义为 optional |
单一消息字段 | 是 |
oneof 中的字段 | 是 |
重复字段和 map | 否 |
2023 版本
是否跟踪字段存在?
字段类型(按优先级降序) | 是否跟踪? |
---|---|
重复字段和 map | 否 |
消息和 Oneof 字段 | 是 |
如果 features.field_presence 设置为 IMPLICIT 的其他单一字段 | 否 |
所有其他字段 | 是 |