Application Note: Feldpräsenz

Erklärt die verschiedenen Präsenz-Tracking-Disziplinen für Protobuf-Felder. Erklärt auch das Verhalten der expliziten Präsenz-Verfolgung für singuläre Proto3-Felder mit grundlegenden Datentypen.

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.

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 repeated Felder 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 optional Feldwerte folgen der Regel, dass „das Letzte zählt".
  • oneof Felder 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 zum oneof gehören. Ähnlich wie bei optional Feldern 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.
  • 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).

FeldtypExplizite 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).

FeldtypoptionalExplizite Präsenz
Singuläre numerische (Ganzzahl oder Gleitkomma)Nein
Singuläre numerische (Ganzzahl oder Gleitkomma)Ja✔️
Singuläres EnumNein
Singuläres EnumJa✔️
Singulärer String oder BytesNein
Singulärer String oder BytesJa✔️
Singuläre NachrichtNein✔️
Singuläre NachrichtJa✔️
WiederholtN/A
OneofsN/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).

FeldtypExplizite 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 Feld foo gesetzt (und nicht gelöscht) wurde.
    • Eine generierte clear_foo-Methode muss verwendet werden, um den Wert zu löschen (d. h. nicht zu setzen).

Ü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.

  1. Fügen Sie ein optional-Feld zu einer .proto-Datei hinzu.
  2. Führen Sie protoc aus (mindestens v3.15 oder v3.12 mit dem Flag --experimental_allow_proto3_optional).
  3. 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?

FeldtypVerfolgt?
Singuläres FeldJa
Singuläres NachrichtenfeldJa
Feld in einem OneofJa
Wiederholtes Feld & MapNein

Proto3

Wird die Feldpräsenz verfolgt?

FeldtypVerfolgt?
Andere singuläre Felderwenn als optional definiert
Singuläres NachrichtenfeldJa
Feld in einem OneofJa
Wiederholtes Feld & MapNein

Edition 2023

Wird die Feldpräsenz verfolgt?

Feldtyp (in absteigender Präzedenz)Verfolgt?
Wiederholtes Feld & MapNein
Nachrichten- und Oneof-FelderJa
Andere singuläre Felder, wenn features.field_presence auf IMPLICIT gesetzt istNein
Alle anderen FelderJa