枚举行为
枚举在不同的语言库中行为不同。本主题涵盖了这些不同的行为,以及将 protobufs 迁移到所有语言都一致的状态的计划。如果您正在寻找有关如何常规使用枚举的信息,请参阅 proto2、proto3 和 editions 2023 语言指南主题中的相应部分。
定义
枚举有两种不同的类型(开放和封闭)。除了处理未知值的方式外,它们的行为完全相同。实际上,这意味着简单情况下的工作方式相同,但某些极端情况会产生有趣的影响。
为了解释,我们假设有以下 .proto
文件(我们现在故意不指定这是一个 syntax = "proto2"
、syntax = "proto3"
还是 edition = "2023"
文件)
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]
的线路格式(wire format)在解析后,重复字段将包含 [0, 1]
,而值 [2, 2]
最终将作为未知字段存储。重新序列化该消息后,线路格式将对应于 [0, 1, 2, 2]
。
类似地,值为封闭枚举的 map 会在值未知时将整个条目(键和值)放入未知字段中。
历史
在引入 syntax = "proto3"
之前,所有枚举都是封闭的。Proto3 和 editions 使用开放枚举,正是因为封闭枚举会导致意想不到的行为。如果需要,您可以使用 features.enum_type
将 editions 枚举显式设置为开放。
规范
以下内容规定了 protobuf 合规实现的行为。由于这个问题很微妙,许多实现都不合规。有关不同实现的行为方式的详细信息,请参见已知问题。
- 当一个
proto2
文件导入一个在proto2
文件中定义的枚举时,该枚举应被视为封闭的。 - 当一个
proto3
文件导入一个在proto3
文件中定义的枚举时,该枚举应被视为开放的。 - 当一个
proto3
文件导入一个在proto2
文件中定义的枚举时,protoc
编译器将产生一个错误。 - 当一个
proto2
文件导入一个在proto3
文件中定义的枚举时,该枚举应被视为开放的。
Editions 会遵循被导入文件中枚举的行为。Proto2 枚举始终被视为封闭的,proto3 枚举始终被视为开放的,而当从另一个 editions 文件导入时,它会使用其特性设置。
已知问题
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()
方法。对于已知集合之外的值(例如2
),getName
方法将返回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
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 将所有枚举都视为封闭的。