Enum の動作
Enum は、言語ライブラリによって動作が異なります。このトピックでは、異なる動作と、Protobuf をすべての言語で一貫した状態にするための計画について説明します。enum の一般的な使用方法については、proto2 および proto3 言語ガイドの対応するセクションを参照してください。
定義
Enum には、2 つの異なる種類(オープン と クローズド)があります。未知の値を処理する点を除けば、両者は同じように動作します。実際には、単純なケースは同じように動作しますが、一部の特殊なケースでは興味深い影響があります。
説明のため、以下の .proto
ファイルがあると仮定します(現時点では、これが syntax = "proto2"
ファイルであるか syntax = "proto3"
ファイルであるかは意図的に指定していません)。
enum Enum {
A = 0;
B = 1;
}
message Msg {
optional Enum enum = 1;
}
オープン と クローズド の区別は、1 つの質問に集約できます。
プログラムがフィールド 1 に値
2
を含むバイナリデータを解析した場合、どうなりますか?
- オープン enum は、値
2
を解析し、それを直接フィールドに格納します。アクセサーはフィールドが設定済みであると報告し、2
を表す値を返します。 - クローズド enum は、値
2
を解析し、それをメッセージの未知のフィールドセットに格納します。アクセサーはフィールドが未設定であると報告し、enum のデフォルト値を返します。
クローズド Enum の影響
クローズド enum の動作は、繰り返しフィールドを解析する際に予期せぬ結果をもたらします。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]
に対応します。
同様に、値に クローズド enum を持つマップは、値が未知の場合、エントリ全体(キーと値)を未知のフィールドに配置します。
履歴
syntax = "proto3"
の導入以前は、すべての enum は クローズド でした。Proto3 は、クローズド enum が引き起こす予期せぬ動作のために、特別に オープン enum を導入しました。
仕様
以下は、Protobuf の準拠実装の動作を規定しています。これは微妙な点が多く、多くの実装が準拠していません。既知の問題で、異なる実装の動作の詳細を確認してください。
proto2
ファイルがproto2
ファイルで定義された enum をインポートする場合、その enum はクローズドとして扱われるべきです。proto3
ファイルがproto3
ファイルで定義された enum をインポートする場合、その enum はオープンとして扱われるべきです。proto3
ファイルがproto2
ファイルで定義された enum をインポートする場合、protoc
コンパイラはエラーを生成します。proto2
ファイルがproto3
ファイルで定義された enum をインポートする場合、その enum はオープンとして扱われるべきです。
既知の問題
C++
既知の C++ リリースはすべて準拠していません。proto2
ファイルが proto3
ファイルで定義された enum をインポートする場合、C++ はそのフィールドをクローズド enum として扱います。エディションでは、この動作は非推奨のフィールド機能 features.(pb.cpp).legacy_closed_enum
で表現されます。準拠する動作に移行するには 2 つのオプションがあります。
- フィールド機能を削除します。これが推奨されるアプローチですが、実行時の動作が変更される可能性があります。この機能がない場合、認識されない整数は、未知のフィールドセットに入れられる代わりに、enum 型にキャストされてフィールドに格納されます。
- enum をクローズドに変更します。これは推奨されず、他の誰かが enum を使用している場合、実行時の動作に影響を与える可能性があります。認識されない整数は、それらのフィールドに入れられる代わりに、未知のフィールドセットに入ります。
C#
既知の C# リリースはすべて準拠していません。C# はすべての enum をオープンとして扱います。
Java
既知の Java リリースはすべて準拠していません。proto2
ファイルが proto3
ファイルで定義された enum をインポートする場合、Java はそのフィールドをクローズド enum として扱います。
エディションでは、この動作は非推奨のフィールド機能 features.(pb.java).legacy_closed_enum
で表現されます。準拠する動作に移行するには 2 つのオプションがあります。
- フィールド機能を削除します。これにより実行時の動作が変更される可能性があります。この機能がない場合、認識されない整数はフィールドに格納され、enum ゲッターからは
UNRECOGNIZED
値が返されます。以前は、これらの値は未知のフィールドセットに入れられていました。 - enum をクローズドに変更します。他の誰かが使用している場合、実行時の動作が変更される可能性があります。認識されない整数は、それらのフィールドに入れられる代わりに、未知のフィールドセットに入ります。
注: Java のオープン enum の処理には、驚くべきエッジケースがあります。以下の定義を考えます。
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
ファイルで定義された enum をインポートする場合、Kotlin はそのフィールドをクローズド enum として扱います。
Kotlin は Java 上に構築されており、その特異性をすべて共有しています。
Go
既知の Go リリースはすべて準拠していません。Go はすべての enum をオープンとして扱います。
JSPB
既知の JSPB リリースはすべて準拠していません。JSPB はすべての enum をオープンとして扱います。
PHP
PHP は準拠しています。
Python
Python はバージョン 4.22.0 (2023年第1四半期リリース) 以降で準拠しています。
サポートが終了した古いバージョンは準拠していません。proto2
ファイルが proto3
ファイルで定義された enum をインポートする場合、非準拠の Python バージョンではそのフィールドをクローズド enum として扱います。
Ruby
既知の Ruby リリースはすべて準拠していません。Ruby はすべての enum をオープンとして扱います。
Objective-C
Objective-C はバージョン 3.22.0 (2023年第1四半期リリース) 以降で準拠しています。
サポートが終了した古いバージョンは準拠していません。proto2
ファイルが proto3
ファイルで定義された enum をインポートする場合、非準拠の ObjC バージョンではそのフィールドをクローズド enum として扱います。
Swift
Swift は準拠しています。
Dart
Dart はすべての enum をクローズドとして扱います。