列舉行為

解釋了列舉目前在 Protocol Buffers 中的工作方式與它們應有的工作方式之間的區別。

列舉在不同的語言庫中行為不同。本主題涵蓋了不同的行為以及將 protobuf 遷移到所有語言中保持一致狀態的計劃。如果您正在尋找有關如何通常使用列舉的資訊,請參閱 proto2proto32023 版語言指南主題中的相應部分。

定義

列舉有兩種不同的型別(開放封閉)。它們在處理未知值時行為相同,只是有所不同。實際上,這意味著簡單情況下的工作方式相同,但某些極端情況會產生有趣的含義。

為了解釋方便,我們假設我們有以下 .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 和版本使用開放列舉,正是因為封閉列舉會產生意想不到的行為。如果需要,您可以使用 features.enum_type 將版本列舉顯式設定為封閉。

規範

以下指定了 protobuf 一致實現的表現。由於這很微妙,許多實現都不一致。有關不同實現行為的詳細資訊,請參閱 已知問題

  • proto2 檔案匯入 proto2 檔案中定義的列舉時,該列舉應被視為封閉
  • proto3 檔案匯入 proto3 檔案中定義的列舉時,該列舉應被視為開放
  • proto3 檔案匯入 proto2 檔案中定義的列舉時,protoc 編譯器將產生錯誤。
  • proto2 檔案匯入 proto3 檔案中定義的列舉時,該列舉應被視為開放

版本尊重從匯入檔案中列舉所具有的任何行為。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()。方法 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 將該欄位視為封閉列舉。

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 將所有列舉都視為封閉