枚举行为

解释 Protocol Buffers 中枚举的当前工作方式与它们应该如何工作。

在不同的语言库中,枚举的行为有所不同。本主题涵盖了不同的行为以及将 protobuf 迁移到跨所有语言一致状态的计划。如果您正在寻找有关如何一般使用枚举的信息,请参阅 proto2proto3 语言指南主题中的相应部分。

定义

枚举有两种不同的类型(开放封闭)。除了处理未知值的方式外,它们的其余行为都相同。实际上,这意味着简单的用例工作方式相同,但某些极端情况具有有趣的含义。

为了解释,让我们假设我们有以下 .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 将生成方法 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

4.22.0 之后,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 将所有枚举视为封闭的。