枚举行为
枚举在不同的语言库中行为不同。本主题涵盖了不同的行为,以及使 protobuf 在所有语言中保持一致的计划。如果您正在寻找有关如何一般性地使用枚举的信息,请参阅 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 字段时会产生意外的后果。当解析 repeated Enum
字段时,所有未知值将被放入未知字段集中。当它被序列化时,这些未知值将再次被写入,但不会在列表中它们原来的位置。例如,给定 .proto 文件:
enum Enum {
A = 0;
B = 1;
}
message Msg {
repeated Enum r = 1;
}
包含字段 1 的值 [0, 2, 1, 2]
的线格式将被解析,使得 repeated 字段包含 [0, 1]
,而值 [2, 2]
将最终存储为未知字段。重新序列化消息后,线格式将对应于 [0, 1, 2, 2]
。
类似地,值使用封闭枚举的 map 会在值未知时将整个条目(键和值)放入未知字段中。
历史
在引入 syntax = "proto3"
之前,所有枚举都是封闭的。Proto3 特别引入了开放枚举,正是因为封闭枚举导致的意外行为。
规范
以下内容规定了 protobuf 合规实现的预期行为。由于这一点很微妙,许多实现并不合规。有关不同实现行为的详细信息,请参阅已知问题。
- 当
proto2
文件导入proto2
文件中定义的枚举时,该枚举应被视为 封闭的。 - 当
proto3
文件导入proto3
文件中定义的枚举时,该枚举应被视为 开放的。 - 当
proto3
文件导入proto2
文件中定义的枚举时,protoc 编译器将产生错误。 - 当
proto2
文件导入proto3
文件中定义的枚举时,该枚举应被视为 开放的。
已知问题
C++
所有已知的 C++ 版本都不合规。当 proto2
文件导入 proto3
文件中定义的枚举时,C++ 会将该字段视为 封闭枚举。在 editions 下,此行为由已弃用的字段特性 features.(pb.cpp).legacy_closed_enum
表示。有两种方法可以转换为合规行为:
- 移除该字段特性。这是推荐的方法,但可能会导致运行时行为变化。如果没有该特性,无法识别的整数将被存储在强制转换为枚举类型的字段中,而不是放入未知字段集。
- 将枚举更改为封闭式。不建议这样做,如果其他人正在使用该枚举,可能会导致运行时行为。无法识别的整数将最终进入未知字段集,而不是这些字段中。
C#
所有已知的 C# 版本都不合规。C# 将所有枚举视为 开放的。
Java
所有已知的 Java 版本都不合规。当 proto2
文件导入 proto3
文件中定义的枚举时,Java 会将该字段视为 封闭枚举。
在 editions 下,此行为由已弃用的字段特性 features.(pb.java).legacy_closed_enum
表示。有两种方法可以转换为合规行为:
- 移除该字段特性。这可能会导致运行时行为变化。如果没有该特性,无法识别的整数将存储在字段中,并且枚举 getter 将返回
UNRECOGNIZED
值。之前,这些值会被放入未知字段集。 - 将枚举更改为封闭式。如果其他人正在使用它,他们可能会看到运行时行为变化。无法识别的整数将最终进入未知字段集,而不是这些字段中。
注意:Java 对开放枚举的处理有一些令人意外的边界情况。给定以下定义:
syntax = "proto3"; enum Enum { A = 0; B = 1; } message Msg { repeated Enum name = 1; }
Java 将生成方法
Enum getName()
和int getNameValue()
。getName
方法对于已知集合之外的值(例如2
)将返回Enum.UNRECOGNIZED
,而getNameValue
将返回2
。类似地,Java 将生成方法
Builder setName(Enum value)
和Builder setNameValue(int value)
。setName
方法在传递Enum.UNRECOGNIZED
时会抛出异常,而setNameValue
将接受2
。
Kotlin
所有已知的 Kotlin 版本都不合规。当 proto2
文件导入 proto3
文件中定义的枚举时,Kotlin 会将该字段视为 封闭枚举。
Kotlin 基于 Java 构建,并继承了其所有的怪异之处。
Go
所有已知的 Go 版本都不合规。Go 将所有枚举视为 开放的。
JSPB
所有已知的 JSPB 版本都不合规。JSPB 将所有枚举视为 开放的。
PHP
PHP 是合规的。
Python
Python 在 4.22.0 及更高版本(2023 年第一季度发布)中是合规的。
不再支持的旧版本不合规。当 proto2 文件导入 proto3 文件中定义的枚举时,不合规的 Python 版本会将该字段视为 封闭枚举。
Ruby
所有已知的 Ruby 版本都不合规。Ruby 将所有枚举视为 开放的。
Objective-C
Objective-C 在 3.22.0 及更高版本(2023 年第一季度发布)中是合规的。
不再支持的旧版本不合规。当 proto2 文件导入 proto3 文件中定义的枚举时,不合规的 ObjC 版本会将该字段视为 封闭枚举。
Swift
Swift 是合规的。
Dart
Dart 将所有枚举视为 封闭的。