Enum の動作

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

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