Enum の動作
Enumは、言語ライブラリによって動作が異なります。このトピックでは、異なる動作と、Protobufをすべての言語で一貫した状態に移行するための計画について説明します。Enumの一般的な使用方法については、proto2、proto3、およびeditions 2023の言語ガイドの対応するセクションを参照してください。
定義
Enumには、「オープン」と「クローズド」の2つの distinct なフレーバーがあります。これらは、不明な値の処理を除いて、同一に動作します。実際には、単純なケースは同じように機能しますが、いくつかのエッジケースには興味深い影響があります。
説明のために、次の.proto
ファイルがあると仮定しましょう(これがsyntax = "proto2"
、syntax = "proto3"
、またはedition = "2023"
のファイルであるかは意図的に指定していません)。
enum Enum {
A = 0;
B = 1;
}
message Msg {
optional Enum enum = 1;
}
「オープン」と「クローズド」の区別は、単一の質問に集約できます。
プログラムが、値
2
を含むフィールド1を含むバイナリデータを解析するとき、何が起こりますか?
- オープン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とeditionsは、「クローズド」Enumが引き起こす予期せぬ動作のため、意図的に「オープン」Enumを使用しています。必要に応じて、features.enum_type
を使用して、editions Enumを明示的にオープンに設定できます。
仕様
以下は、protobufに準拠した実装の動作を規定しています。これは微妙なため、多くの実装は準拠していません。既知の問題で、異なる実装の動作の詳細を参照してください。
proto2
ファイルがproto2
ファイルで定義されたEnumをインポートする場合、そのEnumはクローズドとして扱われるべきです。proto3
ファイルがproto3
ファイルで定義されたEnumをインポートする場合、そのEnumはオープンとして扱われるべきです。proto3
ファイルがproto2
ファイルで定義されたEnumをインポートする場合、protoc
コンパイラはエラーを生成します。proto2
ファイルがproto3
ファイルで定義されたEnumをインポートする場合、そのEnumはオープンとして扱われるべきです。
Editionsは、インポート元のファイルでEnumが持っていた動作を尊重します。Proto2 Enumは常にクローズドとして扱われ、Proto3 Enumは常にオープンとして扱われ、他のeditionsファイルからインポートする場合は機能設定を使用します。
既知の問題
C++
既知のC++リリースはすべて準拠していません。proto2
ファイルがproto3
ファイルで定義されたEnumをインポートする場合、C++はそのフィールドをクローズドEnumとして扱います。
Editionsでは、この動作は非推奨のフィールド機能features.(pb.cpp).legacy_closed_enum
で表されます。準拠した動作に移行するには2つのオプションがあります。
- フィールド機能を削除します。これは推奨されるアプローチですが、ランタイムの動作変更を引き起こす可能性があります。この機能がない場合、認識されない整数は、不明なフィールドセットに入れられる代わりに、Enum型にキャストされてフィールドに格納されます。
- Enumをクローズドに変更します。これは推奨されず、他の誰かがEnumを使用している場合、ランタイムの動作変更を引き起こす可能性があります。認識されない整数は、これらのフィールドの代わりに不明なフィールドセットに入ります。
C#
既知のC#リリースはすべて準拠していません。C#はすべてのEnumをオープンとして扱います。
Java
既知のJavaリリースはすべて準拠していません。proto2
ファイルがproto3
ファイルで定義されたEnumをインポートする場合、JavaはそのフィールドをクローズドEnumとして扱います。
Editionsでは、この動作は非推奨のフィールド機能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をクローズドとして扱います。