Enum の動作

Protocol Buffers での 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 を返しますが、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 をクローズドとして扱います。