枚举行为
枚举在不同的语言库中的行为有所不同。本主题涵盖了不同的行为,以及将 protobufs 迁移到在所有语言中保持一致状态的计划。如果您正在寻找有关如何通用地使用枚举的信息,请参阅 proto2 和 proto3 语言指南主题中的相应章节。
定义
枚举有两种不同的类型(开放和封闭)。除了在处理未知值时,它们的行为完全相同。实际上,这意味着简单的情况工作方式相同,但一些极端情况具有有趣的含义。
为了便于解释,让我们假设我们有以下 .proto
文件(我们目前故意不指定这是否是 syntax = "proto2"
或 syntax = "proto3"
文件)
enum Enum {
A = 0;
B = 1;
}
message Msg {
optional Enum enum = 1;
}
开放和封闭之间的区别可以用一个问题来概括
当程序解析包含字段 1,值为
2
的二进制数据时,会发生什么?
- 开放 枚举将解析值
2
并将其直接存储在字段中。访问器将报告该字段已设置,并将返回表示2
的内容。 - 封闭 枚举将解析值
2
并将其存储在消息的未知字段集中。访问器将报告该字段未设置,并将返回枚举的默认值。
封闭枚举的含义
当解析重复字段时,封闭 枚举的行为会产生意想不到的后果。当解析 repeated Enum
字段时,所有未知值都将放置在 未知字段 集中。当序列化时,这些未知值将再次写入,但不会在其在列表中的原始位置。例如,给定 .proto
文件
enum Enum {
A = 0;
B = 1;
}
message Msg {
repeated Enum r = 1;
}
字段 1 的线格式包含值 [0, 2, 1, 2]
将被解析,以便重复字段包含 [0, 1]
,值 [2, 2]
将最终存储为未知字段。重新序列化消息后,线格式将对应于 [0, 1, 2, 2]
。
类似地,当值为未知时,以封闭 枚举作为值的映射会将整个条目(键和值)放置在未知字段中。
历史
在引入 syntax = "proto3"
之前,所有枚举都是封闭的。Proto3 引入开放 枚举,专门为了解决封闭 枚举引起的意外行为。
规范
以下指定了 protobuf 的一致性实现的行为。由于这很微妙,许多实现都不符合一致性。有关不同实现如何表现的详细信息,请参阅 已知问题。
- 当
proto2
文件导入在proto2
文件中定义的枚举时,该枚举应被视为 封闭的。 - 当
proto3
文件导入在proto3
文件中定义的枚举时,该枚举应被视为 开放的。 - 当
proto3
文件导入在proto2
文件中定义的枚举时,protoc
编译器将产生错误。 - 当
proto2
文件导入在proto3
文件中定义的枚举时,该枚举应被视为 开放的。
已知问题
C++
所有已知的 C++ 版本都不符合一致性。当 proto2
文件导入在 proto3
文件中定义的枚举时,C++ 将该字段视为 封闭枚举。在版本中,此行为由已弃用的字段功能 features.(pb.cpp).legacy_closed_enum
表示。 有两个选项可以转向一致性行为
- 删除字段功能。这是推荐的方法,但可能会导致运行时行为更改。如果没有该功能,无法识别的整数最终将存储在强制转换为枚举类型的字段中,而不是放入未知字段集中。
- 将枚举更改为封闭的。不建议这样做,如果其他人正在使用该枚举,可能会导致运行时行为。无法识别的整数将最终放入未知字段集中,而不是这些字段中。
C#
所有已知的 C# 版本都不符合一致性。C# 将所有枚举都视为 开放的。
Java
所有已知的 Java 版本都不符合一致性。当 proto2
文件导入在 proto3
文件中定义的枚举时,Java 将该字段视为 封闭枚举。
在版本中,此行为由已弃用的字段功能 features.(pb.java).legacy_closed_enum
) 表示。 有两个选项可以转向一致性行为
- 删除字段功能。这可能会导致运行时行为更改。如果没有该功能,无法识别的整数将最终存储在字段中,并且枚举 getter 将返回
UNRECOGNIZED
值。 之前,这些值将被放入未知字段集中。 - 将枚举更改为封闭的。如果其他人正在使用它,他们可能会看到运行时行为更改。无法识别的整数将最终放入未知字段集中,而不是这些字段中。
注意: Java 对 开放 枚举的处理具有令人惊讶的边缘情况。 假设有以下定义
syntax = "proto3"; enum Enum { A = 0; B = 1; } message Msg { repeated Enum name = 1; }
Java 将生成方法
getName()
和getNameValue()
。 方法getName
对于已知集合之外的值(例如2
)将返回Enum.UNRECOGNIZED
,而getNameValue
将返回2
。类似地,Java 将生成方法
Builder setName(Enum value)
和Builder setNameValue(int value)
。 当传递Enum.UNRECOGNIZED
时,方法setName
将抛出异常,而setNameValue
将接受2
。
Kotlin
所有已知的 Kotlin 版本都不符合一致性。当 proto2
文件导入在 proto3
文件中定义的枚举时,Kotlin 将该字段视为 封闭枚举。
Kotlin 构建于 Java 之上,并共享其所有怪异之处。
Go
所有已知的 Go 版本都不符合一致性。Go 将所有枚举都视为 开放的。
JSPB
所有已知的 JSPB 版本都不符合一致性。JSPB 将所有枚举都视为 开放的。
PHP
PHP 符合一致性。
Python
在 4.22.0 版本(于 2023-02-16 左右发布)之后,Python 符合一致性。
在 4.21.x 版本中,默认情况下 Python 符合一致性,但设置 PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
将导致其不符合一致性。
在 4.21.0 版本之前,Python 不符合一致性。
当 proto2
文件导入在 proto3
文件中定义的枚举时,不符合一致性的 Python 版本将该字段视为 封闭枚举。
Ruby
所有已知的 Ruby 版本都不符合一致性。Ruby 将所有枚举都视为 开放的。
Objective-C
在 22.0 版本之后,Objective-C 符合一致性。
在 22.0 版本之前,Objective-C 不符合一致性。当 proto2
文件导入在 proto3
文件中定义的枚举时,它会将该字段视为 封闭枚举。
Swift
Swift 符合一致性。
Dart
Dart 将所有枚举都视为 封闭的。