枚举行为

解释了枚举在 Protocol Buffers 中当前的工作方式与应有的工作方式。

枚举在不同的语言库中的行为有所不同。本主题涵盖了不同的行为,以及将 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 将所有枚举都视为 封闭的