枚举行为

解释枚举目前在 Protocol Buffers 中的工作方式与它们应有的工作方式之间的差异。

枚举在不同的语言库中行为不同。本主题涵盖了这些不同的行为,以及将 protobuf 迁移到在所有语言中保持一致状态的计划。如果您想了解如何常规地使用枚举,请参阅 proto2proto3editions 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] 的线路格式在解析后,重复字段将包含 [0, 1],而值 [2, 2] 将最终作为未知字段存储。在重新序列化消息后,线路格式将对应于 [0, 1, 2, 2]

类似地,值为封闭枚举的映射,在值未知时会将整个条目(键和值)放入未知字段中。

历史

在引入 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 将所有枚举视为封闭的。