Application Note: Feldpräsenz
Hintergrund
Feldpräsenz ist die Vorstellung, ob ein Protobuf-Feld einen Wert hat. Es gibt zwei verschiedene Ausprägungen von Präsenz für Protobufs: implizite Präsenz, bei der die generierte Nachrichten-API Feldwerte (nur) speichert, und explizite Präsenz, bei der die API auch speichert, ob ein Feld gesetzt wurde oder nicht.
Hinweis
Wir empfehlen, für Proto3-Grunddatentypen immer das Labeloptional hinzuzufügen. Dies bietet einen reibungsloseren Übergang zu Editions, die standardmäßig explizite Präsenz verwenden.Präsenz-Disziplinen
Präsenz-Disziplinen definieren die Semantik für die Übersetzung zwischen der API-Repräsentation und der serialisierten Repräsentation. Die implizite Präsenz-Disziplin verlässt sich bei (De)Serialisierungsentscheidungen auf den Feldwert selbst, während sich die explizite Präsenz-Disziplin stattdessen auf den expliziten Tracking-Status verlässt.
Präsenz in Tag-Wert-Stream (Wire Format) Serialisierung
Das Wire-Format ist ein Stream von getaggten, selbst-begrenzenden Werten. Per Definition stellt das Wire-Format eine Sequenz von vorhandenen Werten dar. Mit anderen Worten, jeder Wert, der innerhalb einer Serialisierung gefunden wird, repräsentiert ein vorhandenes Feld; darüber hinaus enthält die Serialisierung keine Informationen über nicht vorhandene Werte.
Die generierte API für eine Proto-Nachricht enthält (De)Serialisierungsdefinitionen, die zwischen API-Typen und einem Stream von definitionsgemäß vorhandenen (Tag, Wert)-Paaren übersetzen. Diese Übersetzung ist so konzipiert, dass sie über Änderungen der Nachrichtendefinition hinweg vorwärts- und rückwärtskompatibel ist; diese Kompatibilität führt jedoch zu einigen (vielleicht überraschenden) Überlegungen beim Deserialisieren von Wire-Formatierten Nachrichten.
- Beim Serialisieren werden Felder mit impliziter Präsenz nicht serialisiert, wenn sie ihren Standardwert enthalten.
- Für numerische Typen ist der Standardwert 0.
- Für Enums ist der Standardwert der Enumerator mit dem Wert Null.
- Für Strings, Bytes und wiederholte Felder ist der Standardwert der Null-Längen-Wert.
- „Leere" Längen-begrenzte Werte (wie leere Strings) können gültig in serialisierten Werten dargestellt werden: das Feld ist „vorhanden" in dem Sinne, dass es im Wire-Format erscheint. Wenn die generierte API die Präsenz jedoch nicht verfolgt, werden diese Werte möglicherweise nicht neu serialisiert; d. h. das leere Feld ist möglicherweise nach einem Serialisierungs-Roundtrip „nicht vorhanden".
- Beim Deserialisieren können doppelte Feldwerte je nach Felddefinition unterschiedlich behandelt werden.
- Doppelte
repeatedFelder werden typischerweise an die API-Repräsentation des Feldes angehängt. (Beachten Sie, dass die Serialisierung eines packed wiederholten Feldes nur einen, Längen-begrenzten Wert im Tag-Stream erzeugt.) - Doppelte
optionalFeldwerte folgen der Regel, dass „das Letzte zählt".
- Doppelte
oneofFelder exponieren die API-Invariant, dass zu einem Zeitpunkt nur ein Feld gesetzt ist. Das Wire-Format kann jedoch mehrere (Tag, Wert)-Paare enthalten, die konzeptionell zumoneofgehören. Ähnlich wie beioptionalFeldern folgt die generierte API der Regel „das Letzte zählt".- Außerhalb des Bereichs liegende Werte werden für Enum-Felder in generierten Proto2-APIs nicht zurückgegeben. Außerhalb des Bereichs liegende Werte können jedoch als unbekannte Felder in der API gespeichert werden, auch wenn der Wire-Format-Tag erkannt wurde.
Präsenz in Named-Field Mapping Formaten
Protobufs können in menschenlesbaren, textuellen Formen dargestellt werden. Zwei bemerkenswerte Formate sind TextFormat (das Ausgabeformat, das von generierten Nachrichten DebugString-Methoden erzeugt wird) und JSON.
Diese Formate haben eigene Korrektheitsanforderungen und sind generell strenger als Tag-Wert-Stream-Formate. TextFormat ahmt jedoch die Semantik des Wire-Formats genauer nach und bietet in bestimmten Fällen ähnliche Semantiken (z. B. Anhängen von wiederholten Namen-Wert-Zuordnungen an ein wiederholtes Feld). Insbesondere, ähnlich wie beim Wire-Format, enthält TextFormat nur Felder, die vorhanden sind.
JSON ist jedoch ein viel strengeres Format und kann einige Semantiken des Wire-Formats oder TextFormat nicht gültig darstellen.
- Insbesondere sind JSON-Elemente semantisch ungeordnet, und jedes Mitglied muss einen eindeutigen Namen haben. Dies unterscheidet sich von den TextFormat-Regeln für wiederholte Felder.
- JSON kann Felder enthalten, die „nicht vorhanden" sind, im Gegensatz zur impliziten Präsenz-Disziplin für andere Formate.
- JSON definiert einen
null-Wert, der verwendet werden kann, um ein definiertes, aber nicht vorhandenes Feld darzustellen. - Wiederholte Feldwerte können in der formatierten Ausgabe enthalten sein, auch wenn sie dem Standardwert (einer leeren Liste) entsprechen.
- JSON definiert einen
- Da JSON-Elemente ungeordnet sind, gibt es keine Möglichkeit, die Regel „das Letzte zählt" eindeutig zu interpretieren.
- In den meisten Fällen ist dies in Ordnung: JSON-Elemente müssen eindeutige Namen haben: wiederholte Feldwerte sind kein gültiges JSON, daher müssen sie nicht aufgelöst werden, wie es bei TextFormat der Fall ist.
- Das bedeutet jedoch, dass es möglicherweise nicht möglich ist,
oneof-Felder eindeutig zu interpretieren: wenn mehrere Fälle vorhanden sind, sind sie ungeordnet.
Theoretisch kann JSON die Präsenz auf semantisch erhaltende Weise darstellen. In der Praxis kann die Korrektheit der Präsenz jedoch je nach Implementierungsentscheidungen variieren, insbesondere wenn JSON als Mittel zur Interoperabilität mit Clients, die keine Protobufs verwenden, gewählt wurde.
Präsenz in Proto2 APIs
Diese Tabelle zeigt, ob die Präsenz für Felder in Proto2-APIs verfolgt wird (sowohl für generierte APIs als auch unter Verwendung dynamischer Reflexion).
| Feldtyp | Explizite Präsenz |
|---|---|
| Singuläre numerische (Ganzzahl oder Gleitkomma) | ✔️ |
| Singuläres Enum | ✔️ |
| Singulärer String oder Bytes | ✔️ |
| Singuläre Nachricht | ✔️ |
| Wiederholt | |
| Oneofs | ✔️ |
| Maps (Zuordnungen) |
Singuläre Felder (aller Typen) verfolgen die Präsenz explizit in der generierten API. Die generierte Nachrichten-Schnittstelle enthält Methoden zur Abfrage der Präsenz von Feldern. Beispielsweise hat das Feld foo eine entsprechende has_foo-Methode. (Der spezifische Name folgt der gleichen sprachspezifischen Namenskonvention wie die Feld-Accessoren.) Diese Methoden werden manchmal innerhalb der Protobuf-Implementierung als „Hazzers" bezeichnet.
Ähnlich wie singuläre Felder verfolgen oneof-Felder explizit, welcher der Mitglieder, falls vorhanden, einen Wert enthält. Betrachten Sie zum Beispiel diesen Beispiel-oneof.
oneof foo {
int32 a = 1;
float b = 2;
}
Abhängig von der Zielsprache würde die generierte API im Allgemeinen mehrere Methoden enthalten:
- Ein Hazzer für den Oneof:
has_foo - Eine Oneof-Fall-Methode:
foo - Hazzers für die Mitglieder:
has_a,has_b - Getter für die Mitglieder:
a,b
Wiederholte Felder und Maps verfolgen keine Präsenz: es gibt keine Unterscheidung zwischen einem leeren und einem nicht vorhandenen wiederholten Feld.
Präsenz in Proto3 APIs
Diese Tabelle zeigt, ob die Präsenz für Felder in Proto3-APIs verfolgt wird (sowohl für generierte APIs als auch unter Verwendung dynamischer Reflexion).
| Feldtyp | optional | Explizite Präsenz |
|---|---|---|
| Singuläre numerische (Ganzzahl oder Gleitkomma) | Nein | |
| Singuläre numerische (Ganzzahl oder Gleitkomma) | Ja | ✔️ |
| Singuläres Enum | Nein | |
| Singuläres Enum | Ja | ✔️ |
| Singulärer String oder Bytes | Nein | |
| Singulärer String oder Bytes | Ja | ✔️ |
| Singuläre Nachricht | Nein | ✔️ |
| Singuläre Nachricht | Ja | ✔️ |
| Wiederholt | N/A | |
| Oneofs | N/A | ✔️ |
| Maps (Zuordnungen) | N/A |
Ähnlich wie bei Proto2-APIs verfolgt Proto3 keine Präsenz explizit für wiederholte Felder. Ohne das Label optional verfolgen Proto3-APIs auch keine Präsenz für grundlegende Datentypen (numerisch, String, Bytes und Enums). Oneof-Felder exponieren affirmatively Präsenz, obwohl möglicherweise nicht die gleichen Hazzer-Methoden wie in Proto2-APIs generiert werden.
Dieses Standardverhalten, keine Präsenz ohne das Label optional zu verfolgen, unterscheidet sich vom Proto2-Verhalten. Wir empfehlen die Verwendung des Labels optional mit Proto3, es sei denn, Sie haben einen bestimmten Grund, dies nicht zu tun.
Unter der impliziten Präsenz-Disziplin ist der Standardwert gleichbedeutend mit „nicht vorhanden" für Serialisierungszwecke. Um ein Feld konzeptionell zu „löschen" (damit es nicht serialisiert wird), würde ein API-Benutzer es auf den Standardwert setzen.
Der Standardwert für Felder vom Enum-Typ unter impliziter Präsenz ist der entsprechende Enumerator mit dem Wert 0. Gemäß den Proto3-Syntaxregeln müssen alle Enum-Typen einen Enumeratorwert haben, der auf 0 abgebildet wird. Konventionsgemäß ist dies ein UNKNOWN oder ähnlich benannter Enumerator. Wenn der Nullwert konzeptionell außerhalb des Gültigkeitsbereichs der für die Anwendung gültigen Werte liegt, kann dieses Verhalten als gleichbedeutend mit expliziter Präsenz betrachtet werden.
Präsenz in Editions APIs
Diese Tabelle zeigt, ob die Präsenz für Felder in Editions-APIs verfolgt wird (sowohl für generierte APIs als auch unter Verwendung dynamischer Reflexion).
| Feldtyp | Explizite Präsenz |
|---|---|
| Singuläre numerische (Ganzzahl oder Gleitkomma) | ✔️ |
| Singuläres Enum | ✔️ |
| Singulärer String oder Bytes | ✔️ |
| Singuläre Nachricht† | ✔️ |
| Wiederholt | |
| Oneofs† | ✔️ |
| Maps (Zuordnungen) |
† Nachrichten und Oneofs hatten nie implizite Präsenz, und Editions erlaubt Ihnen nicht, field_presence = IMPLICIT zu setzen.
Editionsbasierte APIs verfolgen die Feldpräsenz explizit, ähnlich wie Proto2, es sei denn, features.field_presence ist auf IMPLICIT gesetzt. Ähnlich wie bei Proto2-APIs verfolgen editionsbasierte APIs keine Präsenz explizit für wiederholte Felder.
Semantische Unterschiede
Die implizite Präsenz-Serialisierungsdisziplin führt zu sichtbaren Unterschieden zur expliziten Präsenz-Tracking-Disziplin, wenn der Standardwert gesetzt ist. Für ein singuläres Feld mit numerischem Typ, Enum oder String-Typ
- Implizite Präsenz-Disziplin
- Standardwerte werden nicht serialisiert.
- Standardwerte werden nicht aus zusammengeführt.
- Um ein Feld zu „löschen", wird es auf seinen Standardwert gesetzt.
- Der Standardwert kann bedeuten
- das Feld wurde explizit auf seinen Standardwert gesetzt, was im anwendungsspezifischen Wertebereich gültig ist;
- das Feld wurde konzeptionell durch Setzen seines Standards „gelöscht"; oder
- das Feld wurde nie gesetzt.
has_-Methoden werden nicht generiert (aber siehe Hinweis nach dieser Liste)
- Explizite Präsenz-Disziplin
- Explizit gesetzte Werte werden immer serialisiert, einschließlich Standardwerten.
- Nicht gesetzte Felder werden nie aus zusammengeführt.
- Explizit gesetzte Felder – einschließlich Standardwerten – werden aus zusammengeführt.
- Eine generierte
has_foo-Methode zeigt an, ob das Feldfoogesetzt (und nicht gelöscht) wurde. - Eine generierte
clear_foo-Methode muss verwendet werden, um den Wert zu löschen (d. h. nicht zu setzen).
Hinweis
Has_-Methoden werden in den meisten Fällen nicht für implizite Mitglieder generiert. Die Ausnahme von diesem Verhalten ist Dart, das has_-Methoden mit Proto3-Protoschemata-Dateien generiert.Überlegungen zum Zusammenführen
Unter den impliziten Präsenz-Regeln ist es für ein Ziel-Feld praktisch unmöglich, aus seinem Standardwert zusammengeführt zu werden (unter Verwendung der Protobuf-API-Mergefunktionen). Dies liegt daran, dass Standardwerte übersprungen werden, ähnlich der impliziten Präsenz-Serialisierungsdisziplin. Das Zusammenführen aktualisiert die Ziel-(zusammengeführt-zu) Nachricht nur mit den nicht übersprungenen Werten aus der Update-(zusammengeführt-von) Nachricht.
Der Unterschied im Zusammenführungsverhalten hat weitere Auswirkungen auf Protokolle, die auf partielle „Patch"-Updates angewiesen sind. Wenn die Feldpräsenz nicht verfolgt wird, kann ein Update-Patch allein keine Aktualisierung auf den Standardwert darstellen, da nur Nicht-Standardwerte zusammengeführt werden.
Das Aktualisieren zum Setzen eines Standardwerts erfordert in diesem Fall einen externen Mechanismus, wie z. B. FieldMask. Wenn die Präsenz jedoch verfolgt wird, werden alle explizit gesetzten Werte – auch Standardwerte – in das Ziel übernommen.
Überlegungen zur Änderungskompatibilität
Die Änderung eines Feldes zwischen expliziter Präsenz und impliziter Präsenz ist eine binärkompatible Änderung für serialisierte Werte im Wire-Format. Die serialisierte Darstellung der Nachricht kann jedoch variieren, abhängig davon, welche Version der Nachrichtendefinition für die Serialisierung verwendet wurde. Insbesondere, wenn ein „Sender" ein Feld explizit auf seinen Standardwert setzt
- Der serialisierte Wert nach der impliziten Präsenz-Disziplin enthält den Standardwert nicht, auch wenn er explizit gesetzt wurde.
- Der serialisierte Wert nach der expliziten Präsenz-Disziplin enthält jedes „vorhandene" Feld, auch wenn es den Standardwert enthält.
Diese Änderung kann sicher sein oder auch nicht, abhängig von der Semantik der Anwendung. Betrachten Sie zum Beispiel zwei Clients mit unterschiedlichen Versionen einer Nachrichtendefinition.
Client A verwendet diese Definition der Nachricht, die die explizite Präsenz-Serialisierungsdisziplin für das Feld foo befolgt.
syntax = "proto3";
message Msg {
optional int32 foo = 1;
}
Client B verwendet eine Definition derselben Nachricht, außer dass sie der keine Präsenz-Disziplin folgt.
syntax = "proto3";
message Msg {
int32 foo = 1;
}
Betrachten wir nun ein Szenario, in dem Client A die Präsenz von foo beobachtet, während die Clients die „gleiche" Nachricht wiederholt austauschen, indem sie deserialisieren und neu serialisieren.
// 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
Wenn Client A von expliziter Präsenz für foo abhängt, dann ist ein „Roundtrip" über Client B aus der Perspektive von Client A verlustbehaftet. Im Beispiel ist dies keine sichere Änderung: Client A benötigt (durch assert), dass das Feld vorhanden ist; selbst ohne Modifikationen über die API schlägt diese Anforderung in einem wert- und peer-abhängigen Fall fehl.
So aktivieren Sie explizite Präsenz in Proto3
Dies sind die allgemeinen Schritte zur Verwendung der Feld-Tracking-Unterstützung für Proto3.
- Fügen Sie ein
optional-Feld zu einer.proto-Datei hinzu. - Führen Sie
protocaus (mindestens v3.15 oder v3.12 mit dem Flag--experimental_allow_proto3_optional). - Verwenden Sie in Ihrem Anwendungscode die generierten „Hazzer"-Methoden und „Clear"-Methoden anstelle von Vergleichen oder Setzen von Standardwerten.
.proto Dateiinhalte
Dies ist ein Beispiel für eine Proto3-Nachricht mit Feldern, die sowohl keine Präsenz als auch explizite Präsenz-Semantik befolgen.
syntax = "proto3";
package example;
message MyMessage {
// implicit presence:
int32 not_tracked = 1;
// Explicit presence:
optional int32 tracked = 2;
}
protoc Aufruf
Die Präsenz-Verfolgung für Proto3-Nachrichten ist seit der v3.15.0-Version standardmäßig aktiviert. Zuvor bis v3.12.0 war das Flag --experimental_allow_proto3_optional erforderlich, wenn die Präsenz-Verfolgung mit protoc verwendet wurde.
Verwendung des generierten Codes
Der generierte Code für Proto3-Felder mit expliziter Präsenz (dem Label optional) ist derselbe wie in einer Proto2-Datei.
Dies ist die Definition, die in den nachstehenden „impliziten Präsenz"-Beispielen verwendet wird.
syntax = "proto3";
package example;
message Msg {
int32 foo = 1;
}
Dies ist die Definition, die in den nachstehenden „expliziten Präsenz"-Beispielen verwendet wird.
syntax = "proto3";
package example;
message Msg {
optional int32 foo = 1;
}
In den Beispielen konstruiert eine Funktion GetProto und gibt eine Nachricht vom Typ Msg mit nicht spezifiziertem Inhalt zurück.
C++ Beispiel
Implizite Präsenz
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);
}
Explizite Präsenz
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# Beispiel
Implizite Präsenz
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;
}
Explizite Präsenz
var m = GetProto();
if (m.HasFoo) {
// Clear the field:
m.ClearFoo();
} else {
// Field is not present, so set it.
m.Foo = 1;
}
Go Beispiel
Implizite Präsenz
m := GetProto()
if m.Foo != 0 {
// "Clear" the field:
m.Foo = 0
} else {
// Default value: field may not have been present.
m.Foo = 1
}
Explizite Präsenz
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 Beispiel
Diese Beispiele verwenden einen Builder, um das Löschen zu demonstrieren. Das einfache Überprüfen der Präsenz und Abrufen von Werten aus einem Builder folgt der gleichen API wie der Nachrichtentyp.
Implizite Präsenz
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);
}
Explizite Präsenz
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 Beispiel
Implizite Präsenz
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
Explizite Präsenz
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 Beispiel
Implizite Präsenz
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
Explizite Präsenz
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 Beispiel
Implizite Präsenz
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);
}
Explizite Präsenz
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 Beispiel
Implizite Präsenz
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;
}
Explizite Präsenz
Msg *m = [Msg message];
if ([m hasFoo]) {
// Clear the field:
[m clearFoo];
} else {
// Field is not present, so set it.
m.foo = 1;
}
Spickzettel
Proto2
Wird die Feldpräsenz verfolgt?
| Feldtyp | Verfolgt? |
|---|---|
| Singuläres Feld | Ja |
| Singuläres Nachrichtenfeld | Ja |
| Feld in einem Oneof | Ja |
| Wiederholtes Feld & Map | Nein |
Proto3
Wird die Feldpräsenz verfolgt?
| Feldtyp | Verfolgt? |
|---|---|
| Andere singuläre Felder | wenn als optional definiert |
| Singuläres Nachrichtenfeld | Ja |
| Feld in einem Oneof | Ja |
| Wiederholtes Feld & Map | Nein |
Edition 2023
Wird die Feldpräsenz verfolgt?
| Feldtyp (in absteigender Präzedenz) | Verfolgt? |
|---|---|
| Wiederholtes Feld & Map | Nein |
| Nachrichten- und Oneof-Felder | Ja |
Andere singuläre Felder, wenn features.field_presence auf IMPLICIT gesetzt ist | Nein |
| Alle anderen Felder | Ja |