アプリケーションノート:フィールドプレゼンス
背景
フィールドプレゼンスとは、protobufフィールドが値を持っているかどうかの概念です。protobufのプレゼンスには2つの異なる形があります。生成されたメッセージAPIがフィールド値(のみ)を格納する暗黙的なプレゼンスと、APIがフィールドが設定されたかどうかを格納する明示的なプレゼンスです。
歴史的に、proto2は主に明示的なプレゼンスに従ってきましたが、proto3は暗黙的なプレゼンスのセマンティクスのみを公開しています。optional
ラベルで定義された基本的な型(数値、文字列、バイト、enum)の単数proto3フィールドは、proto2のように明示的なプレゼンスを持ちます(この機能はリリース3.15からデフォルトで有効になっています)。
注
proto3の基本的な型には常にoptional
ラベルを追加することをお勧めします。これは、デフォルトで明示的なプレゼンスを使用するエディションへのよりスムーズな道を提供します。プレゼンスの規律
プレゼンスの規律は、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要素は一意の名前を持っている必要があります。繰り返しフィールド値は有効な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_a
、has_b
- メンバーのゲッター:
a
、b
繰り返しフィールドとマップはプレゼンスを追跡しません。空の繰り返しフィールドとnot-presentの繰り返しフィールドの間に区別はありません。
Proto3 APIにおけるプレゼンス
この表は、proto3 APIのフィールドでプレゼンスが追跡されるかどうかを概説しています(生成されたAPIと動的リフレクションの両方)。
フィールド型 | optional | 明示的なプレゼンス |
---|---|---|
単数数値 (整数または浮動小数点) | いいえ | |
単数数値 (整数または浮動小数点) | はい | ✔️ |
単数enum | いいえ | |
単数enum | はい | ✔️ |
単数文字列またはバイト | いいえ | |
単数文字列またはバイト | はい | ✔️ |
単数メッセージ | いいえ | ✔️ |
単数メッセージ | はい | ✔️ |
繰り返し | N/A | |
Oneofs | N/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
メソッドは、値をクリア(つまり、未設定)するために使用する必要があります。
注
Has_
メソッドは暗黙的なメンバーに対しては生成されません。この動作の例外はDartであり、proto3 protoスキーマファイルでhas_
メソッドを生成します。マージに関する考慮事項
暗黙的なプレゼンスルールでは、ターゲットフィールドがデフォルト値からマージ元になることは事実上不可能です(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のフィールド追跡サポートを使用するための一般的な手順です。
.proto
ファイルにoptional
フィールドを追加します。protoc
を実行します(少なくともv3.15、または--experimental_allow_proto3_optional
フラグを使用してv3.12)。- デフォルト値を比較または設定する代わりに、生成された「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_presence がIMPLICIT に設定されている場合のその他の単数フィールド | いいえ |
その他のすべてのフィールド | はい |