凌云的博客

行胜于言

Proto 枚举行为[译]

分类:proto| 发布时间:2025-01-16 08:37:00

原文:Enum Behavior

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

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

定义

枚举有两种不同的风格 开放(Open)和封闭(Closed)。 除了处理未知值的方式之外,它们的行为完全相同。 实际上,这意味着简单的用例表现相同,但一些极端情况会有有趣的影响。

为了解释,让我们假设我们有以下 .proto 文件(我们目前故意不指定这是 syntax = "proto2" 还是 syntax = "proto3" 文件):

enum Enum {
  A = 0;
  B = 1;
}

message Msg {
  optional Enum enum = 1;
}

开放和封闭之间的区别可以用一个问题来概括:

当程序解析包含字段1和值2的二进制数据时会发生什么?

  • Open enum 将解析值2并将其直接存储在字段中。访问器将报告该字段已设置,并返回表示2的内容。
  • Closed enum 将解析值 2 并将其存储在消息的未知字段集中。访问器将报告该字段未设置,并返回枚举的默认值。

Closed 枚举的含义

当解析重复字段时,封闭枚举的行为会产生意想不到的后果。 当解析 重复的 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]。

类似地,对于其值为封闭枚举的映射,每当值为未知时,整个条目(键和值)将被放置在未知字段中。

历史

在引入 syntax = "proto3" 之前,所有枚举都是 封闭(closed) 的。 Proto3 引入 开放(open) 枚举正是因为 封闭 枚举会导致意外的行为。

规格

以下内容指定了符合 protobuf 规范的实现的行为。 由于这一点比较微妙,许多实现都不符合规范。 有关不同实现的行为,请参阅 已知问题

  • 当一个 proto2 文件导入在另一个 proto2 文件中定义的枚举时,该枚举应被视为 封闭(closed)
  • 当一个 proto3 文件导入在另一个 proto3 文件中定义的枚举时,该枚举应被视为 开放(open)
  • 当一个 proto3 文件导入在 proto2 文件中定义的枚举时, protoc 编译器将产生错误。
  • 当一个 proto2 文件导入在 proto3 文件中定义的枚举时,该枚举应被视为 开放(open)

已知问题

C++

所有已知的 C++ 版本都不符合规范。 当一个 proto2 文件导入在 proto3 文件中定义的枚举时,C++ 将该字段视为 封闭(closed) 枚举。 在 editions 中,此行为由弃用的字段特性 features.(pb.cpp).legacy_closed_enum 表示。 有两种方法可以迁移到符合规范的行为:

  • 移除该字段特性。 这是推荐的方法,但可能会导致运行时行为发生变化。 没有该特性,无法识别的整数将最终存储在转换为枚举类型的字段中,而不是放入未知字段集中。
  • 将枚举更改为封闭。 不鼓励这样做,如果 其他人 正在使用该枚举,可能会导致运行时行为。 无法识别的整数将最终存储在未知字段集中,而不是在那些字段中。

C#

所有已知的 C# 版本都不符合规范。 C# 将所有枚举视为 开放(open)。

Java

所有已知的 Java 版本都不符合规范。 当一个 proto2 文件导入在 proto3 文件中定义的枚举时,Java 将该字段视为 封闭(closed) 枚举。

在 editions,这种行为由一个名为 features.(pb.java).legacy_closed_enum 的弃用字段特性控制。 为了实现符合规范的行为,有两个可选方案:

  • 移除该字段特性: 这可能会导致运行时行为发生变化。 在没有该特性的情况下,无法识别的整数将最终存储在该字段中,并且枚举 getter 将返回 UNRECOGNIZED 值。 之前,这些值将被放入未知字段集中。
  • 将枚举更改为封闭: 如果 其他人 正在使用它,可能会导致运行时行为发生变化。 无法识别的整数将最终存储在未知字段集中,而不是在那些字段中。

注意: Java 处理 开放(open) 枚举的方式存在令人惊讶的边界情况。 给定以下定义:

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 将该字段视为 封闭(closed) 枚举。

Kotlin 构建于 Java 之上,并共享其所有奇异之处。

Go

所有已知的 Go 版本都不符合规范。 Go 将所有枚举视为 开放(open)

JSPB

所有已知的 JSPB 版本都不符合规范。 JSPB 将所有枚举视为 开放(open)

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 版本将该字段视为 封闭(closed) 枚举。

Ruby

所有已知的 Ruby 版本都不符合规范。 Ruby 将所有枚举视为 开放(open)

Objective-C

在 22.0 之后, Objective-C 是符合规范的。

在 22.0 之前,Objective-C 是不符合规范的。 当一个 proto2 文件导入了一个在 proto3 文件中定义的枚举时,它会将该字段视为一个 闭合 的枚举。

Swift

Swift 是符合规范的。

Dart

Dart 将所有 enums 视为 闭合 枚举。