アプリケーションノート:フィールドプレゼンス

protobufフィールドの様々なプレゼンストラッキングの規律について説明します。また、基本的な型を持つ単数proto3フィールドに対する明示的なプレゼンストラッキングの振る舞いについても説明します。

背景

フィールドプレゼンスとは、protobufフィールドが値を持っているかどうかの概念です。protobufのプレゼンスには2つの異なる形があります。生成されたメッセージAPIがフィールド値(のみ)を格納する暗黙的なプレゼンスと、APIがフィールドが設定されたかどうかを格納する明示的なプレゼンスです。

歴史的に、proto2は主に明示的なプレゼンスに従ってきましたが、proto3は暗黙的なプレゼンスのセマンティクスのみを公開しています。optionalラベルで定義された基本的な型(数値、文字列、バイト、enum)の単数proto3フィールドは、proto2のように明示的なプレゼンスを持ちます(この機能はリリース3.15からデフォルトで有効になっています)。

プレゼンスの規律

プレゼンスの規律は、API表現シリアライズされた表現の間を翻訳するためのセマンティクスを定義します。暗黙的なプレゼンスの規律は、(デ)シリアライズ時に決定を下すためにフィールド値自体に依存しますが、明示的なプレゼンスの規律は代わりに明示的なトラッキング状態に依存します。

タグ値ストリーム (ワイヤーフォーマット) シリアライゼーションにおけるプレゼンス

ワイヤーフォーマットは、タグ付けされた自己区切り値のストリームです。定義上、ワイヤーフォーマットはpresent値のシーケンスを表します。言い換えれば、シリアライゼーション内で見つかったすべての値はpresentフィールドを表します。さらに、シリアライゼーションにはnot-present値に関する情報は含まれていません。

protoメッセージ用に生成されたAPIには、APIタイプと定義上presentな(タグ、値)ペアのストリームの間を変換する(デ)シリアライゼーション定義が含まれています。この変換は、メッセージ定義の変更を跨いで前方および後方互換性を持つように設計されています。ただし、この互換性により、ワイヤーフォーマットされたメッセージをデシリアライズする際にいくつかの(おそらく驚くべき)考慮事項が導入されます。

  • シリアライズ時、暗黙的なプレゼンスを持つフィールドは、デフォルト値が含まれている場合はシリアライズされません。
    • 数値型の場合、デフォルトは0です。
    • enumの場合、デフォルトはゼロ値の列挙子です。
    • 文字列、バイト、および繰り返しフィールドの場合、デフォルトはゼロ長のValueです。
  • 「空の」長さ区切り値(空文字列など)は、シリアライズされた値で有効に表現できます。フィールドは、ワイヤーフォーマットに表示されるという意味で「present」です。ただし、生成されたAPIがプレゼンスを追跡しない場合、これらの値は再シリアライズされない可能性があります。つまり、空のフィールドはシリアライゼーションのラウンドトリップ後に「not present」になる可能性があります。
  • デシリアライズ時、重複するフィールド値は、フィールド定義に応じて異なる方法で処理される場合があります。
    • 重複するrepeatedフィールドは、通常、フィールドのAPI表現に追加されます。(packed繰り返しフィールドをシリアライズすると、タグストリームに1つの長さ区切り値のみが生成されることに注意してください。)
    • 重複するoptionalフィールド値は、「後勝ち」のルールに従います。
  • oneofフィールドは、一度に1つのフィールドのみが設定されるというAPIレベルの不変条件を公開します。ただし、ワイヤーフォーマットには、概念的にoneofに属する複数の(タグ、値)ペアが含まれる場合があります。optionalフィールドと同様に、生成されたAPIは「後勝ち」ルールに従います。
  • 範囲外の値は、生成されたproto2 APIのenumフィールドに対しては返されません。ただし、範囲外の値は、ワイヤーフォーマットタグが認識されたとしても、APIの不明なフィールドとして格納される場合があります。

名前付きフィールドマッピング形式におけるプレゼンス

Protobufは、人間が読めるテキスト形式で表現できます。2つの注目すべき形式は、TextFormat(生成されたメッセージDebugStringメソッドによって生成される出力形式)とJSONです。

これらの形式には独自の正確性要件があり、一般的にタグ値ストリーム形式よりも厳密です。ただし、TextFormatはワイヤーフォーマットのセマンティクスをより密接に模倣しており、特定の場合には同様のセマンティクスを提供します(たとえば、繰り返し名前と値のマッピングを繰り返しフィールドに追加するなど)。特に、ワイヤーフォーマットと同様に、TextFormatにはpresentフィールドのみが含まれます。

ただし、JSONははるかに厳密な形式であり、ワイヤーフォーマットまたはTextFormatの一部のセマンティクスを有効に表現することはできません。

  • 特に、JSON要素は意味論的に順序付けられておらず、各メンバーは一意の名前を持っている必要があります。これは、繰り返しフィールドに関するTextFormatルールとは異なります。
  • JSONには、他の形式の暗黙的なプレゼンス規律とは異なり、「not present」なフィールドを含めることができます。
    • JSONはnull値を定義しており、これは定義されているがnot-presentなフィールドを表すために使用できます。
    • 繰り返しフィールド値は、デフォルト(空のリスト)と等しい場合でも、フォーマットされた出力に含めることができます。
  • JSON要素は順序付けられていないため、「後勝ち」ルールを明確に解釈する方法はありません。
    • ほとんどの場合、これは問題ありません。JSON要素は一意の名前を持っている必要があります。繰り返しフィールド値は有効なJSONではないため、TextFormatのように解決する必要はありません。
    • ただし、これはoneofフィールドを明確に解釈できない可能性があることを意味します。複数のケースが存在する場合、それらは順序付けられていません。

理論的には、JSONはセマンティクスを保持する方法でプレゼンスを表現できます。しかし実際には、特にJSONがprotobufを使用していないクライアントと相互運用する手段として選択された場合、プレゼンスの正確性は実装の選択によって異なる可能性があります。

Proto2 APIにおけるプレゼンス

この表は、proto2 APIのフィールドでプレゼンスが追跡されるかどうかを概説しています(生成されたAPIと動的リフレクションの両方)。

フィールド型明示的なプレゼンス
単数数値 (整数または浮動小数点)✔️
単数enum✔️
単数文字列またはバイト✔️
単数メッセージ✔️
繰り返し
Oneofs✔️
マップ

単数フィールド(すべての型)は、生成されたAPIで明示的にプレゼンスを追跡します。生成されたメッセージインターフェースには、フィールドのプレゼンスをクエリするメソッドが含まれています。たとえば、フィールドfooには、対応するhas_fooメソッドがあります。(特定の名前は、フィールドアクセサーと同じ言語固有の命名規則に従います。)これらのメソッドは、protobuf実装内では「hazzers」と呼ばれることもあります。

単数フィールドと同様に、oneofフィールドは、メンバーのいずれか(もしあれば)が値を含んでいるかを明示的に追跡します。たとえば、次のoneofの例を考えてみましょう。

oneof foo {
  int32 a = 1;
  float b = 2;
}

ターゲット言語によっては、生成されたAPIには通常、いくつかのメソッドが含まれます。

  • oneofのhazzer:has_foo
  • oneof caseメソッド:foo
  • メンバーのHazzers:has_ahas_b
  • メンバーのゲッター:ab

繰り返しフィールドとマップはプレゼンスを追跡しません。の繰り返しフィールドとnot-presentの繰り返しフィールドの間に区別はありません。

Proto3 APIにおけるプレゼンス

この表は、proto3 APIのフィールドでプレゼンスが追跡されるかどうかを概説しています(生成されたAPIと動的リフレクションの両方)。

フィールド型optional明示的なプレゼンス
単数数値 (整数または浮動小数点)いいえ
単数数値 (整数または浮動小数点)はい✔️
単数enumいいえ
単数enumはい✔️
単数文字列またはバイトいいえ
単数文字列またはバイトはい✔️
単数メッセージいいえ✔️
単数メッセージはい✔️
繰り返しN/A
OneofsN/A✔️
マップN/A

proto2 APIと同様に、proto3は繰り返しフィールドのプレゼンスを明示的に追跡しません。optionalラベルがない場合、proto3 APIは基本的な型(数値、文字列、バイト、およびenum)のプレゼンスも追跡しません。Oneofフィールドはプレゼンスを積極的に公開しますが、proto2 APIのように同じhazzerメソッドのセットが生成されない場合があります。

optionalラベルなしでプレゼンスを追跡しないこのデフォルトの動作は、proto2の動作とは異なります。エディション2023で、明示的なプレゼンスをデフォルトとして再導入しました。特に理由がない限り、proto3でoptionalフィールドを使用することをお勧めします。

暗黙的なプレゼンスの規律では、デフォルト値はシリアライゼーションの目的で「not present」と同義です。フィールドを概念的に「クリア」するために(シリアライズされないように)、APIユーザーはそれをデフォルト値に設定します。

暗黙的なプレゼンス下でのenum型のフィールドのデフォルト値は、対応する0値の列挙子です。proto3構文規則では、すべてのenum型は0にマッピングされる列挙子の値を持つ必要があります。慣例により、これはUNKNOWNまたは同様の名前の列挙子です。ゼロ値が概念的にアプリケーションの有効な値のドメイン外にある場合、この動作は明示的なプレゼンスと同等と考えることができます。

意味論的な違い

暗黙的なプレゼンスシリアライゼーションの規律は、デフォルト値が設定された場合、明示的なプレゼンス追跡の規律とは目に見える違いが生じます。数値、enum、または文字列型を持つ単数フィールドの場合

  • 暗黙的なプレゼンスの規律
    • デフォルト値はシリアライズされません。
    • デフォルト値はマージ元にされません
    • フィールドを「クリア」するには、デフォルト値に設定します。
    • デフォルト値は次の意味を持つ可能性があります。
      • フィールドは、アプリケーション固有の値のドメインで有効なデフォルト値に明示的に設定されました。
      • フィールドは、デフォルトを設定することにより概念的に「クリア」されました。または
      • フィールドは一度も設定されませんでした。
    • has_メソッドは生成されません(ただし、このリストの後の注を参照してください)
  • 明示的なプレゼンスの規律
    • 明示的に設定された値は、デフォルト値を含め、常にシリアライズされます。
    • 未設定のフィールドはマージ元にされることはありません。
    • 明示的に設定されたフィールド(デフォルト値を含む)はマージ元にされます
    • 生成されたhas_fooメソッドは、フィールドfooが設定されているかどうか(クリアされていないかどうか)を示します。
    • 生成されたclear_fooメソッドは、値をクリア(つまり、未設定)するために使用する必要があります。

マージに関する考慮事項

暗黙的なプレゼンスルールでは、ターゲットフィールドがデフォルト値からマージ元になることは事実上不可能です(protobufのAPIマージ関数を使用)。これは、デフォルト値が暗黙的なプレゼンスシリアライゼーションの規律と同様にスキップされるためです。マージは、更新(マージ元)メッセージからのスキップされていない値を使用して、ターゲット(マージ先)メッセージのみを更新します。

マージ動作の違いは、部分的な「パッチ」更新に依存するプロトコルにさらに影響を与えます。フィールドプレゼンスが追跡されない場合、デフォルト値以外の値のみがマージ元になるため、更新パッチだけではデフォルト値の更新を表すことができません。

この場合、デフォルト値を設定するための更新には、FieldMaskなどの外部メカニズムが必要です。ただし、プレゼンスが追跡される場合、明示的に設定されたすべての値(デフォルト値を含む)がターゲットにマージされます。

変更互換性に関する考慮事項

明示的なプレゼンス暗黙的なプレゼンスの間でフィールドを変更することは、ワイヤーフォーマットでシリアライズされた値に対してバイナリ互換性のある変更です。ただし、メッセージのシリアライズされた表現は、シリアライゼーションに使用されたメッセージ定義のバージョンによって異なる場合があります。具体的には、「送信者」がフィールドを明示的にデフォルト値に設定した場合

  • 暗黙的なプレゼンス規律に従うシリアライズされた値には、明示的に設定された場合でも、デフォルト値は含まれていません。
  • 明示的なプレゼンス規律に従うシリアライズされた値には、デフォルト値が含まれている場合でも、すべての「present」フィールドが含まれています。

この変更は、アプリケーションのセマンティクスによっては安全な場合とそうでない場合があります。たとえば、メッセージ定義のバージョンが異なる2つのクライアントを考えてみましょう。

クライアントAは、フィールドfooに対して明示的なプレゼンスシリアライゼーションの規律に従うメッセージのこの定義を使用します。

syntax = "proto3";
message Msg {
  optional int32 foo = 1;
}

クライアントBは、同じメッセージの定義を使用しますが、プレゼンスなしの規律に従います。

syntax = "proto3";
message Msg {
  int32 foo = 1;
}

ここで、クライアントAが、クライアントが「同じ」メッセージをデシリアライズおよび再シリアライズすることで繰り返し交換する際に、fooのプレゼンスを観察するシナリオを考えてみましょう。

// Client A:
Msg m_a;
m_a.set_foo(1);                  // non-default value
assert(m_a.has_foo());           // OK
Send(m_a.SerializeAsString());   // to client B

// Client B:
Msg m_b;
m_b.ParseFromString(Receive());  // from client A
assert(m_b.foo() == 1);          // OK
Send(m_b.SerializeAsString());   // to client A

// Client A:
m_a.ParseFromString(Receive());  // from client B
assert(m_a.foo() == 1);          // OK
assert(m_a.has_foo());           // OK
m_a.set_foo(0);                  // default value
Send(m_a.SerializeAsString());   // to client B

// Client B:
Msg m_b;
m_b.ParseFromString(Receive());  // from client A
assert(m_b.foo() == 0);          // OK
Send(m_b.SerializeAsString());   // to client A

// Client A:
m_a.ParseFromString(Receive());  // from client B
assert(m_a.foo() == 0);          // OK
assert(m_a.has_foo());           // FAIL

クライアントAがfoo明示的なプレゼンスに依存している場合、クライアントBを介した「ラウンドトリップ」は、クライアントAの視点から見ると損失があります。例では、これは安全な変更ではありません。クライアントAは(assertによって)フィールドが存在することを要求します。APIを介した変更がなくても、その要件は値とピアに依存するケースで失敗します。

Proto3で明示的なプレゼンスを有効にする方法

これらは、proto3のフィールド追跡サポートを使用するための一般的な手順です。

  1. .protoファイルにoptionalフィールドを追加します。
  2. protocを実行します(少なくともv3.15、または--experimental_allow_proto3_optionalフラグを使用してv3.12)。
  3. デフォルト値を比較または設定する代わりに、生成された「hazzer」メソッドと「clear」メソッドをアプリケーションコードで使用します。

.protoファイルの変更

これは、プレゼンスなし明示的なプレゼンスの両方のセマンティクスに従うフィールドを持つproto3メッセージの例です。

syntax = "proto3";
package example;

message MyMessage {
  // implicit presence:
  int32 not_tracked = 1;

  // Explicit presence:
  optional int32 tracked = 2;
}

protocの呼び出し

proto3メッセージのプレゼンストラッキングは、v3.15.0リリース以降、デフォルトで有効になっています。以前はv3.12.0までは、protocでプレゼンストラッキングを使用する場合、--experimental_allow_proto3_optionalフラグが必要でした。

生成されたコードの使用

明示的なプレゼンスoptionalラベル)を持つproto3フィールド用に生成されたコードは、proto2ファイルの場合と同じになります。

これは、以下の「暗黙的なプレゼンス」の例で使用される定義です。

syntax = "proto3";
package example;
message Msg {
  int32 foo = 1;
}

これは、以下の「明示的なプレゼンス」の例で使用される定義です。

syntax = "proto3";
package example;
message Msg {
  optional int32 foo = 1;
}

例では、関数GetProtoは、指定されていないコンテンツを持つタイプMsgのメッセージを構築して返します。

C++の例

暗黙的なプレゼンス

Msg m = GetProto();
if (m.foo() != 0) {
  // "Clear" the field:
  m.set_foo(0);
} else {
  // Default value: field may not have been present.
  m.set_foo(1);
}

明示的なプレゼンス

Msg m = GetProto();
if (m.has_foo()) {
  // Clear the field:
  m.clear_foo();
} else {
  // Field is not present, so set it.
  m.set_foo(1);
}

C#の例

暗黙的なプレゼンス

var m = GetProto();
if (m.Foo != 0) {
  // "Clear" the field:
  m.Foo = 0;
} else {
  // Default value: field may not have been present.
  m.Foo = 1;
}

明示的なプレゼンス

var m = GetProto();
if (m.HasFoo) {
  // Clear the field:
  m.ClearFoo();
} else {
  // Field is not present, so set it.
  m.Foo = 1;
}

Goの例

暗黙的なプレゼンス

m := GetProto()
if m.Foo != 0 {
  // "Clear" the field:
  m.Foo = 0
} else {
  // Default value: field may not have been present.
  m.Foo = 1
}

明示的なプレゼンス

m := GetProto()
if m.Foo != nil {
  // Clear the field:
  m.Foo = nil
} else {
  // Field is not present, so set it.
  m.Foo = proto.Int32(1)
}

Javaの例

これらの例では、クリアを実証するためにBuilderを使用しています。Builderからプレゼンスを確認して値を取得するだけで、メッセージタイプと同じAPIに従います。

暗黙的なプレゼンス

Msg.Builder m = GetProto().toBuilder();
if (m.getFoo() != 0) {
  // "Clear" the field:
  m.setFoo(0);
} else {
  // Default value: field may not have been present.
  m.setFoo(1);
}

明示的なプレゼンス

Msg.Builder m = GetProto().toBuilder();
if (m.hasFoo()) {
  // Clear the field:
  m.clearFoo()
} else {
  // Field is not present, so set it.
  m.setFoo(1);
}

Pythonの例

暗黙的なプレゼンス

m = example.Msg()
if m.foo != 0:
  # "Clear" the field:
  m.foo = 0
else:
  # Default value: field may not have been present.
  m.foo = 1

明示的なプレゼンス

m = example.Msg()
if m.HasField('foo'):
  # Clear the field:
  m.ClearField('foo')
else:
  # Field is not present, so set it.
  m.foo = 1

Rubyの例

暗黙的なプレゼンス

m = Msg.new
if m.foo != 0
  # "Clear" the field:
  m.foo = 0
else
  # Default value: field may not have been present.
  m.foo = 1
end

明示的なプレゼンス

m = Msg.new
if m.has_foo?
  # Clear the field:
  m.clear_foo
else
  # Field is not present, so set it.
  m.foo = 1
end

Javascriptの例

暗黙的なプレゼンス

var m = new Msg();
if (m.getFoo() != 0) {
  // "Clear" the field:
  m.setFoo(0);
} else {
  // Default value: field may not have been present.
  m.setFoo(1);
}

明示的なプレゼンス

var m = new Msg();
if (m.hasFoo()) {
  // Clear the field:
  m.clearFoo()
} else {
  // Field is not present, so set it.
  m.setFoo(1);
}

Objective-Cの例

暗黙的なプレゼンス

Msg *m = [Msg message];
if (m.foo != 0) {
  // "Clear" the field:
  m.foo = 0;
} else {
  // Default value: field may not have been present.
  m.foo = 1;
}

明示的なプレゼンス

Msg *m = [Msg message];
if ([m hasFoo]) {
  // Clear the field:
  [m clearFoo];
} else {
  // Field is not present, so set it.
  m.foo = 1;
}

チートシート

Proto2

フィールドプレゼンスは追跡されますか?

フィールド型追跡?
単数フィールドはい
単数メッセージフィールドはい
oneofのフィールドはい
繰り返しフィールドとマップいいえ

Proto3

フィールドプレゼンスは追跡されますか?

フィールド型追跡?
その他の単数フィールドoptionalとして定義されている場合
単数メッセージフィールドはい
oneofのフィールドはい
繰り返しフィールドとマップいいえ

エディション2023

フィールドプレゼンスは追跡されますか?

フィールド型(優先順位降順)追跡?
繰り返しフィールドとマップいいえ
メッセージフィールドとOneofフィールドはい
features.field_presenceIMPLICITに設定されている場合のその他の単数フィールドいいえ
その他のすべてのフィールドはい