Enum の動作
Enumは言語ライブラリによって動作が異なります。このトピックでは、異なる動作と、すべての言語でProtobufsが一貫した状態になるように移行する計画について説明します。Enumの一般的な使用方法については、proto2、proto3、およびエディション2023の言語ガイドの対応するセクションを参照してください。
定義
Enumには、「オープン」と「クローズド」の2つの明確な種類があります。これらは、不明な値の処理を除いて、同一に動作します。実際には、単純なケースは同じように機能しますが、いくつかのエッジケースは興味深い意味を持ちます。
説明のために、以下の.protoファイルがあると仮定します(これがsyntax = "proto2"、syntax = "proto3"、またはedition = "2023"ファイルであるかどうかは、ここでは意図的に指定しません)。
enum Enum {
A = 0;
B = 1;
}
message Msg {
optional Enum enum = 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を使用します。必要に応じて、features.enum_typeを使用してエディションのEnumを明示的にオープンに設定できます。
仕様
以下は、Protobufの適合実装の動作を規定します。これは微妙なため、多くの実装は適合していません。異なる実装がどのように動作するかについては、既知の問題を参照してください。
proto2ファイルがproto2ファイルで定義されたEnumをインポートする場合、そのEnumはクローズドとして扱われるべきです。proto3ファイルがproto3ファイルで定義されたEnumをインポートする場合、そのEnumはオープンとして扱われるべきです。proto3ファイルがproto2ファイルで定義されたEnumをインポートする場合、protocコンパイラはエラーを生成します。proto2ファイルがproto3ファイルで定義されたEnumをインポートする場合、そのEnumはオープンとして扱われるべきです。
エディションは、インポート元のファイルでEnumが持っていた動作を尊重します。Proto2のEnumは常にクローズドとして扱われ、proto3の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をクローズドとして扱います。