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

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

背景

フィールドの有無 (Field presence) とは、protobuf フィールドが値を持っているかどうかの概念です。protobuf には、暗黙的な有無 (implicit presence)明示的な有無 (explicit presence) の2つの異なる表現があります。暗黙的な有無では、生成されたメッセージ API がフィールド値のみを格納し、明示的な有無では、API がフィールドが設定されているかどうかを追跡します。

プレゼンスの規律

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

タグ-値ストリーム (ワイヤー形式) シリアライゼーションにおけるプレゼンス

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

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

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

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

Protobufs は人間が読めるテキスト形式で表現できます。注目すべき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 の実装内で「hazzers」と呼ばれることもあります。

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

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

ターゲット言語に応じて、生成された API には一般にいくつかのメソッドが含まれます。

  • oneof のハッザー: has_foo
  • oneof case メソッド: 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値がアプリケーションの有効な値のドメイン外にあると概念的に考えられる場合、この動作は 明示的なプレゼンス と同等と見なすことができます。

エディション API におけるプレゼンス

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

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

† メッセージと oneof には暗黙的なプレゼンスが一度もなく、エディションでは field_presence = IMPLICIT を設定することはできません。

エディションベースの API は、features.field_presenceIMPLICIT に設定されていない限り、proto2 と同様にフィールドのプレゼンスを明示的に追跡します。proto2 API と同様に、エディションベースの 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. デフォルト値を比較したり設定したりする代わりに、アプリケーションコードで生成された「ハッザー」メソッドと「クリア」メソッドを使用します。

.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.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 に設定されている場合、その他の単数形フィールドいいえ
その他すべてのフィールドはい