Enum の挙動

Protocol Buffers における Enum の現在の動作と、本来あるべき動作について説明します。

Enum の動作は、言語ライブラリによって異なります。このトピックでは、さまざまな動作と、protobuf をすべての言語で一貫性のある状態に移行させる計画について説明します。Enum の一般的な使用方法に関する情報をお探しの場合は、proto2 および proto3 言語ガイドの対応するセクションを参照してください。

定義

Enum には、2 つの異なる種類 (openclosed) があります。未知の値の処理を除いて、動作は同一です。実際には、単純なケースでは同じように動作しますが、いくつかの特殊なケースでは興味深い意味合いがあります。

説明のために、次の .proto ファイルがあると仮定しましょう (これが syntax = "proto2" ファイルまたは syntax = "proto3" ファイルであるかは、ここでは意図的に指定しません)。

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

message Msg {
  optional Enum enum = 1;
}

openclosed の区別は、単一の質問に集約できます。

プログラムが値 2 を持つフィールド 1 を含むバイナリデータを解析するとどうなりますか?

  • Open Enum は、値 2 を解析し、フィールドに直接格納します。アクセサは、フィールドが設定済みであると報告し、2 を表す何かを返します。
  • Closed Enum は、値 2 を解析し、メッセージの不明なフィールドセットに格納します。アクセサは、フィールドが未設定であると報告し、Enum のデフォルト値を返します。

Closed Enum の影響

closed Enum の動作は、repeated フィールドを解析するときに予期しない結果をもたらします。repeated 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] に対応します。

同様に、値に closed Enum を持つマップは、値が不明な場合は常にエントリ全体 (キーと値) を不明なフィールドに配置します。

履歴

syntax = "proto3" の導入以前は、すべての Enum が closed でした。Proto3 は、closed Enum が引き起こす予期しない動作のために、特に open Enum を導入しました。

仕様

以下は、protobuf の準拠実装の動作を指定します。これは微妙なため、多くの実装が準拠していません。さまざまな実装の動作の詳細については、既知の問題 を参照してください。

  • proto2 ファイルが proto2 ファイルで定義された Enum をインポートする場合、その Enum は closed として扱われる必要があります。
  • proto3 ファイルが proto3 ファイルで定義された Enum をインポートする場合、その Enum は open として扱われる必要があります。
  • proto3 ファイルが proto2 ファイルで定義された Enum をインポートする場合、protoc コンパイラはエラーを生成します。
  • proto2 ファイルが proto3 ファイルで定義された Enum をインポートする場合、その Enum は open として扱われる必要があります。

既知の問題

C++

既知のすべての C++ リリースは準拠していません。proto2 ファイルが proto3 ファイルで定義された Enum をインポートする場合、C++ はそのフィールドを closed Enum として扱います。エディションでは、この動作は非推奨のフィールド機能 features.(pb.cpp).legacy_closed_enum で表されます。準拠した動作に移行するためのオプションは 2 つあります。

  • フィールド機能を削除します。これは推奨されるアプローチですが、ランタイムの動作変更を引き起こす可能性があります。この機能がない場合、認識されない整数は、不明なフィールドセットに入れられる代わりに、Enum 型にキャストされてフィールドに格納されます。
  • Enum を closed に変更します。これは推奨されませんが、他の誰かが Enum を使用している場合は、ランタイムの動作を引き起こす可能性があります。認識されない整数は、これらのフィールドの代わりに不明なフィールドセットに入れられます。

C#

既知のすべての C# リリースは準拠していません。C# はすべての Enum を open として扱います。

Java

既知のすべての Java リリースは準拠していません。proto2 ファイルが proto3 ファイルで定義された Enum をインポートする場合、Java はそのフィールドを closed Enum として扱います。

エディションでは、この動作は非推奨のフィールド機能 features.(pb.java).legacy_closed_enum) で表されます。準拠した動作に移行するためのオプションは 2 つあります。

  • フィールド機能を削除します。これにより、ランタイムの動作変更が発生する可能性があります。この機能がない場合、認識されない整数はフィールドに格納され、Enum ゲッターによって UNRECOGNIZED 値が返されます。以前は、これらの値は不明なフィールドセットに入れられていました。
  • Enum を closed に変更します。他の誰かがそれを使用している場合、ランタイムの動作変更が発生する可能性があります。認識されない整数は、これらのフィールドの代わりに不明なフィールドセットに入れられます。

注: Java の open Enum の処理には、驚くべきエッジケースがあります。次の定義が与えられた場合

syntax = "proto3";

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

message Msg {
  repeated Enum name = 1;
}

Java は、メソッド Enum getName() および int getNameValue() を生成します。メソッド getName は、既知のセット外の値 (2 など) に対して Enum.UNRECOGNIZED を返しますが、getNameValue2 を返します。

同様に、Java はメソッド Builder setName(Enum value) および Builder setNameValue(int value) を生成します。メソッド setName は、Enum.UNRECOGNIZED が渡されると例外をスローしますが、setNameValue2 を受け入れます。

Kotlin

既知のすべての Kotlin リリースは準拠していません。proto2 ファイルが proto3 ファイルで定義された Enum をインポートする場合、Kotlin はそのフィールドを closed Enum として扱います。

Kotlin は Java 上に構築されており、Java のすべての奇妙さを共有しています。

Go

既知のすべての Go リリースは準拠していません。Go はすべての Enum を open として扱います。

JSPB

既知のすべての JSPB リリースは準拠していません。JSPB はすべての Enum を 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 ファイルで定義された Enum をインポートする場合、非準拠の Python バージョンは、そのフィールドを closed Enum として扱います。

Ruby

既知のすべての Ruby リリースは準拠していません。Ruby はすべての Enum を open として扱います。

Objective-C

22.0 以降、Objective-C は準拠しています。

22.0 より前では、Objective-C は準拠していませんでした。proto2 ファイルが proto3 ファイルで定義された Enum をインポートした場合、そのフィールドを closed Enum として扱っていました。

Swift

Swift は準拠しています。

Dart

Dart はすべての Enum を closed として扱います。