Sprachführer (Editionen)
Dieser Leitfaden beschreibt, wie die Protokollpuffer-Sprache zur Strukturierung Ihrer Protokollpuffer-Daten verwendet wird, einschließlich der .proto-Dateisyntax und wie Sie Datenzugriffsklassen aus Ihren .proto-Dateien generieren. Er deckt die Edition 2023 bis Edition 2024 der Protokollpuffer-Sprache ab. Informationen darüber, wie sich Editionen konzeptionell von proto2 und proto3 unterscheiden, finden Sie unter Übersicht über Protobuf-Editionen.
Informationen zur proto2-Syntax finden Sie im Proto2 Sprachleitfaden.
Informationen zur proto3-Syntax finden Sie im Proto3 Sprachleitfaden.
Dies ist ein Referenzhandbuch – ein Schritt-für-Schritt-Beispiel, das viele der in diesem Dokument beschriebenen Funktionen verwendet, finden Sie im Tutorial für Ihre gewählte Sprache.
Definieren eines Nachrichtentyps
Betrachten wir zunächst ein sehr einfaches Beispiel. Angenommen, Sie möchten ein Nachrichtenformat für Suchanfragen definieren, bei dem jede Suchanfrage eine Abfragestrzeichenkette, die gewünschte Ergebnisseite und die Anzahl der Ergebnisse pro Seite enthält. Hier ist die .proto-Datei, die Sie zur Definition des Nachrichtentyps verwenden.
edition = "2023";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
Die erste Zeile der Datei gibt an, dass Sie Edition 2023 der protobuf-Sprachspezifikation verwenden.
- Die
edition(odersyntaxfür proto2/proto3) muss die erste nicht-leere, nicht-Kommentarzeile der Datei sein. - Wenn keine
editionodersyntaxangegeben ist, geht der Protocol Buffer-Compiler davon aus, dass Sie proto2 verwenden.
- Die
Die
SearchRequest-Nachrichtendefinition gibt drei Felder (Namens-/Wertpaare) an, eines für jedes Datenelement, das Sie in diesem Nachrichtentyp aufnehmen möchten. Jedes Feld hat einen Namen und einen Typ.
Festlegen von Feldtypen
Im vorherigen Beispiel sind alle Felder skalare Typen: zwei ganze Zahlen (page_number und results_per_page) und eine Zeichenkette (query). Sie können auch Aufzählungen und zusammengesetzte Typen wie andere Nachrichtentypen für Ihr Feld angeben.
Zuweisen von Feldnummern
Sie müssen jedem Feld in Ihrer Nachrichtendefinition eine Nummer zwischen 1 und 536.870.911 zuweisen, mit den folgenden Einschränkungen:
- Die angegebene Nummer muss eindeutig für alle Felder dieser Nachricht sein.
- Feldnummern
19.000bis19.999sind für die Protocol Buffers-Implementierung reserviert. Der Protocol Buffer-Compiler wird sich beschweren, wenn Sie eine dieser reservierten Feldnummern in Ihrer Nachricht verwenden. - Sie können keine zuvor reservierten Feldnummern oder Feldnummern verwenden, die Erweiterungen zugewiesen wurden.
Diese Nummer kann nicht geändert werden, sobald Ihr Nachrichtentyp in Gebrauch ist, da sie das Feld im Nachrichten-Wire-Format identifiziert. Eine „Änderung“ einer Feldnummer entspricht dem Löschen dieses Feldes und der Erstellung eines neuen Feldes mit demselben Typ, aber einer neuen Nummer. Weitere Informationen hierzu finden Sie unter Felder löschen.
Feldnummern dürfen niemals wiederverwendet werden. Nehmen Sie niemals eine Feldnummer aus der reservierten Liste zur Wiederverwendung mit einer neuen Felddefinition auf. Siehe Folgen der Wiederverwendung von Feldnummern.
Sie sollten die Feldnummern 1 bis 15 für die am häufigsten gesetzten Felder verwenden. Kleinere Feldnummern benötigen weniger Speicherplatz im Wire-Format. Feldnummern im Bereich 1 bis 15 benötigen beispielsweise ein Byte zur Kodierung. Feldnummern im Bereich 16 bis 2047 benötigen zwei Bytes. Mehr dazu erfahren Sie in Protocol Buffer-Kodierung.
Folgen der Wiederverwendung von Feldnummern
Die Wiederverwendung einer Feldnummer macht die Dekodierung von Wire-Format-Nachrichten mehrdeutig.
Das Protobuf-Wire-Format ist schlank und bietet keine Möglichkeit, Felder zu erkennen, die mit einer Definition kodiert und mit einer anderen dekodiert wurden.
Das Kodieren eines Feldes mit einer Definition und anschließende Dekodieren desselben Feldes mit einer anderen Definition kann zu folgenden Problemen führen:
- Verlorene Entwicklerzeit durch Debugging
- Ein Parse-/Merge-Fehler (im besten Fall)
- Ausgelaufene PII/SPII (personenbezogene/sensible personenbezogene Daten)
- Datenbeschädigung
Häufige Ursachen für die Wiederverwendung von Feldnummern
Neubewertung von Feldern (manchmal zur Erreichung einer ästhetisch ansprechenderen Reihenfolge für Felder). Neubewertung löscht und fügt effektiv alle beteiligten Felder neu hinzu, was zu inkompatiblen Wire-Format-Änderungen führt.
Löschen eines Feldes und Nicht-Reservieren der Nummer, um zukünftige Wiederverwendung zu verhindern.
- Dies war aus mehreren Gründen ein sehr einfacher Fehler bei Erweiterungsfeldern. Erweiterungsdeklarationen bieten einen Mechanismus zur Reservierung von Erweiterungsfeldern.
Die Feldnummer ist auf 29 Bits statt 32 Bits begrenzt, da drei Bits zur Angabe des Wire-Formats des Feldes verwendet werden. Mehr dazu finden Sie im Thema Kodierung.
Festlegen der Feldkardinalität
Nachrichtenfelder können einer der folgenden Typen sein:
Singular (einzeln):
Ein singuläres Feld hat keine explizite Kardinalitätsbezeichnung. Es hat zwei mögliche Zustände:
- Das Feld ist gesetzt und enthält einen Wert, der explizit gesetzt oder aus dem Wire-Format gelesen wurde. Es wird an das Wire-Format serialisiert.
- Das Feld ist nicht gesetzt und gibt den Standardwert zurück. Es wird nicht an das Wire-Format serialisiert.
Sie können prüfen, ob der Wert explizit gesetzt wurde.
Proto3 implizite Felder, die auf Editionen migriert wurden, verwenden das
field_presence-Feature mit dem WertIMPLICIT.Proto2
required-Felder, die auf Editionen migriert wurden, verwenden ebenfalls dasfield_presence-Feature, aber aufLEGACY_REQUIREDgesetzt.repeated(wiederholt): Dieser Feldtyp kann in einer wohlgeformten Nachricht null- oder mehrmals vorkommen. Die Reihenfolge der wiederholten Werte wird beibehalten.map(Zuordnung): Dies ist ein Feldtyp mit Schlüssel/Wert-Paaren. Weitere Informationen zu diesem Feldtyp finden Sie unter Maps.
Wiederholte Felder sind standardmäßig gepackt (Packed)
In Protobuf-Editionen verwenden repeated-Felder von skalaren numerischen Typen standardmäßig die packed-Kodierung.
Weitere Informationen zur packed-Kodierung finden Sie unter Protocol Buffer-Kodierung.
Wohlgeformte Nachrichten
Der Begriff „wohlgeformt“, wenn er auf Protobuf-Nachrichten angewendet wird, bezieht sich auf die serialisierten/deserialisierten Bytes. Der protoc-Parser validiert, ob eine gegebene proto-Definitionsdatei lesbar ist.
Singuläre Felder können mehrfach in den Wire-Format-Bytes vorkommen. Der Parser akzeptiert die Eingabe, aber nur die letzte Instanz dieses Feldes ist über die generierten Bindings zugänglich. Weitere Informationen hierzu finden Sie unter Last One Wins.
Hinzufügen weiterer Nachrichtentypen
Mehrere Nachrichtentypen können in einer einzigen .proto-Datei definiert werden. Dies ist nützlich, wenn Sie mehrere zusammengehörige Nachrichten definieren – zum Beispiel, wenn Sie das Antwortnachrichtenformat definieren möchten, das Ihrer SearchResponse-Nachricht entspricht, können Sie es derselben .proto hinzufügen.
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
message SearchResponse {
...
}
Kombinieren von Nachrichten führt zu bloat Während mehrere Nachrichtentypen (wie Nachrichten, Enums und Dienste) in einer einzigen .proto-Datei definiert werden können, kann dies auch zu Abhängigkeits-Bloat führen, wenn eine große Anzahl von Nachrichten mit unterschiedlichen Abhängigkeiten in einer einzigen Datei definiert wird. Es wird empfohlen, so wenige Nachrichtentypen wie möglich pro .proto-Datei einzuschließen.
Hinzufügen von Kommentaren
So fügen Sie Ihren .proto-Dateien Kommentare hinzu:
Bevorzugen Sie Zeilenendkommentare im C/C++/Java-Stil „//“ auf der Zeile vor dem .proto-Codeelement
Inline-/Mehrzeilenkommentare im C-Stil
/* ... */werden ebenfalls akzeptiert.- Bei mehrzeiligen Kommentaren wird eine Randzeile mit „*“ bevorzugt.
/**
* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response.
*/
message SearchRequest {
string query = 1;
// Which page number do we want?
int32 page_number = 2;
// Number of results to return per page.
int32 results_per_page = 3;
}
Löschen von Feldern
Das Löschen von Feldern kann zu schwerwiegenden Problemen führen, wenn es nicht korrekt durchgeführt wird.
Wenn Sie ein Feld nicht mehr benötigen und alle Referenzen aus dem Client-Code gelöscht wurden, können Sie die Felddefinition aus der Nachricht löschen. Sie müssen jedoch die gelöschte Feldnummer reservieren. Wenn Sie die Feldnummer nicht reservieren, ist es möglich, dass ein Entwickler diese Nummer in Zukunft wiederverwendet.
Sie sollten auch den Feldnamen reservieren, um das Parsen von Nachrichten mit JSON- und TextFormat-Kodierungen weiterhin zu ermöglichen.
Reservierte Feldnummern
Wenn Sie einen Nachrichtentyp aktualisieren, indem Sie ein Feld vollständig löschen oder auskommentieren, können zukünftige Entwickler die Feldnummer wiederverwenden, wenn sie eigene Aktualisierungen an dem Typ vornehmen. Dies kann zu schwerwiegenden Problemen führen, wie in Folgen der Wiederverwendung von Feldnummern beschrieben. Um sicherzustellen, dass dies nicht geschieht, fügen Sie Ihre gelöschte Feldnummer zur reserved-Liste hinzu.
Der protoc-Compiler generiert Fehlermeldungen, wenn zukünftige Entwickler versuchen, diese reservierten Feldnummern zu verwenden.
message Foo {
reserved 2, 15, 9 to 11;
}
Reservierte Feldnummernbereiche sind inklusiv (9 bis 11 ist dasselbe wie 9, 10, 11).
Reservierte Feldnamen
Die spätere Wiederverwendung eines alten Feldnamens ist im Allgemeinen sicher, außer bei der Verwendung von TextProto- oder JSON-Kodierungen, bei denen der Feldname serialisiert wird. Um dieses Risiko zu vermeiden, können Sie den gelöschten Feldnamen zur reserved-Liste hinzufügen.
Reservierte Namen beeinflussen nur das Verhalten des protoc-Compilers und nicht das Laufzeitverhalten, mit einer Ausnahme: TextProto-Implementierungen können unbekannte Felder (ohne eine Fehlermeldung wie bei anderen unbekannten Feldern) mit reservierten Namen beim Parsen verwerfen (nur die C++- und Go-Implementierungen tun dies derzeit). Das Laufzeit-JSON-Parsing wird von reservierten Namen nicht beeinflusst.
message Foo {
reserved 2, 15, 9 to 11;
reserved foo, bar;
}
Beachten Sie, dass Sie Feldnamen und Feldnummern nicht in derselben reserved-Anweisung mischen können.
Was wird aus Ihrer .proto-Datei generiert?
Wenn Sie den Protocol Buffer-Compiler für eine .proto-Datei ausführen, generiert der Compiler den Code in Ihrer gewählten Sprache, den Sie benötigen, um mit den in der Datei beschriebenen Nachrichtentypen zu arbeiten, einschließlich des Abrufens und Setzens von Feldwerten, des Serialisierens Ihrer Nachrichten in einen Ausgabestrom und des Parsens Ihrer Nachrichten aus einem Eingabestrom.
- Für C++ generiert der Compiler aus jeder
.proto-Datei eine.h- und eine.cc-Datei mit einer Klasse für jeden in Ihrer Datei beschriebenen Nachrichtentyp. - Für Java generiert der Compiler eine
.java-Datei mit einer Klasse für jeden Nachrichtentyp sowie eine spezielleBuilder-Klasse zum Erstellen von Nachrichtenklasseninstanzen. - Für Kotlin generiert der Compiler zusätzlich zum generierten Java-Code eine
.kt-Datei für jeden Nachrichtentyp mit einer verbesserten Kotlin-API. Dies beinhaltet eine DSL, die die Erstellung von Nachrichteninstanzen vereinfacht, einen nullable Feld-Accessor und eine Kopierfunktion. - Python ist etwas anders – der Python-Compiler generiert ein Modul mit einer statischen Beschreibung jedes Nachrichtentyps in Ihrer
.proto-Datei, die dann mit einer Metaklasse verwendet wird, um die notwendige Python-Datenzugriffsklasse zur Laufzeit zu erstellen. - Für Go generiert der Compiler eine
.pb.go-Datei mit einem Typ für jeden Nachrichtentyp in Ihrer Datei. - Für Ruby generiert der Compiler eine
.rb-Datei mit einem Ruby-Modul, das Ihre Nachrichtentypen enthält. - Für Objective-C generiert der Compiler aus jeder
.proto-Datei einepbobjc.h- und einepbobjc.m-Datei mit einer Klasse für jeden in Ihrer Datei beschriebenen Nachrichtentyp. - Für C# generiert der Compiler aus jeder
.proto-Datei eine.cs-Datei mit einer Klasse für jeden in Ihrer Datei beschriebenen Nachrichtentyp. - Für PHP generiert der Compiler eine
.php-Nachrichtendatei für jeden in Ihrer Datei beschriebenen Nachrichtentyp und eine.php-Metadatendatei für jede kompilierte.proto-Datei. Die Metadatendatei wird verwendet, um die gültigen Nachrichtentypen in den Deskriptorpool zu laden. - Für Dart generiert der Compiler eine
.pb.dart-Datei mit einer Klasse für jeden Nachrichtentyp in Ihrer Datei.
Weitere Informationen zur Verwendung der APIs für jede Sprache finden Sie im Tutorial für Ihre gewählte Sprache. Für noch detailliertere API-Informationen siehe die entsprechende API-Referenz.
Skalare Werttypen
Ein skalarer Nachrichtenfeld kann einen der folgenden Typen haben – die Tabelle zeigt den im .proto-Datei angegebenen Typ und den entsprechenden Typ in der automatisch generierten Klasse
| Proto-Typ | Hinweise |
|---|---|
| double | |
| float | |
| int32 | Verwendet variable Längen-Kodierung. Ineffizient für die Kodierung negativer Zahlen – wenn Ihr Feld wahrscheinlich negative Werte hat, verwenden Sie stattdessen sint32. |
| int64 | Verwendet variable Längen-Kodierung. Ineffizient für die Kodierung negativer Zahlen – wenn Ihr Feld wahrscheinlich negative Werte hat, verwenden Sie stattdessen sint64. |
| uint32 | Verwendet variable Längen-Kodierung. |
| uint64 | Verwendet variable Längen-Kodierung. |
| sint32 | Verwendet variable Längen-Kodierung. Signed-Int-Wert. Diese kodieren negative Zahlen effizienter als normale int32s. |
| sint64 | Verwendet variable Längen-Kodierung. Signed-Int-Wert. Diese kodieren negative Zahlen effizienter als normale int64s. |
| fixed32 | Immer vier Bytes. Effizienter als uint32, wenn Werte oft größer als 228 sind. |
| fixed64 | Immer acht Bytes. Effizienter als uint64, wenn Werte oft größer als 256 sind. |
| sfixed32 | Immer vier Bytes. |
| sfixed64 | Immer acht Bytes. |
| bool | |
| string | Eine Zeichenkette muss immer UTF-8-kodierten oder 7-Bit-ASCII-Text enthalten und darf nicht länger als 232 sein. |
| bytes | Kann jede beliebige Byte-Sequenz enthalten, die nicht länger als 232 ist. |
| Proto-Typ | C++-Typ | Java/Kotlin-Typ[1] | Python-Typ[3] | Go-Typ | Ruby-Typ | C#-Typ | PHP-Typ | Dart-Typ | Rust-Typ |
|---|---|---|---|---|---|---|---|---|---|
| double | double | double | float | float64 | Float | double | float | double | f64 |
| float | float | float | float | float32 | Float | float | float | double | f32 |
| int32 | int32_t | int | int | int32 | Fixnum oder Bignum (je nach Bedarf) | int | integer | int | i32 |
| int64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| uint32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum oder Bignum (je nach Bedarf) | uint | integer | int | u32 |
| uint64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
| sint32 | int32_t | int | int | int32 | Fixnum oder Bignum (je nach Bedarf) | int | integer | int | i32 |
| sint64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| fixed32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum oder Bignum (je nach Bedarf) | uint | integer | int | u32 |
| fixed64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
| sfixed32 | int32_t | int | int | int32 | Fixnum oder Bignum (je nach Bedarf) | int | integer | int | i32 |
| sfixed64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | bool |
| string | string | String | str/unicode[5] | string | String (UTF-8) | string | string | String | ProtoString |
| bytes | string | ByteString | str (Python 2), bytes (Python 3) | []byte | String (ASCII-8BIT) | ByteString | string | List | ProtoBytes |
[1] Kotlin verwendet die entsprechenden Typen von Java, auch für vorzeichenlose Typen, um die Kompatibilität in gemischten Java/Kotlin-Codebasen zu gewährleisten.
[2] In Java werden vorzeichenlose 32-Bit- und 64-Bit-Integer mit ihren vorzeichenbehafteten Gegenstücken dargestellt, wobei das höchste Bit einfach im Vorzeichenbit gespeichert wird.
[3] In allen Fällen führt das Setzen von Werten auf ein Feld eine Typenprüfung durch, um sicherzustellen, dass es gültig ist.
[4] 64-Bit- oder vorzeichenlose 32-Bit-Integer werden beim Dekodieren immer als long dargestellt, können aber int sein, wenn beim Setzen des Feldes ein int angegeben wird. In allen Fällen muss der Wert beim Setzen in den dargestellten Typ passen. Siehe [2].
[5] Python-Zeichenketten werden bei der Dekodierung als Unicode dargestellt, können aber str sein, wenn eine ASCII-Zeichenkette angegeben wird (dies kann sich ändern).
[6] Integer wird auf 64-Bit-Maschinen verwendet und String auf 32-Bit-Maschinen.
Wie diese Typen beim Serialisieren Ihrer Nachricht kodiert werden, erfahren Sie unter Protocol Buffer-Kodierung.
Standard-Feldwerte
Wenn eine Nachricht geparst wird und die kodierten Nachrichtendaten kein bestimmtes Feld enthalten, gibt der Zugriff auf dieses Feld im geparsten Objekt den Standardwert für dieses Feld zurück. Die Standardwerte sind typspezifisch:
- Für Zeichenketten ist der Standardwert die leere Zeichenkette.
- Für Bytes ist der Standardwert ein leeres Byte-Array.
- Für Boolesche Werte ist der Standardwert false.
- Für numerische Typen ist der Standardwert Null.
- Bei Nachrichtenfeldern ist das Feld nicht gesetzt. Sein genauer Wert hängt von der Sprache ab. Weitere Details finden Sie im generierten Code-Leitfaden.
- Für Aufzählungen ist der Standardwert der erste definierte Aufzählungswert, der 0 sein muss. Siehe Standardwert für Enum.
Der Standardwert für wiederholte Felder ist leer (im Allgemeinen eine leere Liste in der entsprechenden Sprache).
Der Standardwert für Map-Felder ist leer (im Allgemeinen eine leere Map in der entsprechenden Sprache).
Überschreiben von Standard-Skalarwerten
In Protobuf-Editionen können Sie explizite Standardwerte für singuläre Nicht-Nachrichtenfelder angeben. Angenommen, Sie möchten beispielsweise einen Standardwert von 10 für das Feld SearchRequest.result_per_page angeben:
int32 result_per_page = 3 [default = 10];
Wenn der Absender result_per_page nicht angibt, beobachtet der Empfänger den folgenden Zustand:
- Das Feld result_per_page ist nicht vorhanden. Das heißt, die Methode
has_result_per_page()(Hazzer-Methode) würdefalsezurückgeben. - Der Wert von
result_per_page(zurückgegeben vom „Getter“) ist10.
Wenn der Absender einen Wert für result_per_page sendet, wird der Standardwert von 10 ignoriert und der Wert des Absenders wird vom „Getter“ zurückgegeben.
Weitere Details zur Funktionsweise von Standardwerten im generierten Code finden Sie im generierten Code-Leitfaden für Ihre gewählte Sprache.
Explizite Standardwerte können nicht für Felder angegeben werden, bei denen das field_presence-Feature auf IMPLICIT gesetzt ist.
Aufzählungen (Enums)
Wenn Sie einen Nachrichtentyp definieren, möchten Sie vielleicht, dass eines seiner Felder nur einen von einer vordefinierten Liste von Werten haben kann. Angenommen, Sie möchten ein Feld corpus für jede SearchRequest hinzufügen, wobei der Korpus UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS oder VIDEO sein kann. Dies können Sie sehr einfach tun, indem Sie Ihrer Nachrichtendefinition ein enum mit einer Konstanten für jeden möglichen Wert hinzufügen.
Im folgenden Beispiel haben wir ein enum namens Corpus mit allen möglichen Werten und ein Feld vom Typ Corpus hinzugefügt.
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
Corpus corpus = 4;
}
Standardwert für Enum
Der Standardwert für das Feld SearchRequest.corpus ist CORPUS_UNSPECIFIED, da dies der erste in der Enum definierte Wert ist.
In Edition 2023 muss der erste in einer Enum-Definition definierte Wert den Wert Null haben und sollte den Namen ENUM_TYPE_NAME_UNSPECIFIED oder ENUM_TYPE_NAME_UNKNOWN haben. Dies liegt daran:
- Der Nullwert muss das erste Element sein, um mit den Semantiken von proto2 kompatibel zu sein, bei denen der erste Enum-Wert der Standardwert ist, es sei denn, ein anderer Wert wird explizit angegeben.
- Es muss einen Nullwert geben, um mit den Semantiken von proto3 kompatibel zu sein, bei denen der Nullwert als Standardwert für alle implizit-präsenten Felder verwendet wird, die diesen Enum-Typ verwenden.
Es wird außerdem empfohlen, dass dieser erste Standardwert keine semantische Bedeutung hat, außer „dieser Wert wurde nicht angegeben“.
Der Standardwert für ein Enum-Feld wie das Feld SearchRequest.corpus kann wie folgt explizit überschrieben werden:
Corpus corpus = 4 [default = CORPUS_UNIVERSAL];
Wenn ein Enum-Typ von proto2 mit option features.enum_type = CLOSED; migriert wurde, gibt es keine Einschränkung für den ersten Wert im Enum. Es wird nicht empfohlen, den ersten Wert dieser Art von Enums zu ändern, da dies den Standardwert für jedes Feld ändert, das diesen Enum-Typ ohne einen expliziten Feldstandard verwendet.
Enum-Wert-Aliase
Sie können Aliase definieren, indem Sie denselben Wert verschiedenen Enum-Konstanten zuweisen. Dazu müssen Sie die Option allow_alias auf true setzen. Andernfalls generiert der Protocol Buffer-Compiler eine Warnmeldung, wenn Aliase gefunden werden. Obwohl alle Alias-Werte für die Serialisierung gültig sind, wird nur der erste Wert beim Deserialisieren verwendet.
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // Uncommenting this line will cause a warning message.
ENAA_FINISHED = 2;
}
Enum-Konstanten
Enumeratoren-Konstanten müssen im Bereich einer 32-Bit-Ganzzahl liegen. Da enum-Werte Varint-Kodierung im Wire-Format verwenden, sind negative Werte ineffizient und daher nicht empfohlen. Sie können enums innerhalb einer Nachrichtendefinition definieren, wie im vorherigen Beispiel, oder außerhalb – diese enums können in jeder Nachrichtendefinition in Ihrer .proto-Datei wiederverwendet werden. Sie können auch einen in einer Nachricht deklarierten enum-Typ als Typ eines Feldes in einer anderen Nachricht verwenden, indem Sie die Syntax _MessageType_._EnumType_ verwenden.
Sprachspezifische Enum-Implementierungen
Wenn Sie den Protocol Buffer-Compiler für eine .proto-Datei ausführen, die ein enum verwendet, generiert der Code für Java, Kotlin oder C++ ein entsprechendes enum oder eine spezielle EnumDescriptor-Klasse für Python, die zur Erstellung einer Reihe von symbolischen Konstanten mit ganzzahligen Werten in der zur Laufzeit generierten Klasse verwendet wird.
Wichtig
Der generierte Code kann sprachspezifischen Einschränkungen hinsichtlich der Anzahl der Enumeratoren unterliegen (einige Tausend für eine Sprache). Überprüfen Sie die Einschränkungen für die von Ihnen zu verwendenden Sprachen.Während der Deserialisierung werden nicht erkannte Enum-Werte in der Nachricht beibehalten, obwohl die Darstellung, wie dies bei der Deserialisierung der Nachricht geschieht, sprachabhängig ist. In Sprachen, die offene Enum-Typen mit Werten außerhalb des Bereichs der angegebenen Symbole unterstützen, wie C++ und Go, wird der unbekannte Enum-Wert einfach als seine zugrunde liegende Ganzzahlendarstellung gespeichert. In Sprachen mit geschlossenen Enum-Typen wie Java wird ein Fall im Enum verwendet, um einen nicht erkannten Wert darzustellen, und die zugrunde liegende Ganzzahl kann mit speziellen Accessoren abgerufen werden. In beiden Fällen wird der nicht erkannte Wert immer noch mit der Nachricht serialisiert, wenn die Nachricht serialisiert wird.
Wichtig
Informationen darüber, wie Enums funktionieren sollten, im Gegensatz dazu, wie sie derzeit in verschiedenen Sprachen funktionieren, finden Sie unter Enum-Verhalten.Weitere Informationen zur Arbeit mit Nachrichten-enums in Ihren Anwendungen finden Sie im generierten Code-Leitfaden für Ihre gewählte Sprache.
Reservierte Werte
Wenn Sie einen Enum-Typ aktualisieren, indem Sie einen Enum-Eintrag vollständig entfernen oder auskommentieren, können zukünftige Benutzer den numerischen Wert bei eigenen Aktualisierungen des Typs wiederverwenden. Dies kann zu schwerwiegenden Problemen führen, wenn sie später alte Instanzen derselben .proto laden, einschließlich Datenbeschädigung, Datenschutzfehlern usw. Eine Möglichkeit, dies zu verhindern, besteht darin, anzugeben, dass die numerischen Werte (und/oder Namen, die ebenfalls Probleme bei der JSON-Serialisierung verursachen können) Ihrer gelöschten Einträge reserved sind. Der Protocol Buffer-Compiler wird sich beschweren, wenn zukünftige Benutzer diese Bezeichner verwenden. Sie können angeben, dass Ihr reservierter numerischer Wertebereich mit dem Schlüsselwort max bis zum maximal möglichen Wert reicht.
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved FOO, BAR;
}
Beachten Sie, dass Sie Feldnamen und numerische Werte nicht in derselben reserved-Anweisung mischen können.
Verwenden anderer Nachrichtentypen
Sie können andere Nachrichtentypen als Feldtypen verwenden. Angenommen, Sie möchten beispielsweise Result-Nachrichten in jeder SearchResponse-Nachricht enthalten – dazu können Sie einen Result-Nachrichtentyp in derselben .proto-Datei definieren und dann ein Feld vom Typ Result in SearchResponse angeben.
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
Importieren von Definitionen
Im vorherigen Beispiel ist der Result-Nachrichtentyp in derselben Datei wie SearchResponse definiert – was ist, wenn der Nachrichtentyp, den Sie als Feldtyp verwenden möchten, bereits in einer anderen .proto-Datei definiert ist?
Sie können Definitionen aus anderen .proto-Dateien verwenden, indem Sie sie importieren. Um Definitionen aus anderen .proto-Dateien zu importieren, fügen Sie eine Importanweisung am Anfang Ihrer Datei hinzu.
import "myproject/other_protos.proto";
Ab Edition 2024 können Sie auch import option verwenden, um benutzerdefinierte Optionsdefinitionen aus anderen .proto-Dateien zu verwenden. Im Gegensatz zu regulären Imports erlaubt dies nur die Verwendung von benutzerdefinierten Optionsdefinitionen, aber keine anderen Nachrichten- oder Enum-Definitionen, um Abhängigkeiten im generierten Code zu vermeiden.
import option "myproject/other_protos.proto";
Standardmäßig können Sie nur Definitionen aus direkt importierten .proto-Dateien verwenden. Manchmal müssen Sie jedoch eine .proto-Datei an einen neuen Ort verschieben. Anstatt die .proto-Datei direkt zu verschieben und alle Aufrufe in einer einzigen Änderung zu aktualisieren, können Sie eine Platzhalter-.proto-Datei am alten Speicherort platzieren, um alle Importe mithilfe des Konzepts import public an den neuen Speicherort weiterzuleiten.
Beachten Sie, dass die Funktionalität für öffentliche Imports nicht in Java, Kotlin, TypeScript, JavaScript, GCL sowie in C++-Zielen, die Protobuf-statische Reflexion verwenden, verfügbar ist.
import public-Abhängigkeiten können von jedem Code, der das Proto mit der import public-Anweisung importiert, transitiv genutzt werden. Zum Beispiel:
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
Der Protokollcompiler sucht importierte Dateien in einem Satz von Verzeichnissen, die auf der Protokollcompiler-Befehlszeile mit dem Flag -I/--proto_path angegeben sind. Wenn kein Flag angegeben wurde, sucht er im Verzeichnis, in dem der Compiler aufgerufen wurde. Im Allgemeinen sollten Sie das Flag --proto_path auf den Stamm Ihres Projekts setzen und vollqualifizierte Namen für alle Importe verwenden.
Symbol-Sichtbarkeit
Die Sichtbarkeit, welche Symbole beim Importieren durch andere protos verfügbar oder nicht verfügbar sind, wird durch das Feature features.default_symbol_visibility und die Schlüsselwörter export und local gesteuert, die in Edition 2024 hinzugefügt wurden.
Nur Symbole, die entweder über die Standard-Symbol-Sichtbarkeit oder mit einem export-Schlüsselwort exportiert werden, können von der importierenden Datei referenziert werden.
Verwenden von proto2- und proto3-Nachrichtentypen
Es ist möglich, proto2- und proto3-Nachrichtentypen zu importieren und sie in Ihren Editionen-Nachrichten zu verwenden und umgekehrt.
Verschachtelte Typen
Sie können Nachrichtentypen innerhalb anderer Nachrichtentypen definieren und verwenden, wie im folgenden Beispiel – hier wird die Nachricht Result innerhalb der Nachricht SearchResponse definiert
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
Wenn Sie diesen Nachrichtentyp außerhalb seines übergeordneten Nachrichtentyps wiederverwenden möchten, verweisen Sie darauf als _Elternteil_._Typ_
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
Sie können Nachrichten beliebig tief verschachteln. Beachten Sie im folgenden Beispiel, dass die beiden verschachtelten Typen namens Inner völlig unabhängig voneinander sind, da sie innerhalb verschiedener Nachrichten definiert sind
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
Aktualisieren eines Nachrichtentyps
Wenn ein bestehender Nachrichtentyp nicht mehr all Ihren Anforderungen entspricht – Sie möchten beispielsweise, dass das Nachrichtenformat ein zusätzliches Feld hat –, aber Sie möchten immer noch Code verwenden, der mit dem alten Format erstellt wurde, machen Sie sich keine Sorgen! Es ist sehr einfach, Nachrichtentypen zu aktualisieren, ohne Ihren bestehenden Code zu beeinträchtigen, wenn Sie das binäre Wire-Format verwenden.
Hinweis
Wenn Sie ProtoJSON oder Proto-Textformat zum Speichern Ihrer Protokollpuffer-Nachrichten verwenden, sind die Änderungen, die Sie in Ihrer Proto-Definition vornehmen können, unterschiedlich. Die sicheren Änderungen des ProtoJSON-Wire-Formats sind hier beschrieben.Prüfen Sie Proto-Best Practices und die folgenden Regeln
Binärdraht-unsichere Änderungen
Wire-unsichere Änderungen sind Schemaänderungen, die fehlschlagen, wenn Sie Daten parsen, die mit dem alten Schema serialisiert wurden, mit einem Parser, der das neue Schema verwendet (oder umgekehrt). Nehmen Sie nur Wire-unsichere Änderungen vor, wenn Sie wissen, dass alle Serialisierer und Deserialisierer der Daten das neue Schema verwenden.
- Das Ändern von Feldnummern für vorhandene Felder ist nicht sicher.
- Das Ändern der Feldnummer entspricht dem Löschen des Feldes und dem Hinzufügen eines neuen Feldes mit demselben Typ. Wenn Sie ein Feld neu nummerieren möchten, lesen Sie die Anweisungen zum Löschen eines Feldes.
- Das Verschieben von Feldern in eine vorhandene
oneofist nicht sicher.
Binärdraht-sichere Änderungen
Wire-sichere Änderungen sind solche, bei denen es völlig sicher ist, das Schema auf diese Weise zu entwickeln, ohne Risiko von Datenverlust oder neuen Parsing-Fehlern.
Beachten Sie, dass alle Wire-sicheren Änderungen eine breaking change für den Anwendungscode in einer bestimmten Sprache darstellen können. Beispielsweise wäre das Hinzufügen eines Wertes zu einem bereits vorhandenen Enum ein Kompilierungsfehler für jeden Code mit einem erschöpfenden Switch auf diesem Enum. Aus diesem Grund vermeidet Google möglicherweise einige dieser Änderungen an öffentlichen Nachrichten: Die AIPs enthalten Anleitungen, welche dieser Änderungen dort sicher vorgenommen werden können.
- Das Hinzufügen neuer Felder ist sicher.
- Wenn Sie neue Felder hinzufügen, können alle von Code serialisierten Nachrichten, die Ihr „altes“ Nachrichtenformat verwenden, immer noch von Ihrem neuen generierten Code geparst werden. Sie sollten die Standardwerte für diese Elemente im Auge behalten, damit neuer Code ordnungsgemäß mit von altem Code generierten Nachrichten interagieren kann. Ebenso können von Ihrem neuen Code erstellte Nachrichten von Ihrem alten Code geparst werden: Alte Binärdateien ignorieren neue Felder beim Parsen einfach. Weitere Details finden Sie im Abschnitt Unbekannte Felder.
- Das Entfernen von Feldern ist sicher.
- Derselbe Feldnummer darf in Ihrem aktualisierten Nachrichtentyp nicht wiederverwendet werden. Möglicherweise möchten Sie das Feld stattdessen umbenennen, vielleicht mit dem Präfix „OBSOLETE_“, oder die Feldnummer reservieren, damit zukünftige Benutzer Ihres
.protodie Nummer nicht versehentlich wiederverwenden können.
- Derselbe Feldnummer darf in Ihrem aktualisierten Nachrichtentyp nicht wiederverwendet werden. Möglicherweise möchten Sie das Feld stattdessen umbenennen, vielleicht mit dem Präfix „OBSOLETE_“, oder die Feldnummer reservieren, damit zukünftige Benutzer Ihres
- Das Hinzufügen zusätzlicher Werte zu einem Enum ist sicher.
- Das Ändern eines einzelnen expliziten Präsenzfeldes oder einer Erweiterung in ein Mitglied einer **neuen**
oneofist sicher. - Das Ändern einer
oneof, die nur ein Feld enthält, in ein explizites Präsenzfeld ist sicher. - Das Ändern eines Feldes in eine Erweiterung mit derselben Nummer und demselben Typ ist sicher.
Binärdraht-kompatible Änderungen (bedingt sicher)
Im Gegensatz zu Wire-sicheren Änderungen bedeutet Wire-kompatibel, dass dieselben Daten sowohl vor als auch nach einer gegebenen Änderung geparst werden können. Das Parsen der Daten kann jedoch bei dieser Art von Änderung verlustbehaftet sein. Beispielsweise ist die Änderung eines int32 in ein int64 eine kompatible Änderung, aber wenn ein Wert größer als INT32_MAX geschrieben wird, verwirft ein Client, der ihn als int32 liest, die höherwertigen Bits der Zahl.
Sie können kompatible Änderungen an Ihrem Schema nur vornehmen, wenn Sie die Einführung in Ihrem System sorgfältig verwalten. Sie können beispielsweise ein int32 in ein int64 ändern, aber sicherstellen, dass Sie weiterhin nur gültige int32-Werte schreiben, bis das neue Schema auf alle Endpunkte angewendet wurde, und dann anschließend größere Werte schreiben.
Wenn Ihr Schema außerhalb Ihrer Organisation veröffentlicht wird, sollten Sie im Allgemeinen keine Wire-kompatiblen Änderungen vornehmen, da Sie die Einführung des neuen Schemas nicht verwalten können, um zu wissen, wann der unterschiedliche Wertebereich sicher verwendet werden kann.
int32,uint32,int64,uint64undboolsind alle kompatibel.- Wenn eine Zahl von der Wire gelesen wird, die nicht in den entsprechenden Typ passt, erhalten Sie denselben Effekt, als ob Sie die Zahl in C++ in diesen Typ umgewandelt hätten (z. B. wenn eine 64-Bit-Zahl als int32 gelesen wird, wird sie auf 32 Bit abgeschnitten).
sint32undsint64sind miteinander kompatibel, aber **nicht** mit den anderen Ganzzahltypen.- Wenn der geschriebene Wert zwischen INT_MIN und INT_MAX (einschließlich) lag, wird er mit beiden Typen als derselbe Wert geparst. Wenn ein sint64-Wert außerhalb dieses Bereichs geschrieben und als sint32 geparst wurde, wird der Varint auf 32 Bit abgeschnitten und dann die Zickzack-Dekodierung durchgeführt (was zu einem anderen beobachteten Wert führt).
stringundbytessind kompatibel, solange die Bytes gültiges UTF-8 sind.- Eingebettete Nachrichten sind mit
byteskompatibel, wenn die Bytes eine kodierte Instanz der Nachricht enthalten. fixed32ist mitsfixed32undfixed64mitsfixed64kompatibel.- Für
string,bytesund Nachrichtenfelder ist singulär mitrepeatedkompatibel.- Bei serialisierten Daten eines wiederholten Feldes als Eingabe nehmen Clients, die dieses Feld als singulär erwarten, den letzten Eingabewert, wenn es sich um ein primitives Feld handelt, oder zusammenführen alle Eingabeelemente, wenn es sich um ein Nachrichtenfeld handelt. Beachten Sie, dass dies für numerische Typen, einschließlich Boolescher Werte und Enums, **nicht** generell sicher ist. Wiederholte Felder von numerischen Typen werden standardmäßig im packed-Format serialisiert, das nicht korrekt geparst wird, wenn ein singuläres Feld erwartet wird.
enumist kompatibel mitint32,uint32,int64unduint64- Beachten Sie, dass Client-Code sie möglicherweise unterschiedlich behandelt, wenn die Nachricht deserialisiert wird: Beispielsweise werden nicht erkannte Proto3-
enum-Werte in der Nachricht beibehalten, aber wie dies bei der Deserialisierung der Nachricht dargestellt wird, ist sprachabhängig.
- Beachten Sie, dass Client-Code sie möglicherweise unterschiedlich behandelt, wenn die Nachricht deserialisiert wird: Beispielsweise werden nicht erkannte Proto3-
- Das Ändern eines Feldes von einer
map<K, V>zu dem entsprechendenrepeated-Nachrichtenfeld ist binär kompatibel (siehe Maps, unten, für das Nachrichtenlayout und andere Einschränkungen).- Die Sicherheit der Änderung hängt jedoch von der Anwendung ab: Beim Deserialisieren und erneuten Serialisieren einer Nachricht erzeugen Clients, die die
repeated-Felddefinition verwenden, ein semantisch identisches Ergebnis; Clients, die diemap-Felddefinition verwenden, können jedoch Einträge neu anordnen und Einträge mit doppelten Schlüsseln verwerfen.
- Die Sicherheit der Änderung hängt jedoch von der Anwendung ab: Beim Deserialisieren und erneuten Serialisieren einer Nachricht erzeugen Clients, die die
Unbekannte Felder
Unbekannte Felder sind wohlgeformte Protokollpuffer-serialisierte Daten, die Felder darstellen, die der Parser nicht erkennt. Wenn beispielsweise ein altes Binärformat Daten parst, die von einem neuen Binärformat mit neuen Feldern gesendet wurden, werden diese neuen Felder im alten Binärformat zu unbekannten Feldern.
Editionsnachrichten behalten unbekannte Felder und schließen sie beim Parsen und in der serialisierten Ausgabe ein, was dem Verhalten von Proto2 und Proto3 entspricht.
Beibehalten unbekannter Felder
Einige Aktionen können dazu führen, dass unbekannte Felder verloren gehen. Wenn Sie beispielsweise eine der folgenden Aktionen ausführen, gehen unbekannte Felder verloren
- Serialisieren eines Proto in JSON.
- Iterieren über alle Felder in einer Nachricht, um eine neue Nachricht zu füllen.
Um den Verlust unbekannter Felder zu vermeiden, gehen Sie wie folgt vor
- Verwenden Sie binär; vermeiden Sie die Verwendung von Textformaten für den Datenaustausch.
- Verwenden Sie nachrichtenorientierte APIs wie
CopyFrom()undMergeFrom(), um Daten zu kopieren, anstatt Feld für Feld zu kopieren
TextFormat ist ein Sonderfall. Das Serialisieren in TextFormat gibt unbekannte Felder mit ihren Feldnummern aus. Das Parsen von TextFormat-Daten zurück in ein Binär-Proto schlägt jedoch fehl, wenn Einträge mit Feldnummern vorhanden sind.
Erweiterungen (Extensions)
Eine Erweiterung ist ein Feld, das außerhalb seiner Container-Nachricht definiert ist; normalerweise in einer .proto-Datei, die von der .proto-Datei der Container-Nachricht getrennt ist.
Warum Erweiterungen verwenden?
Es gibt zwei Hauptgründe für die Verwendung von Erweiterungen
- Die
.proto-Datei der Container-Nachricht hat weniger Importe/Abhängigkeiten. Dies kann die Build-Zeiten verbessern, zirkuläre Abhängigkeiten auflösen und ansonsten lose Kopplung fördern. Erweiterungen sind dafür sehr gut geeignet. - Ermöglichen Sie Systemen, Daten an eine Container-Nachricht mit minimaler Abhängigkeit und Koordination anzuhängen. Erweiterungen sind dafür aufgrund des begrenzten Feldnummernbereichs und der Folgen der Wiederverwendung von Feldnummern keine gute Lösung. Wenn Ihr Anwendungsfall eine sehr geringe Koordination für eine große Anzahl von Erweiterungen erfordert, sollten Sie stattdessen den
Any-Nachrichtentyp verwenden.
Beispielerweiterung
Die Verwendung einer Erweiterung ist ein zweistufiger Prozess. Zuerst müssen Sie in der Nachricht, die Sie erweitern möchten (dem „Container“), einen Bereich von Feldnummern für Erweiterungen reservieren. Dann definieren Sie in einer separaten Datei das Erweiterungsfeld selbst.
Das folgende Beispiel zeigt, wie eine Erweiterung für Kitten-Videos zu einer generischen UserContent-Nachricht hinzugefügt wird.
Schritt 1: Reservieren Sie einen Erweiterungsbereich in der Container-Nachricht.
Die Container-Nachricht muss das Schlüsselwort extensions verwenden, um einen Bereich von Feldnummern für andere zur Verwendung zu reservieren. Es ist Best Practice, auch eine declaration für die spezifische Erweiterung hinzuzufügen, die Sie hinzufügen möchten. Diese Deklaration fungiert als Vorwärtsdeklaration, was es Entwicklern erleichtert, Erweiterungen zu finden und die Wiederverwendung von Feldnummern zu vermeiden.
// media/user_content.proto
edition = "2023";
package media;
// A container for user-created content.
message UserContent {
extensions 100 to 199 [
declaration = {
number: 126,
full_name: ".kittens.kitten_videos",
type: ".kittens.Video",
repeated: true
}
];
}
Diese Deklaration gibt die Feldnummer, den vollständigen Namen, den Typ und die Kardinalität der Erweiterung an, die an anderer Stelle definiert wird.
Schritt 2: Definieren Sie die Erweiterung in einer separaten Datei.
Die Erweiterung selbst wird in einer anderen .proto-Datei definiert, die sich normalerweise auf ein bestimmtes Feature (wie Kitten-Videos) konzentriert. Dies vermeidet, dass von dem generischen Container eine Abhängigkeit zu dem spezifischen Feature entsteht.
// kittens/video_ext.proto
edition = "2023";
import "media/user_content.proto"; // Imports the container message
import "kittens/video.proto"; // Imports the extension's message type
package kittens;
// This defines the extension field.
extend media.UserContent {
repeated Video kitten_videos = 126;
}
Der extend-Block verbindet das neue Feld kitten_videos zurück mit der Nachricht media.UserContent, wobei die Feldnummer 126 verwendet wird, die im Container reserviert wurde.
Es gibt keinen Unterschied im Wire-Format-Encoding von Erweiterungsfeldern im Vergleich zu einem Standardfeld mit derselben Feldnummer, demselben Typ und derselben Kardinalität. Daher ist es sicher, ein Standardfeld aus seinem Container in eine Erweiterung zu verschieben oder ein Erweiterungsfeld in seine Container-Nachricht als Standardfeld zu verschieben, solange Feldnummer, Typ und Kardinalität konstant bleiben.
Da Erweiterungen jedoch außerhalb der Container-Nachricht definiert werden, werden keine spezialisierten Accessoren generiert, um bestimmte Erweiterungsfelder abzurufen und festzulegen. Für unser Beispiel wird der Protobuf-Compiler keine AddKittenVideos()- oder GetKittenVideos()-Accessoren **generieren**. Stattdessen werden Erweiterungen über parametrisierte Funktionen wie HasExtension(), ClearExtension(), GetExtension(), MutableExtension() und AddExtension() aufgerufen.
In C++ würde es ungefähr so aussehen
UserContent user_content;
user_content.AddExtension(kittens::kitten_videos, new kittens::Video());
assert(1 == user_content.GetRepeatedExtension(kittens::kitten_videos).size());
user_content.GetRepeatedExtension(kittens::kitten_videos)[0];
Definieren von Erweiterungsbereichen
Wenn Sie der Besitzer einer Container-Nachricht sind, müssen Sie einen Erweiterungsbereich für die Erweiterungen Ihrer Nachricht definieren.
Feldnummern, die Erweiterungsfeldern zugewiesen sind, können nicht für Standardfelder wiederverwendet werden.
Es ist sicher, einen Erweiterungsbereich nach seiner Definition zu erweitern. Eine gute Standardeinstellung ist die Zuweisung von 1000 relativ kleinen Nummern und die dichte Besetzung dieses Raums mithilfe von Erweiterungsdeklarationen
message ModernExtendableMessage {
// All extensions in this range should use extension declarations.
extensions 1000 to 2000 [verification = DECLARATION];
}
Beim Hinzufügen eines Bereichs für Erweiterungsdeklarationen vor den eigentlichen Erweiterungen sollten Sie verification = DECLARATION hinzufügen, um zu erzwingen, dass Deklarationen für diesen neuen Bereich verwendet werden. Dieser Platzhalter kann entfernt werden, sobald eine tatsächliche Deklaration hinzugefügt wurde.
Es ist sicher, einen vorhandenen Erweiterungsbereich in separate Bereiche aufzuteilen, die denselben Gesamtbereich abdecken. Dies kann für die Migration eines Legacy-Nachrichtentyps zu Erweiterungsdeklarationen erforderlich sein. Vor der Migration könnte der Bereich beispielsweise wie folgt definiert sein
message LegacyMessage {
extensions 1000 to max;
}
Und nach der Migration (Aufteilung des Bereichs) kann er so aussehen
message LegacyMessage {
// Legacy range that was using an unverified allocation scheme.
extensions 1000 to 524999999 [verification = UNVERIFIED];
// Current range that uses extension declarations.
extensions 525000000 to max [verification = DECLARATION];
}
Es ist nicht sicher, die Startfeldnummer zu erhöhen oder die Endfeldnummer zu verringern, um einen Erweiterungsbereich zu verschieben oder zu verkleinern. Diese Änderungen können eine bestehende Erweiterung ungültig machen.
Bevorzugen Sie Feldnummern von 1 bis 15 für Standardfelder, die in den meisten Instanzen Ihres Proto gefüllt sind. Es wird nicht empfohlen, diese Nummern für Erweiterungen zu verwenden.
Wenn Ihre Nummerierungsübereinkunft Erweiterungen mit sehr großen Feldnummern beinhalten könnte, können Sie angeben, dass Ihr Erweiterungsbereich bis zur maximal möglichen Feldnummer reicht, indem Sie das Schlüsselwort max verwenden
message Foo {
extensions 1000 to max;
}
max ist 229 - 1 oder 536.870.911.
Auswählen von Erweiterungsnummern
Erweiterungen sind einfach Felder, die außerhalb ihrer Container-Nachrichten angegeben werden können. Alle Regeln für die Zuweisung von Feldnummern gelten auch für Erweiterungsfeldnummern. Dieselben Folgen der Wiederverwendung von Feldnummern gelten auch für die Wiederverwendung von Erweiterungsfeldnummern.
Die Auswahl eindeutiger Erweiterungsfeldnummern ist einfach, wenn die Container-Nachricht Erweiterungsdeklarationen verwendet. Bei der Definition einer neuen Erweiterung wählen Sie die niedrigste Feldnummer oberhalb aller anderen Deklarationen aus dem höchsten in der Container-Nachricht definierten Erweiterungsbereich. Wenn beispielsweise eine Container-Nachricht wie folgt definiert ist
message Container {
// Legacy range that was using an unverified allocation scheme
extensions 1000 to 524999999;
// Current range that uses extension declarations. (highest extension range)
extensions 525000000 to max [
declaration = {
number: 525000001,
full_name: ".bar.baz_ext",
type: ".bar.Baz"
}
// 525,000,002 is the lowest field number above all other declarations
];
}
Die nächste Erweiterung von Container sollte eine neue Deklaration mit der Nummer 525000002 hinzufügen.
Nicht verifizierte Zuweisung von Erweiterungsfeldnummern (nicht empfohlen)
Der Besitzer einer Container-Nachricht kann sich entscheiden, auf Erweiterungsdeklarationen zugunsten einer eigenen, nicht verifizierten Strategie zur Zuweisung von Erweiterungsfeldnummern zu verzichten.
Ein nicht verifiziertes Allokationsschema verwendet einen Mechanismus außerhalb des Protobuf-Ökosystems, um Erweiterungsfeldnummern innerhalb des ausgewählten Erweiterungsbereichs zuzuweisen. Ein Beispiel könnte die Verwendung der Commit-Nummer eines Monorepos sein. Dieses System ist aus Sicht des Protobuf-Compilers „nicht verifiziert“, da es keine Möglichkeit gibt zu überprüfen, ob eine Erweiterung eine ordnungsgemäß erworbene Erweiterungsfeldnummer verwendet.
Der Vorteil eines nicht verifizierten Systems gegenüber einem verifizierten System wie Erweiterungsdeklarationen ist die Möglichkeit, eine Erweiterung zu definieren, ohne mit dem Besitzer der Container-Nachricht zu koordinieren.
Der Nachteil eines nicht verifizierten Systems ist, dass der Protobuf-Compiler die Teilnehmer nicht vor der Wiederverwendung von Erweiterungsfeldnummern schützen kann.
Nicht verifizierte Strategien zur Zuweisung von Erweiterungsfeldnummern werden nicht empfohlen, da die Folgen der Wiederverwendung von Feldnummern für alle Erweiterer einer Nachricht gelten (nicht nur für den Entwickler, der die Empfehlungen nicht befolgt hat). Wenn Ihr Anwendungsfall eine sehr geringe Koordination erfordert, sollten Sie stattdessen die Any-Nachricht verwenden.
Nicht verifizierte Strategien zur Zuweisung von Erweiterungsfeldnummern sind auf den Bereich von 1 bis 524.999.999 beschränkt. Feldnummern ab 525.000.000 können nur mit Erweiterungsdeklarationen verwendet werden.
Festlegen von Erweiterungstypen
Erweiterungen können jeden Feldtyp haben, außer oneofs und maps.
Verschachtelte Erweiterungen (nicht empfohlen)
Sie können Erweiterungen im Geltungsbereich einer anderen Nachricht deklarieren
import "common/user_profile.proto";
package puppies;
message Photo {
extend common.UserProfile {
int32 likes_count = 111;
}
...
}
In diesem Fall ist der C++-Code zum Zugriff auf diese Erweiterung
UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);
Mit anderen Worten, der einzige Effekt ist, dass likes_count im Geltungsbereich von puppies.Photo definiert ist.
Dies ist eine häufige Quelle der Verwirrung: Das Deklarieren eines extend-Blocks, der in einen Nachrichtentyp verschachtelt ist, impliziert **nicht** irgendeine Beziehung zwischen dem äußeren Typ und dem erweiterten Typ. Insbesondere bedeutet das frühere Beispiel **nicht**, dass Photo irgendeine Art von Unterklasse von UserProfile ist. Es bedeutet lediglich, dass das Symbol likes_count im Geltungsbereich von Photo deklariert ist; es ist einfach ein statisches Mitglied.
Ein gängiges Muster ist, Erweiterungen im Geltungsbereich des Feldtyps der Erweiterung zu definieren – zum Beispiel hier eine Erweiterung von media.UserContent vom Typ puppies.Photo, wobei die Erweiterung als Teil von Photo definiert ist
import "media/user_content.proto";
package puppies;
message Photo {
extend media.UserContent {
Photo puppy_photo = 127;
}
...
}
Es gibt jedoch keine Verpflichtung, eine Erweiterung mit einem Nachrichtentyp innerhalb dieses Typs zu definieren. Sie können auch das Standarddefinitionsmuster verwenden
import "media/user_content.proto";
package puppies;
message Photo {
...
}
// This can even be in a different file.
extend media.UserContent {
Photo puppy_photo = 127;
}
Diese **Standard-Syntax (auf Dateiebene) wird bevorzugt**, um Verwechslungen zu vermeiden. Die verschachtelte Syntax wird von Benutzern, die mit Erweiterungen noch nicht vertraut sind, oft fälschlicherweise als Vererbung missverstanden.
Any
Der Any-Nachrichtentyp ermöglicht es Ihnen, Nachrichten als eingebettete Typen zu verwenden, ohne deren .proto-Definition zu haben. Eine Any enthält eine beliebige serialisierte Nachricht als bytes, zusammen mit einer URL, die als global eindeutiger Identifikator für die Nachricht dient und auf deren Typ verweist. Um den Typ Any zu verwenden, müssen Sie google/protobuf/any.proto importieren.
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
Die Standard-Typ-URL für einen gegebenen Nachrichtentyp ist type.googleapis.com/_packagename_._messagename_.
Verschiedene Sprachimplementierungen unterstützen Laufzeitbibliothekshelfer zum packen und entpacken von Any-Werten auf typsichere Weise – z. B. hat der Any-Typ in Java spezielle pack()- und unpack()-Accessoren, während es in C++ PackFrom()- und UnpackTo()-Methoden gibt
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
Wenn Sie enthaltende Nachrichten auf eine kleine Anzahl von Typen beschränken und eine Genehmigung benötigen, bevor neue Typen zur Liste hinzugefügt werden, sollten Sie stattdessen Erweiterungen mit Erweiterungsdeklarationen anstelle von Any-Nachrichtentypen verwenden.
Oneof
Wenn Sie eine Nachricht mit vielen singulären Feldern haben und höchstens ein Feld gleichzeitig gesetzt sein wird, können Sie dieses Verhalten erzwingen und Speicher sparen, indem Sie die Oneof-Funktion verwenden.
Oneof-Felder sind wie singuläre Felder, außer dass alle Felder in einem Oneof Speicher teilen und höchstens ein Feld gleichzeitig gesetzt sein kann. Das Setzen eines beliebigen Mitglieds des Oneof löscht automatisch alle anderen Mitglieder. Sie können überprüfen, welcher Wert in einem Oneof gesetzt ist (falls vorhanden), indem Sie eine spezielle case()- oder WhichOneof()-Methode verwenden, je nach gewählter Sprache.
Beachten Sie, dass **wenn mehrere Werte gesetzt sind, der zuletzt gesetzte Wert, wie durch die Reihenfolge im Proto bestimmt, alle vorherigen überschreibt**.
Feldnummern für Oneof-Felder müssen innerhalb der umschließenden Nachricht eindeutig sein.
Verwenden von Oneof
Um ein Oneof in Ihrer .proto-Datei zu definieren, verwenden Sie das Schlüsselwort oneof gefolgt von Ihrem Oneof-Namen, in diesem Fall test_oneof
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
Anschließend fügen Sie Ihre Oneof-Felder zur Oneof-Definition hinzu. Sie können Felder jeden Typs hinzufügen, außer map-Felder und repeated-Felder. Wenn Sie ein wiederholtes Feld zu einem Oneof hinzufügen müssen, können Sie eine Nachricht verwenden, die das wiederholte Feld enthält.
In Ihrem generierten Code haben Oneof-Felder dieselben Getter und Setter wie normale Felder. Sie erhalten auch eine spezielle Methode, um zu überprüfen, welcher Wert (falls vorhanden) im Oneof gesetzt ist. Weitere Informationen zur Oneof-API für Ihre gewählte Sprache finden Sie in der entsprechenden API-Referenz.
Oneof-Funktionen
Das Setzen eines Oneof-Feldes löscht automatisch alle anderen Mitglieder des Oneof. Wenn Sie also mehrere Oneof-Felder setzen, behält nur das **letzte** gesetzte Feld seinen Wert.
SampleMessage message; message.set_name("name"); CHECK(message.has_name()); // Calling mutable_sub_message() will clear the name field and will set // sub_message to a new instance of SubMessage with none of its fields set. message.mutable_sub_message(); CHECK(!message.has_name());Wenn der Parser mehrere Mitglieder desselben Oneof auf der Wire findet, wird nur das zuletzt gefundene Mitglied in der geparsten Nachricht verwendet. Beim Parsen von Daten auf der Wire, beginnend am Anfang der Bytes, werten Sie den nächsten Wert aus und wenden die folgenden Parsing-Regeln an
Prüfen Sie zuerst, ob ein **anderes** Feld im selben Oneof derzeit gesetzt ist, und löschen Sie es, falls ja.
Wenden Sie dann die Inhalte so an, als ob das Feld nicht in einem Oneof wäre
- Ein Primitiv überschreibt jeden bereits gesetzten Wert
- Eine Nachricht wird in jeden bereits gesetzten Wert gemergt
Erweiterungen werden für Oneof nicht unterstützt.
Ein Oneof kann nicht
repeatedsein.Reflection-APIs funktionieren für Oneof-Felder.
Wenn Sie ein Oneof-Feld auf seinen Standardwert setzen (z. B. ein int32-Oneof-Feld auf 0 setzen), wird der „Fall“ dieses Oneof-Feldes gesetzt und der Wert auf der Wire serialisiert.
Wenn Sie C++ verwenden, stellen Sie sicher, dass Ihr Code keine Speicherabstürze verursacht. Der folgende Beispielcode wird abstürzen, da
sub_messagedurch den Aufruf der Methodeset_name()bereits gelöscht wurde.SampleMessage message; SubMessage* sub_message = message.mutable_sub_message(); message.set_name("name"); // Will delete sub_message sub_message->set_... // Crashes hereAuch in C++ gilt: Wenn Sie zwei Nachrichten mit Oneofs
Swap(), dann hat jede Nachricht am Ende den Oneof-Fall der anderen: Im folgenden Beispiel hatmsg1einesub_messageundmsg2hat einenname.SampleMessage msg1; msg1.set_name("name"); SampleMessage msg2; msg2.mutable_sub_message(); msg1.swap(&msg2); CHECK(msg1.has_sub_message()); CHECK(msg2.has_name());
Abwärtskompatibilitätsprobleme
Seien Sie vorsichtig beim Hinzufügen oder Entfernen von Oneof-Feldern. Wenn die Überprüfung des Wertes eines Oneof None/NOT_SET zurückgibt, könnte dies bedeuten, dass das Oneof nicht gesetzt wurde oder auf ein Feld in einer anderen Version des Oneof gesetzt wurde. Es gibt keinen Unterschied, da es keine Möglichkeit gibt zu wissen, ob ein unbekanntes Feld auf der Wire ein Mitglied des Oneof ist.
Probleme mit der Wiederverwendung von Tags
- Verschieben von singulären Feldern in oder aus einem Oneof: Sie können einige Ihrer Informationen verlieren (einige Felder werden gelöscht), nachdem die Nachricht serialisiert und geparst wurde. Sie können jedoch sicher ein einzelnes Feld in ein **neues** Oneof verschieben und möglicherweise mehrere Felder verschieben, wenn bekannt ist, dass nur eines jemals gesetzt ist. Weitere Details finden Sie unter Aktualisieren eines Nachrichtentyps.
- Löschen eines Oneof-Feldes und Wiedereinfügen: Dies kann dazu führen, dass Ihr aktuell gesetztes Oneof-Feld nach dem Serialisieren und Parsen der Nachricht gelöscht wird.
- Aufteilen oder Zusammenführen eines Oneof: Dies hat ähnliche Probleme wie das Verschieben singulärer Felder.
Maps (Zuordnungen)
Wenn Sie eine assoziative Zuordnung als Teil Ihrer Datendefinition erstellen möchten, bietet Protobuf eine praktische Kurzschreibweise
map<key_type, value_type> map_field = N;
…wobei der key_type jeder ganzzahlige oder Zeichenfolgentyp sein kann (also jeder skalare Typ außer Gleitkommatypen und bytes). Beachten Sie, dass weder Enum noch Proto-Nachrichten für key_type gültig sind. Der value_type kann jeder Typ sein, außer einer anderen Map.
Wenn Sie beispielsweise eine Map von Projekten erstellen möchten, bei denen jede Project-Nachricht mit einem Zeichenfolgen-Schlüssel verbunden ist, könnten Sie sie wie folgt definieren
map<string, Project> projects = 3;
Map-Funktionen
- Erweiterungen werden für Maps nicht unterstützt.
- Map-Felder können nicht
repeatedsein. - Die Reihenfolge des Wire-Formats und die Iterationsreihenfolge der Map-Werte sind undefiniert, sodass Sie sich nicht auf eine bestimmte Reihenfolge Ihrer Map-Elemente verlassen können.
- Beim Generieren von Textformat für eine
.proto-Datei werden Maps nach Schlüssel sortiert. Numerische Schlüssel werden numerisch sortiert. - Beim Parsen von der Wire oder beim Zusammenführen wird bei doppelten Map-Schlüsseln der zuletzt gesehene Schlüssel verwendet. Beim Parsen einer Map aus dem Textformat kann das Parsen fehlschlagen, wenn doppelte Schlüssel vorhanden sind.
- Wenn Sie einen Schlüssel, aber keinen Wert für ein Map-Feld angeben, ist das Verhalten beim Serialisieren des Feldes sprachabhängig. In C++, Java, Kotlin und Python wird der Standardwert des Typs serialisiert, während in anderen Sprachen nichts serialisiert wird.
- Kein Symbol
FooEntrykann im selben Geltungsbereich wie eine Mapfooexistieren, daFooEntrybereits von der Implementierung der Map verwendet wird.
Die generierte Map-API ist derzeit für alle unterstützten Sprachen verfügbar. Weitere Informationen zur Map-API für Ihre gewählte Sprache finden Sie in der entsprechenden API-Referenz.
Abwärtskompatibilität
Die Map-Syntax ist auf der Wire äquivalent zu Folgendem, sodass Protobuf-Implementierungen, die Maps nicht unterstützen, Ihre Daten immer noch verarbeiten können
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
Jede Protobuf-Implementierung, die Maps unterstützt, muss sowohl Daten produzieren als auch akzeptieren, die von der früheren Definition akzeptiert werden können.
Pakete (Packages)
Sie können eine optionale package-Spezifikation zu einer .proto-Datei hinzufügen, um Namenskonflikte zwischen Protokollnachrichtentypen zu vermeiden.
package foo.bar;
message Open { ... }
Sie können dann die Paket-Spezifikation beim Definieren von Feldern Ihres Nachrichtentyps verwenden
message Foo {
...
foo.bar.Open open = 1;
...
}
Die Art und Weise, wie eine Paket-Spezifikation den generierten Code beeinflusst, hängt von Ihrer gewählten Sprache ab
- In **C++** werden die generierten Klassen in einen C++-Namespace eingeschlossen. Beispielsweise wäre
Openim Namespacefoo::bar. - In **Java** und **Kotlin** wird das Paket als Java-Paket verwendet, es sei denn, Sie geben explizit eine
option java_packagein Ihrer.proto-Datei an. - In **Python** wird die Direktive
packageignoriert, da Python-Module nach ihrem Speicherort im Dateisystem organisiert sind. - In **Go** wird die Direktive
packageignoriert, und die generierte Datei.pb.gobefindet sich im Paket, das nach der entsprechendengo_proto_libraryBazel-Regel benannt ist. Für Open-Source-Projekte **müssen** Sie entweder einego_package-Option angeben oder das Bazel-Flag-Msetzen. - In **Ruby** werden die generierten Klassen in verschachtelte Ruby-Namespaces eingeschlossen, die in den erforderlichen Ruby-Namenskonventionstil (erstes Zeichen groß; wenn das erste Zeichen kein Buchstabe ist, wird
PB_vorangestellt) konvertiert werden. Zum Beispiel wäreOpenim NamespaceFoo::Bar. - In **PHP** wird das Paket als Namespace nach der Konvertierung in PascalCase verwendet, es sei denn, Sie geben explizit eine
option php_namespacein Ihrer.proto-Datei an. Zum Beispiel wäreOpenim NamespaceFoo\Bar. - In **C#** wird das Paket als Namespace nach der Konvertierung in PascalCase verwendet, es sei denn, Sie geben explizit eine
option csharp_namespacein Ihrer.proto-Datei an. Zum Beispiel wäreOpenim NamespaceFoo.Bar.
Beachten Sie, dass, auch wenn die Direktive package den generierten Code nicht direkt beeinflusst (z. B. in Python), es dennoch dringend empfohlen wird, das Paket für die .proto-Datei anzugeben, da dies andernfalls zu Namenskonflikten in Deskriptoren führen und das Proto für andere Sprachen nicht portierbar machen kann.
Pakete und Namensauflösung
Die Typnamensauflösung in der Protokollpuffer-Sprache funktioniert wie in C++: Zuerst wird der innerste Geltungsbereich durchsucht, dann der nächstinnere usw., wobei jedes Paket als „innerhalb“ seines übergeordneten Pakets betrachtet wird. Ein führender Punkt (z. B. .foo.bar.Baz) bedeutet, stattdessen vom äußersten Geltungsbereich aus zu starten.
Der Protokollpuffer-Compiler löst alle Typnamen auf, indem er die importierten .proto-Dateien parst. Der Code-Generator für jede Sprache weiß, wie auf jeden Typ in dieser Sprache verwiesen werden kann, auch wenn er unterschiedliche Geltungsbereichsregeln hat.
Definieren von Diensten
Wenn Sie Ihre Nachrichtentypen mit einem RPC-System (Remote Procedure Call) verwenden möchten, können Sie eine RPC-Dienstschnittstelle in einer .proto-Datei definieren, und der Protokollpuffer-Compiler generiert Dienstschnittstellen-Code und Stubs in Ihrer gewählten Sprache. Wenn Sie beispielsweise einen RPC-Dienst mit einer Methode definieren möchten, die Ihre SearchRequest entgegennimmt und eine SearchResponse zurückgibt, können Sie ihn wie folgt in Ihrer .proto-Datei definieren
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
Das direkteste RPC-System zur Verwendung mit Protokollpuffern ist gRPC: ein sprach- und plattformunabhängiges Open-Source-RPC-System, das bei Google entwickelt wurde. gRPC funktioniert besonders gut mit Protokollpuffern und ermöglicht es Ihnen, den relevanten RPC-Code direkt aus Ihren .proto-Dateien mithilfe eines speziellen Protokollpuffer-Compiler-Plugins zu generieren.
Wenn Sie gRPC nicht verwenden möchten, ist es auch möglich, Protokollpuffer mit Ihrer eigenen RPC-Implementierung zu verwenden. Weitere Informationen dazu finden Sie im Proto2 Language Guide.
Es gibt auch eine Reihe von laufenden Drittanbieterprojekten zur Entwicklung von RPC-Implementierungen für Protokollpuffer. Eine Liste von Links zu Projekten, die uns bekannt sind, finden Sie auf der Wiki-Seite Drittanbieter-Add-ons.
JSON-Mapping
Das standardmäßige binäre Wire-Format von Protobuf ist das bevorzugte Serialisierungsformat für die Kommunikation zwischen zwei Systemen, die Protobufs verwenden. Für die Kommunikation mit Systemen, die JSON anstelle des Protobuf-Wire-Formats verwenden, unterstützt Protobuf eine kanonische Kodierung in ProtoJSON.
Optionen
Einzelne Deklarationen in einer .proto-Datei können mit einer Reihe von Optionen annotiert werden. Optionen ändern nicht die Gesamtbedeutung einer Deklaration, können aber beeinflussen, wie sie in einem bestimmten Kontext behandelt wird. Die vollständige Liste der verfügbaren Optionen ist in /google/protobuf/descriptor.proto definiert.
Einige Optionen sind Dateiebene-Optionen, d. h. sie sollten im obersten Geltungsbereich geschrieben werden, nicht innerhalb von Nachrichten-, Enum- oder Dienstdefinitionen. Einige Optionen sind Nachrichtenebene-Optionen, d. h. sie sollten innerhalb von Nachrichtendefinitionen geschrieben werden. Einige Optionen sind Feldebene-Optionen, d. h. sie sollten innerhalb von Felddefinitionen geschrieben werden. Optionen können auch für Enum-Typen, Enum-Werte, Oneof-Felder, Diensttypen und Dienstmethoden geschrieben werden; derzeit gibt es jedoch keine nützlichen Optionen für diese.
Hier sind einige der am häufigsten verwendeten Optionen
java_package(Date Option): Das Paket, das Sie für Ihre generierten Java/Kotlin-Klassen verwenden möchten. Wenn keine explizitejava_package-Option in der.proto-Datei angegeben ist, wird standardmäßig das Proto-Paket (angegeben mit dem Schlüsselwort „package“ in der.proto-Datei) verwendet. Proto-Pakete eignen sich jedoch im Allgemeinen nicht gut als Java-Pakete, da Proto-Pakete nicht mit umgekehrten Domainnamen beginnen müssen. Wenn kein Java- oder Kotlin-Code generiert wird, hat diese Option keine Auswirkung.option java_package = "com.example.foo";java_outer_classname(File Option): Der Klassenname (und damit der Dateiname) für die Wrapper-Java-Klasse, die Sie generieren möchten. Wenn keine explizitejava_outer_classname-Option in der.proto-Datei angegeben ist, wird der Klassenname durch Konvertierung des.proto-Dateinamens in Camelcase gebildet (so wirdfoo_bar.protozuFooBar.java). Wenn die Optionjava_multiple_filesdeaktiviert ist, werden alle anderen Klassen/Enums/etc., die für die.proto-Datei generiert werden, **innerhalb** dieser äußeren Wrapper-Java-Klasse als verschachtelte Klassen/Enums/etc. generiert. Wenn kein Java-Code generiert wird, hat diese Option keine Auswirkung.option java_outer_classname = "Ponycopter";java_multiple_files(Datei-Option): Wenn falsch, wird nur eine einzelne.java-Datei für diese.proto-Datei generiert, und alle für die Top-Level-Nachrichten, Dienste und Aufzählungen generierten Java-Klassen/Enums/usw. werden in einer äußeren Klasse verschachtelt (siehejava_outer_classname). Wenn wahr, werden separate.java-Dateien für jede der für die Top-Level-Nachrichten, Dienste und Aufzählungen generierten Java-Klassen/Enums/usw. generiert, und die Wrapper-Java-Klasse, die für diese.proto-Datei generiert wird, enthält keine verschachtelten Klassen/Enums/usw. Dies ist eine boolesche Option, die standardmäßig auffalsegesetzt ist. Wenn kein Java-Code generiert wird, hat diese Option keine Auswirkung. Diese wurde in Edition 2024 entfernt und durchfeatures.(pb.java).nest_in_file_classersetzt.option java_multiple_files = true;optimize_for(Datei-Option): Kann aufSPEED,CODE_SIZEoderLITE_RUNTIMEgesetzt werden. Dies beeinflusst die C++- und Java-Code-Generatoren (und möglicherweise Drittanbieter-Generatoren) wie folgt:SPEED(Standard): Der Protocol-Buffer-Compiler generiert Code für die Serialisierung, das Parsen und die Ausführung anderer gängiger Operationen mit Ihren Nachrichtentypen. Dieser Code ist hochoptimiert.CODE_SIZE: Der Protocol-Buffer-Compiler generiert minimale Klassen und verlässt sich auf gemeinsam genutzten, reflexionsbasierten Code zur Implementierung von Serialisierung, Parsing und verschiedenen anderen Operationen. Der generierte Code wird dadurch viel kleiner sein als beiSPEED, aber die Operationen werden langsamer sein. Klassen implementieren weiterhin genau die gleiche öffentliche API wie imSPEED-Modus. Dieser Modus ist am nützlichsten in Apps, die eine sehr große Anzahl von.proto-Dateien enthalten und nicht alle davon blitzschnell benötigen.LITE_RUNTIME: Der Protocol-Buffer-Compiler generiert Klassen, die nur von der „Lite“-Runtime-Bibliothek (libprotobuf-liteanstelle vonlibprotobuf) abhängen. Die Lite-Runtime ist viel kleiner als die vollständige Bibliothek (ungefähr um eine Größenordnung kleiner), aber sie lässt bestimmte Funktionen wie Deskriptoren und Reflexion weg. Dies ist besonders nützlich für Apps, die auf eingeschränkten Plattformen wie Mobiltelefonen laufen. Der Compiler generiert weiterhin schnelle Implementierungen aller Methoden, wie er es imSPEED-Modus tut. Generierte Klassen implementieren nur dieMessageLite-Schnittstelle in jeder Sprache, die nur eine Teilmenge der Methoden der vollständigenMessage-Schnittstelle bereitstellt.
option optimize_for = CODE_SIZE;cc_generic_services,java_generic_services,py_generic_services(Datei-Optionen): Generische Dienste sind veraltet. Ob der Protocol-Buffer-Compiler abstrakten Dienstcode basierend auf Dienstdefinitionen in C++, Java bzw. Python generieren soll. Aus Kompatibilitätsgründen sind diese standardmäßig auftruegesetzt. Seit Version 2.3.0 (Januar 2010) wird es jedoch als vorzuziehen angesehen, dass RPC-Implementierungen Code-Generator-Plugins bereitstellen, um Code zu generieren, der für jedes System spezifischer ist, anstatt sich auf die „abstrakten“ Dienste zu verlassen.// This file relies on plugins to generate service code. option cc_generic_services = false; option java_generic_services = false; option py_generic_services = false;cc_enable_arenas(Datei-Option): Aktiviert die Arena-Speicherverwaltung für C++-generierten Code.objc_class_prefix(Datei-Option): Legt das Objective-C-Klassenpräfix fest, das allen von diesem .proto generierten Objective-C-Klassen und Enums vorangestellt wird. Es gibt keinen Standardwert. Sie sollten Präfixe zwischen 3-5 Großbuchstaben verwenden, wie von Apple empfohlen. Beachten Sie, dass alle 2-Buchstaben-Präfixe von Apple reserviert sind.packed(Feldoption): In Protocol Buffers Editionen ist diese Option auftruegesperrt. Um das nicht gepackte Wireformat zu verwenden, können Sie diese Option mit einem Editions-Feature überschreiben. Dies bietet Kompatibilität mit Parsern vor Version 2.3.0 (selten benötigt), wie im folgenden Beispiel gezeigt.repeated int32 samples = 4 [features.repeated_field_encoding = EXPANDED];deprecated(Feldoption): Wenn auftruegesetzt, zeigt dies an, dass das Feld veraltet ist und von neuem Code nicht verwendet werden sollte. In den meisten Sprachen hat dies keine tatsächliche Auswirkung. In Java wird dies zu einer@Deprecated-Annotation. Für C++ generiert clang-tidy Warnungen, wenn veraltete Felder verwendet werden. In Zukunft können andere sprachspezifische Code-Generatoren Deprecations-Annotationen für die Accessoren des Feldes generieren, was wiederum eine Warnung auslöst, wenn Code kompiliert wird, der versucht, das Feld zu verwenden. Wenn das Feld von niemandem verwendet wird und Sie verhindern möchten, dass neue Benutzer es verwenden, sollten Sie die Felddeklaration durch eine reservierte Anweisung ersetzen.int32 old_field = 6 [deprecated = true];
Optionen für Enum-Werte
Enum-Wert-Optionen werden unterstützt. Sie können die Option deprecated verwenden, um anzuzeigen, dass ein Wert nicht mehr verwendet werden sollte. Sie können auch benutzerdefinierte Optionen mithilfe von Erweiterungen erstellen.
Das folgende Beispiel zeigt die Syntax zum Hinzufügen dieser Optionen.
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
string string_name = 123456789;
}
enum Data {
DATA_UNSPECIFIED = 0;
DATA_SEARCH = 1 [deprecated = true];
DATA_DISPLAY = 2 [
(string_name) = "display_value"
];
}
Der C++-Code zum Lesen der Option string_name könnte ungefähr so aussehen:
const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);
Siehe Benutzerdefinierte Optionen, um zu erfahren, wie benutzerdefinierte Optionen auf Enum-Werte und Felder angewendet werden.
Benutzerdefinierte Optionen
Protocol Buffers erlaubt es Ihnen auch, Ihre eigenen Optionen zu definieren und zu verwenden. Beachten Sie, dass dies ein fortgeschrittenes Feature ist, das die meisten Leute nicht benötigen. Wenn Sie glauben, dass Sie Ihre eigenen Optionen erstellen müssen, lesen Sie das Proto2 Sprachhandbuch für Details. Beachten Sie, dass die Erstellung benutzerdefinierter Optionen Erweiterungen verwendet.
Ab der Edition 2024 importieren Sie benutzerdefinierte Optionsdefinitionen mithilfe von import option. Siehe Importieren.
Option-Aufbewahrung
Optionen haben eine Vorstellung von Aufbewahrung (retention), die steuert, ob eine Option im generierten Code beibehalten wird. Optionen haben standardmäßig Laufzeitaufbewahrung (runtime retention), was bedeutet, dass sie im generierten Code beibehalten werden und somit zur Laufzeit im generierten Deskriptorpool sichtbar sind. Sie können jedoch retention = RETENTION_SOURCE setzen, um anzugeben, dass eine Option (oder ein Feld innerhalb einer Option) zur Laufzeit nicht beibehalten werden muss. Dies wird als Source Retention bezeichnet.
Die Aufbewahrung von Optionen ist ein fortgeschrittenes Feature, um das sich die meisten Benutzer keine Sorgen machen müssen, aber es kann nützlich sein, wenn Sie bestimmte Optionen verwenden möchten, ohne die Code-Größenkosten für die Beibehaltung in Ihren Binärdateien zu zahlen. Optionen mit Source Retention sind für protoc und protoc Plugins weiterhin sichtbar, sodass Code-Generatoren sie zur Anpassung ihres Verhaltens verwenden können.
Die Aufbewahrung kann direkt auf einer Option gesetzt werden, so:
extend google.protobuf.FileOptions {
int32 source_retention_option = 1234
[retention = RETENTION_SOURCE];
}
Sie kann auch auf einem einfachen Feld gesetzt werden, in diesem Fall tritt sie nur in Kraft, wenn dieses Feld innerhalb einer Option vorkommt.
message OptionsMessage {
int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}
Sie können gerne retention = RETENTION_RUNTIME setzen, aber dies hat keine Auswirkung, da es das Standardverhalten ist. Wenn ein Nachrichtenfeld als RETENTION_SOURCE markiert ist, werden seine gesamten Inhalte verworfen; Felder darin können dies nicht überschreiben, indem sie versuchen, RETENTION_RUNTIME zu setzen.
Hinweis
Ab Protocol Buffers 22.0 ist die Unterstützung für Optionsaufbewahrung noch in Arbeit und nur C++ und Java werden unterstützt. Go hat Unterstützung ab Version 1.29.0. Die Python-Unterstützung ist vollständig, hat es aber noch nicht in eine Veröffentlichung geschafft.Option-Ziele
Felder haben eine targets-Option, die die Arten von Entitäten steuert, auf die das Feld angewendet werden kann, wenn es als Option verwendet wird. Wenn ein Feld beispielsweise targets = TARGET_TYPE_MESSAGE hat, kann dieses Feld nicht in einer benutzerdefinierten Option auf einer Aufzählung (oder einer anderen Nicht-Nachrichten-Entität) gesetzt werden. Protoc erzwingt dies und löst einen Fehler aus, wenn die Zielbeschränkungen verletzt werden.
Auf den ersten Blick mag dieses Feature unnötig erscheinen, da jede benutzerdefinierte Option eine Erweiterung der Optionsnachricht für eine bestimmte Entität ist, was die Option bereits auf diese eine Entität beschränkt. Allerdings sind Optionsziele nützlich, wenn Sie eine gemeinsam genutzte Optionsnachricht haben, die auf mehrere Entitätstypen angewendet wird, und Sie die Verwendung einzelner Felder in dieser Nachricht steuern möchten. Zum Beispiel:
message MyOptions {
string file_only_option = 1 [targets = TARGET_TYPE_FILE];
int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
targets = TARGET_TYPE_ENUM];
}
extend google.protobuf.FileOptions {
MyOptions file_options = 50000;
}
extend google.protobuf.MessageOptions {
MyOptions message_options = 50000;
}
extend google.protobuf.EnumOptions {
MyOptions enum_options = 50000;
}
// OK: this field is allowed on file options
option (file_options).file_only_option = "abc";
message MyMessage {
// OK: this field is allowed on both message and enum options
option (message_options).message_and_enum_option = 42;
}
enum MyEnum {
MY_ENUM_UNSPECIFIED = 0;
// Error: file_only_option cannot be set on an enum.
option (enum_options).file_only_option = "xyz";
}
Generieren Ihrer Klassen
Um den Java-, Kotlin-, Python-, C++-, Go-, Ruby-, Objective-C- oder C#-Code zu generieren, den Sie zum Arbeiten mit den Nachrichtentypen in einer .proto-Datei benötigen, müssen Sie den Protocol-Buffer-Compiler protoc auf die .proto-Datei anwenden. Wenn Sie den Compiler noch nicht installiert haben, laden Sie das Paket herunter und befolgen Sie die Anweisungen in der README. Für Go müssen Sie außerdem ein spezielles Code-Generator-Plugin für den Compiler installieren; dieses und die Installationsanweisungen finden Sie im golang/protobuf Repository auf GitHub.
Der Protocol-Compiler wird wie folgt aufgerufen:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
IMPORT_PATHgibt ein Verzeichnis an, in dem nach.proto-Dateien gesucht werden soll, wennimport-Direktiven aufgelöst werden. Wenn dies weggelassen wird, wird das aktuelle Verzeichnis verwendet. Mehrere Importverzeichnisse können angegeben werden, indem die Option--proto_pathmehrmals übergeben wird; sie werden in der angegebenen Reihenfolge durchsucht.-I=_IMPORT_PATH_kann als Kurzform von--proto_pathverwendet werden.
Hinweis: Dateipfade relativ zu ihrem proto_path müssen in einer bestimmten Binärdatei global eindeutig sein. Wenn Sie beispielsweise proto/lib1/data.proto und proto/lib2/data.proto haben, können diese beiden Dateien nicht zusammen mit -I=proto/lib1 -I=proto/lib2 verwendet werden, da es mehrdeutig wäre, welche Datei import "data.proto" bedeutet. Stattdessen sollte -Iproto/ verwendet werden und die globalen Namen werden lib1/data.proto und lib2/data.proto sein.
Wenn Sie eine Bibliothek veröffentlichen und andere Benutzer Ihre Nachrichten direkt verwenden dürfen, sollten Sie einen eindeutigen Bibliotheksnamen in den Pfad aufnehmen, unter dem sie voraussichtlich verwendet werden, um Namenskollisionen zu vermeiden. Wenn Sie mehrere Verzeichnisse in einem Projekt haben, ist es bewährte Praxis, einen einzigen -I auf ein übergeordnetes Verzeichnis des Projekts zu setzen.
Sie können eine oder mehrere Ausgaberichtlinien angeben:
--cpp_outgeneriert C++-Code inDST_DIR. Mehr dazu finden Sie in der Referenz für generierten C++-Code.--java_outgeneriert Java-Code inDST_DIR. Mehr dazu finden Sie in der Referenz für generierten Java-Code.--kotlin_outgeneriert zusätzlichen Kotlin-Code inDST_DIR. Mehr dazu finden Sie in der Referenz für generierten Kotlin-Code.--python_outgeneriert Python-Code inDST_DIR. Mehr dazu finden Sie in der Referenz für generierten Python-Code.--go_outgeneriert Go-Code inDST_DIR. Mehr dazu finden Sie in der Referenz für generierten Go-Code.--ruby_outgeneriert Ruby-Code inDST_DIR. Mehr dazu finden Sie in der Referenz für generierten Ruby-Code.--objc_outgeneriert Objective-C-Code inDST_DIR. Mehr dazu finden Sie in der Referenz für generierten Objective-C-Code.--csharp_outgeneriert C#-Code inDST_DIR. Mehr dazu finden Sie in der Referenz für generierten C#-Code.--php_outgeneriert PHP-Code inDST_DIR. Mehr dazu finden Sie in der Referenz für generierten PHP-Code.
Als zusätzlichen Komfort, wenn die
DST_DIRmit.zipoder.jarendet, schreibt der Compiler die Ausgabe in eine einzelne ZIP-formatierte Archivdatei mit dem angegebenen Namen..jar-Ausgaben erhalten auch eine Manifestdatei gemäß den Anforderungen der Java JAR-Spezifikation. Beachten Sie, dass das Archiv überschrieben wird, wenn es bereits existiert.Sie müssen eine oder mehrere
.proto-Dateien als Eingabe angeben. Mehrere.proto-Dateien können gleichzeitig angegeben werden. Obwohl die Dateien relativ zum aktuellen Verzeichnis benannt sind, muss jede Datei in einem derIMPORT_PATHs liegen, damit der Compiler ihren kanonischen Namen ermitteln kann.
Dateiposition
Es ist ratsam, .proto-Dateien nicht im selben Verzeichnis wie andere Sprachquellen abzulegen. Erwägen Sie die Erstellung eines Unterpakets proto für .proto-Dateien unter dem Stammverzeichnis Ihres Projekts.
Position sollte sprachunabhängig sein
Beim Arbeiten mit Java-Code ist es praktisch, zusammengehörige .proto-Dateien im selben Verzeichnis wie den Java-Quellcode abzulegen. Wenn jedoch Nicht-Java-Code jemals dieselben Protos verwendet, ist das Pfadpräfix nicht mehr sinnvoll. Legen Sie daher im Allgemeinen die Protos in ein sprachunabhängiges Verzeichnis, z. B. //myteam/mypackage.
Die Ausnahme von dieser Regel ist, wenn klar ist, dass die Protos nur in einem Java-Kontext verwendet werden, z. B. zum Testen.
Unterstützte Plattformen
Für Informationen über
- die unterstützten Betriebssysteme, Compiler, Build-Systeme und C++-Versionen siehe Grundlegende C++-Support-Richtlinie.
- die unterstützten PHP-Versionen siehe Unterstützte PHP-Versionen.