アプリケーションノート:フィールドの存在

protobuf フィールドのさまざまなプレゼンス追跡の規律について説明します。また、基本型を持つ単一の proto3 フィールドに対する明示的なプレゼンス追跡の動作についても説明します。

背景

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

プレゼンスの規律

プレゼンスの規律は、API 表現とシリアライズされた表現との間の変換におけるセマンティクスを定義します。「暗黙的なプレゼンス」の規律は、(デ)シリアライズ時にフィールド値自体に依存して決定を行いますが、「明示的なプレゼンス」の規律は、明示的な追跡状態に依存します。

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

ワイヤーフォーマットは、タグ付けされ、自己区切りの値のストリームです。定義上、ワイヤーフォーマットは「存在する」値のシーケンスを表します。つまり、シリアライズ内で見つかるすべての値は「存在する」フィールドを表し、さらに、シリアライズには存在しない値に関する情報は含まれません。

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

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

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

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

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

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

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

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

Proto2 API におけるプレゼンス

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

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

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

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

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

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

  • oneof のハザース: has_foo
  • oneof ケースメソッド: foo
  • メンバーのハザース: has_a, has_b
  • メンバーのゲッター: a, b

繰り返しフィールドとマップはプレゼンスを追跡しません。空の繰り返しフィールドと存在しない繰り返しフィールドの間に区別はありません。

Proto3 API におけるプレゼンス

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

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

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

optional ラベルなしでプレゼンスを追跡しないこのデフォルトの動作は、proto2 の動作とは異なります。特別な理由がない限り、proto3 で optional ラベルを使用することをお勧めします。

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

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

Editions API におけるプレゼンス

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

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

† メッセージと oneof はこれまで暗黙的なプレゼンスを持っておらず、エディションでは field_presence = IMPLICIT を設定することはできません。

Editions ベースの API は、features.field_presenceIMPLICIT に設定されていない限り、proto2 と同様にフィールドのプレゼンスを明示的に追跡します。proto2 API と同様に、Editions ベースの API は繰り返しフィールドのプレゼンスを明示的に追跡しません。

セマンティクスの違い

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

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

マージに関する考慮事項

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

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

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

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

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

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

この変更は、アプリケーションのセマンティクスによっては安全である場合とそうでない場合があります。例えば、異なるバージョンのメッセージ定義を持つ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 内のフィールドはい
繰り返しフィールド&マップいいえ

Edition 2023

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

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