列舉行為

說明列舉目前在 Protocol Buffers 中的運作方式,以及它們應有的運作方式。

列舉在不同的語言函式庫中的行為有所不同。本主題涵蓋了不同的行為,以及將 protobufs 遷移到跨所有語言一致狀態的計畫。如果您正在尋找關於如何一般使用列舉的資訊,請參閱 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 表示。有兩個選項可以轉向符合規範的行為

  • 移除欄位特性。這可能會導致執行階段行為變更。如果沒有該特性,無法辨識的整數最終會儲存在欄位中,並且列舉取值器會傳回 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)。當傳遞 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 將所有列舉都視為封閉