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

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

背景

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

存在の規律

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

「タグ値ストリーム」(ワイヤーフォーマット)のシリアル化における存在

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

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

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

「名前付きフィールドマッピング」フォーマットにおける存在

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

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

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

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

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

Proto2 APIにおける存在

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

フィールド型明示的な存在
単一の数値 (整数または浮動小数点)✔️
単一の列挙型✔️
単一の文字列またはバイト✔️
単一のメッセージ✔️
繰り返し
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と動的リフレクションの両方)でフィールドの存在が追跡されるかどうかを示しています。

フィールド型オプション明示的な存在
単一の数値 (整数または浮動小数点)いいえ
単一の数値 (整数または浮動小数点)はい✔️
単一の列挙型いいえ
単一の列挙型はい✔️
単一の文字列またはバイトいいえ
単一の文字列またはバイトはい✔️
単一のメッセージいいえ✔️
単一のメッセージはい✔️
繰り返し該当なし
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における存在

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

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

† メッセージとoneofはこれまで暗黙的な存在を持ったことがなく、エディションでは`field_presence = IMPLICIT`を設定することはできません。

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

意味的な違い

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

  • 暗黙的な存在の規律
    • デフォルト値はシリアル化されません。
    • デフォルト値はマージされません。
    • フィールドを「クリア」するには、デフォルト値に設定します。
    • デフォルト値は以下のいずれかの意味を持ちます。
      • フィールドが明示的にデフォルト値に設定されたものであり、これはアプリケーション固有の値のドメインで有効である。
      • フィールドがデフォルトに設定されることで、概念的に「クリア」されたものである。
      • フィールドが一度も設定されなかった。
    • 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以降、またはv3.12で--experimental_allow_proto3_optionalフラグを使用)。
  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に設定されている場合のその他の単一フィールドいいえ
その他のすべてのフィールドはい