应用说明:字段存在
背景
字段存在性(Field presence)是指 Protobuf 字段是否具有值的概念。Protobuf 的存在性有两种不同的表现形式:隐式存在性,其中生成的消息 API 仅存储字段值;以及显式存在性,其中 API 还存储字段是否已设置。
注意
我们建议始终为 proto3 基本类型添加optional
标签。这为迁移到 editions 提供了更平滑的路径,因为 editions 默认使用显式存在性。存在性规则
存在性规则定义了在 API 表示和序列化表示之间进行转换的语义。隐式存在性规则依赖于字段值本身在(反)序列化时做出决策,而显式存在性规则则依赖于显式跟踪状态。
在标签-值流(有线格式)序列化中的存在性
有线格式是带标签的、自分隔值的流。根据定义,有线格式表示一系列存在的值。换句话说,序列化中找到的每个值都代表一个存在的字段;此外,序列化不包含任何关于不存在值的信息。
为 proto 消息生成的 API 包含(反)序列化定义,这些定义在 API 类型和定义上存在的(标签,值)对流之间进行转换。此转换旨在在消息定义发生变化时保持向前和向后兼容;然而,这种兼容性在反序列化有线格式消息时引入了一些(可能令人惊讶的)注意事项。
- 序列化时,具有隐式存在性的字段如果包含其默认值,则不会被序列化。
- 对于数值类型,默认值为 0。
- 对于枚举,默认值是值为零的枚举器。
- 对于字符串、字节和 repeated 字段,默认值是零长度值。
- “空”的长度分隔值(例如空字符串)可以在序列化值中有效表示:该字段是“存在”的,因为它出现在有线格式中。但是,如果生成的 API 不跟踪存在性,那么这些值可能不会被重新序列化;也就是说,空字段在序列化往返后可能变为“不存在”。
- 反序列化时,重复的字段值可能会根据字段定义以不同的方式处理。
- 重复的
repeated
字段通常会附加到该字段的 API 表示中。(请注意,序列化一个 packed 的 repeated 字段在标签流中只产生一个长度分隔的值。) - 重复的
optional
字段值遵循“后者为准”的规则。
- 重复的
oneof
字段在 API 级别公开了不变量,即一次只有一个字段被设置。然而,有线格式可能包含多个名义上属于该oneof
的(标签,值)对。与optional
字段类似,生成的 API 遵循“后者为准”的规则。- 在生成的 proto2 API 中,不会为枚举字段返回超出范围的值。然而,超出范围的值可能会作为未知字段存储在 API 中,即使有线格式的标签被识别了。
在命名字段映射格式中的存在性
Protobuf 可以用人类可读的文本形式表示。两种值得注意的格式是 TextFormat(由生成的消息 DebugString
方法产生的输出格式)和 JSON。
这些格式有其自身的正确性要求,并且通常比标签-值流格式更严格。然而,TextFormat 更紧密地模仿了有线格式的语义,并且在某些情况下提供类似的语义(例如,将重复的名称-值映射附加到 repeated 字段)。特别是,与有线格式类似,TextFormat 仅包含存在的字段。
然而,JSON 是一种更严格的格式,不能有效表示有线格式或 TextFormat 的某些语义。
- 值得注意的是,JSON 元素在语义上是无序的,并且每个成员必须具有唯一的名称。这与 TextFormat 对 repeated 字段的规则不同。
- 与其他格式的隐式存在性规则不同,JSON 可能包含“不存在”的字段。
- JSON 定义了一个
null
值,可用于表示已定义但不存在的字段。 - 重复字段的值可能会包含在格式化输出中,即使它们等于默认值(一个空列表)。
- JSON 定义了一个
- 由于 JSON 元素是无序的,因此无法明确解释“后者为准”的规则。
- 在大多数情况下,这没有问题:JSON 元素必须具有唯一的名称:重复的字段值不是有效的 JSON,因此它们不需要像 TextFormat 那样进行解析。
- 然而,这意味着可能无法明确解释
oneof
字段:如果存在多个 case,它们是无序的。
理论上,JSON 可以以保留语义的方式表示存在性。然而在实践中,存在性的正确性可能因实现选择而异,特别是如果选择 JSON 是为了与不使用 Protobuf 的客户端进行互操作。
Proto2 API 中的存在性
此表概述了在 proto2 API 中是否跟踪字段的存在性(包括生成的 API 和使用动态反射)。
字段类型 | 显式存在性 |
---|---|
Singular 数值(整数或浮点数) | ✔️ |
Singular 枚举 | ✔️ |
Singular 字符串或字节 | ✔️ |
Singular 消息 | ✔️ |
Repeated | |
Oneof | ✔️ |
映射 |
Singular 字段(所有类型)在生成的 API 中明确跟踪存在性。生成的消息接口包括查询字段存在性的方法。例如,字段 foo
有一个对应的 has_foo
方法。(具体名称遵循与字段访问器相同的特定语言命名约定。)这些方法在 Protobuf 实现内部有时被称为“hazzers”。
与 singular 字段类似,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
Repeated 字段和 map 不跟踪存在性:空和不存在的 repeated 字段之间没有区别。
Proto3 API 中的存在性
此表概述了在 proto3 API 中是否跟踪字段的存在性(包括生成的 API 和使用动态反射)。
字段类型 | optional | 显式存在性 |
---|---|---|
Singular 数值(整数或浮点数) | 否 | |
Singular 数值(整数或浮点数) | 是 | ✔️ |
Singular 枚举 | 否 | |
Singular 枚举 | 是 | ✔️ |
Singular 字符串或字节 | 否 | |
Singular 字符串或字节 | 是 | ✔️ |
Singular 消息 | 否 | ✔️ |
Singular 消息 | 是 | ✔️ |
Repeated | 不适用 | |
Oneof | 不适用 | ✔️ |
映射 | 不适用 |
与 proto2 API 类似,proto3 不对 repeated 字段显式跟踪存在性。如果没有 optional
标签,proto3 API 也不对基本类型(数值、字符串、字节和枚举)跟踪存在性。Oneof 字段确实公开存在性,尽管可能不会生成与 proto2 API 中相同的 hazzer 方法集。
这种在没有 optional
标签时不跟踪存在性的默认行为与 proto2 的行为不同。我们建议在 proto3 中使用 optional
标签,除非您有特殊的理由不这样做。
在隐式存在性规则下,默认值在序列化时等同于“不存在”。为了在概念上“清除”一个字段(使其不会被序列化),API 用户会将其设置为默认值。
在隐式存在性下,枚举类型字段的默认值是其对应的 0 值枚举器。根据 proto3 语法规则,所有枚举类型都必须有一个映射到 0 的枚举器值。按照惯例,这是一个 UNKNOWN
或类似命名的枚举器。如果零值在概念上超出了应用程序有效值的范围,则此行为可以被认为等同于显式存在性。
Editions API 中的存在性
此表概述了在 editions API 中是否跟踪字段的存在性(包括生成的 API 和使用动态反射)。
字段类型 | 显式存在性 |
---|---|
Singular 数值(整数或浮点数) | ✔️ |
Singular 枚举 | ✔️ |
Singular 字符串或字节 | ✔️ |
Singular 消息† | ✔️ |
Repeated | |
Oneofs† | ✔️ |
映射 |
† 消息和 oneofs 从未有过隐式存在性,并且 editions 不允许您设置 field_presence = IMPLICIT
。
基于 Editions 的 API 默认显式跟踪字段存在性,类似于 proto2,除非将 features.field_presence
设置为 IMPLICIT
。与 proto2 API 类似,基于 editions 的 API 不对 repeated 字段显式跟踪存在性。
语义差异
当设置了默认值时,隐式存在性序列化规则与显式存在性跟踪规则会产生可见的差异。对于一个 singular 的数值、枚举或字符串类型的字段:
- 隐式存在性规则
- 默认值不会被序列化。
- 默认值不会被合并来源(merged-from)。
- 要“清除”一个字段,需要将其设置为其默认值。
- 默认值可能意味着:
- 该字段被显式设置为其默认值,这在特定于应用程序的值域中是有效的;
- 该字段通过设置其默认值在概念上被“清除”了;或者
- 该字段从未被设置过。
- 不会生成
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 使用字段跟踪支持的一般步骤:
- 向
.proto
文件添加一个optional
字段。 - 运行
protoc
(至少 v3.15,或者 v3.12 使用--experimental_allow_proto3_optional
标志)。 - 在应用程序代码中使用生成的“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
字段存在性是否被跟踪?
字段类型 | 是否跟踪? |
---|---|
Singular 字段 | 是 |
Singular 消息字段 | 是 |
oneof 中的字段 | 是 |
Repeated 字段和 map | 否 |
Proto3
字段存在性是否被跟踪?
字段类型 | 是否跟踪? |
---|---|
其他 singular 字段 | 如果定义为 optional |
Singular 消息字段 | 是 |
oneof 中的字段 | 是 |
Repeated 字段和 map | 否 |
Edition 2023
字段存在性是否被跟踪?
字段类型(按优先级降序排列) | 是否跟踪? |
---|---|
Repeated 字段和 map | 否 |
消息和 Oneof 字段 | 是 |
如果 features.field_presence 设置为 IMPLICIT 的其他 singular 字段 | 否 |
所有其他字段 | 是 |