Enum の動作

Enumが現在Protocol Buffersでどのように機能しているか、そしてどのように機能すべきかを説明します。

Enumは言語ライブラリによって動作が異なります。このトピックでは、異なる動作と、すべての言語でProtobufsが一貫した状態になるように移行する計画について説明します。Enumの一般的な使用方法については、proto2proto3、およびエディション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を返しますが、getNameValue2を返します。

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

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をクローズドとして扱います。