C# Erstellter Code Leitfaden

Beschreibt genau, welchen C#-Code der Protocol Buffer Compiler für Protokolldefinitionen mit Proto3-Syntax generiert.

Sie sollten den Proto2-Sprachleitfaden, den Proto3-Sprachleitfaden oder den Editions-Sprachleitfaden lesen, bevor Sie dieses Dokument lesen.

Compiler-Aufruf

Der Protocol Buffer Compiler erzeugt C#-Ausgabe, wenn er mit dem Befehlszeilenflag --csharp_out aufgerufen wird. Der Parameter der Option --csharp_out ist das Verzeichnis, in das der Compiler Ihre C#-Ausgabe schreiben soll. Abhängig von anderen Optionen kann der Compiler jedoch Unterverzeichnisse des angegebenen Verzeichnisses erstellen. Der Compiler erstellt eine einzige Quelldatei für jede .proto-Datei als Eingabe, wobei die Standarderweiterung .cs ist, die jedoch über Compiler-Optionen konfigurierbar ist.

C#-spezifische Optionen

Sie können dem Protocol Buffer Compiler weitere C#-Optionen über das Befehlszeilenflag --csharp_opt übergeben. Die unterstützten Optionen sind

  • file_extension: Legt die Dateierweiterung für den generierten Code fest. Der Standardwert ist .cs, aber eine gängige Alternative ist .g.cs, um anzuzeigen, dass die Datei generierten Code enthält.

  • base_namespace: Wenn diese Option angegeben ist, erstellt der Generator eine Verzeichnishierarchie für generierten Quellcode, die den Namespaces der generierten Klassen entspricht. Der Wert der Option wird verwendet, um anzugeben, welcher Teil des Namespaces als "Basis" für das Ausgabeverzeichnis betrachtet werden soll. Zum Beispiel mit dem folgenden Befehl

    protoc --proto_path=bar --csharp_out=src --csharp_opt=base_namespace=Example player.proto
    

    wobei player.proto die Option csharp_namespace Example.Game hat, generiert der Protocol Buffer Compiler eine Datei src/Game/Player.cs. Diese Option würde normalerweise mit der Option default namespace in einem C#-Projekt in Visual Studio übereinstimmen. Wenn die Option angegeben, aber mit einem leeren Wert versehen ist, wird der vollständige C#-Namespace, wie er in der generierten Datei verwendet wird, für die Verzeichnishierarchie verwendet. Wenn die Option überhaupt nicht angegeben ist, werden die generierten Dateien einfach in das Verzeichnis geschrieben, das durch --csharp_out angegeben wurde, ohne dass eine Hierarchie erstellt wird.

  • internal_access: Wenn diese Option angegeben ist, erstellt der Generator Typen mit dem Zugriffsmodifikator internal anstelle von public.

  • serializable: Wenn diese Option angegeben ist, fügt der Generator das Attribut [Serializable] zu generierten Nachrichtenklassen hinzu.

Mehrere Optionen können durch Kommas getrennt angegeben werden, wie im folgenden Beispiel

protoc --proto_path=src --csharp_out=build/gen --csharp_opt=file_extension=.g.cs,base_namespace=Example,internal_access src/foo.proto

Dateistruktur

Der Name der Ausgabedatei wird aus dem .proto-Dateinamen abgeleitet, indem er in Pascal-Case umgewandelt wird, wobei Unterstriche als Worttrenner behandelt werden. So führt beispielsweise eine Datei namens player_record.proto zu einer Ausgabedatei namens PlayerRecord.cs (wobei die Dateierweiterung mit --csharp_opt angegeben werden kann, wie oben gezeigt).

Jede generierte Datei hat die folgende Form in Bezug auf öffentliche Member. (Die Implementierung wird hier nicht gezeigt.)

namespace [...]
{
  public static partial class [... descriptor class name ...]
  {
    public static FileDescriptor Descriptor { get; }
  }

  [... Enums ...]
  [... Message classes ...]
}

Der namespace wird aus dem package des Protos abgeleitet, wobei dieselben Konvertierungsregeln wie für den Dateinamen gelten. Zum Beispiel würde ein Proto-Paket von example.high_score zu einem Namespace von Example.HighScore führen. Sie können den Standard-generierten Namespace für eine bestimmte .proto-Datei mit der File-Option csharp_namespace überschreiben.

Jede Top-Level-Enum und Nachricht führt zur Deklaration einer Enum oder Klasse als Member des Namespaces. Zusätzlich wird immer eine einzelne statische partielle Klasse für den File-Deskriptor generiert. Diese wird für reflexionsbasierte Operationen verwendet. Die Deskriptor-Klasse erhält denselben Namen wie die Datei, ohne die Erweiterung. Wenn jedoch eine Nachricht mit demselben Namen existiert (was ziemlich häufig vorkommt), wird die Deskriptor-Klasse in einem verschachtelten Proto-Namespace platziert, um Kollisionen mit der Nachricht zu vermeiden.

Als Beispiel für all diese Regeln betrachten wir die Datei timestamp.proto, die Teil von Protocol Buffers ist. Eine gekürzte Version von timestamp.proto sieht wie folgt aus

edition = "2023";

package google.protobuf;
option csharp_namespace = "Google.Protobuf.WellKnownTypes";

message Timestamp { ... }

Die generierte Datei Timestamp.cs hat die folgende Struktur

namespace Google.Protobuf.WellKnownTypes
{
  namespace Proto
  {
    public static partial class Timestamp
    {
      public static FileDescriptor Descriptor { get; }
    }
  }

  public sealed partial class Timestamp : IMessage<Timestamp>
  {
    [...]
  }
}

Nachrichten

Angesichts einer einfachen Nachrichten Deklaration:

message Foo {}

Der Protocol Buffer Compiler generiert eine versiegelte, partielle Klasse namens Foo, die das IMessage<Foo>-Interface implementiert, wie unten mit Member-Deklarationen gezeigt. Weitere Informationen finden Sie in den Inline-Kommentaren.

public sealed partial class Foo : IMessage<Foo>
{
  // Static properties for parsing and reflection
  public static MessageParser<Foo> Parser { get; }
  public static MessageDescriptor Descriptor { get; }

  // Explicit implementation of IMessage.Descriptor, to avoid conflicting with
  // the static Descriptor property. Typically the static property is used when
  // referring to a type known at compile time, and the instance property is used
  // when referring to an arbitrary message, such as during JSON serialization.
  MessageDescriptor IMessage.Descriptor { get; }

  // Parameterless constructor which calls the OnConstruction partial method if provided.
  public Foo();
  // Deep-cloning constructor
  public Foo(Foo);
  // Partial method which can be implemented in manually-written code for the same class, to provide
  // a hook for code which should be run whenever an instance is constructed.
  partial void OnConstruction();

  // Implementation of IDeepCloneable<T>.Clone(); creates a deep clone of this message.
  public Foo Clone();

  // Standard equality handling; note that IMessage<T> extends IEquatable<T>
  public override bool Equals(object other);
  public bool Equals(Foo other);
  public override int GetHashCode();

  // Converts the message to a JSON representation
  public override string ToString();

  // Serializes the message to the protobuf binary format
  public void WriteTo(CodedOutputStream output);
  // Calculates the size of the message in protobuf binary format
  public int CalculateSize();

  // Merges the contents of the given message into this one. Typically
  // used by generated code and message parsers.
  public void MergeFrom(Foo other);

  // Merges the contents of the given protobuf binary format stream
  // into this message. Typically used by generated code and message parsers.
  public void MergeFrom(CodedInputStream input);
}

Beachten Sie, dass all diese Member immer vorhanden sind; die Option optimize_for beeinflusst die Ausgabe des C#-Code-Generators nicht.

Verschachtelte Typen

Eine Nachricht kann innerhalb einer anderen Nachricht deklariert werden. Zum Beispiel:

message Foo {
  message Bar {
  }
}

In diesem Fall – oder wenn eine Nachricht eine verschachtelte Enum enthält – generiert der Compiler eine verschachtelte Types-Klasse und dann eine Bar-Klasse innerhalb der Types-Klasse, sodass der vollständige generierte Code wie folgt aussehen würde

namespace [...]
{
  public sealed partial class Foo : IMessage<Foo>
  {
    public static partial class Types
    {
      public sealed partial class Bar : IMessage<Bar> { ... }
    }
  }
}

Obwohl die Zwischenklasse Types umständlich ist, ist sie erforderlich, um das häufige Szenario zu behandeln, dass ein verschachtelter Typ ein entsprechendes Feld in der Nachricht hat. Andernfalls hätten Sie sowohl eine Eigenschaft als auch einen Typ mit demselben Namen, die in derselben Klasse verschachtelt sind – und das wäre ungültiges C#.

Felder

Der Protocol Buffer Compiler generiert eine C#-Eigenschaft für jedes Feld, das innerhalb einer Nachricht definiert ist. Die genaue Art der Eigenschaft hängt von der Art des Feldes ab: seinem Typ und ob es singulär, wiederholt oder ein Map-Feld ist.

Singuläre Felder

Jedes singuläre Feld generiert eine Lese-/Schreib-Eigenschaft. Ein Feld vom Typ string oder bytes generiert eine ArgumentNullException, wenn ein Nullwert angegeben wird; das Abrufen eines Wertes aus einem Feld, das nicht explizit gesetzt wurde, gibt einen leeren String oder ByteString zurück. Nachrichtenfelder können auf Null gesetzt werden, was im Wesentlichen das Feld löscht. Dies entspricht nicht dem Setzen des Wertes auf eine "leere" Instanz des Nachrichtentyps.

Wiederholte Felder

Jedes wiederholte Feld generiert eine schreibgeschützte Eigenschaft vom Typ Google.Protobuf.Collections.RepeatedField<T>, wobei T der Elementtyp des Feldes ist. Größtenteils verhält sich dies wie List<T>, hat aber eine zusätzliche Add-Überladung, die es ermöglicht, eine Sammlung von Elementen auf einmal hinzuzufügen. Dies ist praktisch beim Befüllen eines wiederholten Feldes in einem Objektinitialisierer. Darüber hinaus hat RepeatedField<T> direkten Unterstützung für Serialisierung, Deserialisierung und Klonen, aber dies wird normalerweise vom generierten Code anstelle von manuell geschriebenem Anwendungscode verwendet.

Wiederholte Felder dürfen keine Nullwerte enthalten, selbst bei Nachrichtentypen, außer bei den nullable Wrapper-Typen, die unten erklärt werden.

Map-Felder

Jedes Map-Feld generiert eine schreibgeschützte Eigenschaft vom Typ Google.Protobuf.Collections.MapField<TKey, TValue>, wobei TKey der Schlüsseltyp des Feldes und TValue der Werttyp des Feldes ist. Größtenteils verhält sich dies wie Dictionary<TKey, TValue>, hat aber eine zusätzliche Add-Überladung, die es ermöglicht, ein weiteres Dictionary auf einmal hinzuzufügen. Dies ist praktisch beim Befüllen eines wiederholten Feldes in einem Objektinitialisierer. Darüber hinaus hat MapField<TKey, TValue> direkte Unterstützung für Serialisierung, Deserialisierung und Klonen, aber dies wird normalerweise vom generierten Code anstelle von manuell geschriebenem Anwendungscode verwendet. Schlüssel in der Map dürfen nicht null sein; Werte dürfen null sein, wenn der entsprechende singuläre Feldtyp Nullwerte unterstützen würde.

Oneof-Felder

Jedes Feld innerhalb eines Oneof hat eine separate Eigenschaft, wie ein reguläres singuläres Feld. Der Compiler generiert jedoch zusätzlich eine Eigenschaft, um zu ermitteln, welches Feld in der Enum gesetzt wurde, zusammen mit einer Enum und einer Methode zum Löschen des Oneof. Zum Beispiel für diese Oneof-Felddarstellung

oneof avatar {
  string image_url = 1;
  bytes image_data = 2;
}

Der Compiler generiert diese öffentlichen Member

enum AvatarOneofCase
{
  None = 0,
  ImageUrl = 1,
  ImageData = 2
}

public AvatarOneofCase AvatarCase { get; }
public void ClearAvatar();
public string ImageUrl { get; set; }
public ByteString ImageData { get; set; }

Wenn eine Eigenschaft der aktuelle "Fall" des Oneof ist, gibt das Abrufen dieser Eigenschaft den für diese Eigenschaft gesetzten Wert zurück. Andernfalls gibt das Abrufen der Eigenschaft den Standardwert für den Typ der Eigenschaft zurück – nur ein Member eines Oneof kann gleichzeitig gesetzt sein.

Das Setzen einer beliebigen konstituierenden Eigenschaft des Oneof ändert den gemeldeten "Fall" des Oneof. Wie bei einem regulären singulären Feld können Sie ein Oneof-Feld vom Typ string oder bytes nicht auf einen Nullwert setzen. Das Setzen eines Feldes vom Nachrichtentyp auf null ist gleichbedeutend mit dem Aufruf der Oneof-spezifischen Clear-Methode.

Wrapper-Typ-Felder

Die meisten der bekannten Typen beeinflussen die Codegenerierung nicht, aber die Wrapper-Typen (wie StringWrapper und Int32Wrapper) ändern den Typ und das Verhalten der Eigenschaften.

Alle Wrapper-Typen, die C#-Werttypen entsprechen (wie Int32Wrapper, DoubleWrapper und BoolWrapper), werden auf Nullable<T> abgebildet, wobei T der entsprechende nicht-nullable Typ ist. Zum Beispiel führt ein Feld vom Typ DoubleValue zu einer C#-Eigenschaft vom Typ Nullable<double>.

Felder vom Typ StringWrapper oder BytesWrapper führen zur Generierung von C#-Eigenschaften vom Typ string und ByteString, jedoch mit dem Standardwert null und der Zulassung von null als Eigenschaftswert.

Für alle Wrapper-Typen sind Nullwerte in einem wiederholten Feld nicht zulässig, aber als Werte für Map-Einträge zulässig.

Aufzählungen (Enums)

Gegeben eine Enumerationsdefinition wie

enum Color {
  COLOR_UNSPECIFIED = 0;
  COLOR_RED = 1;
  COLOR_GREEN = 5;
  COLOR_BLUE = 1234;
}

Der Protocol Buffer Compiler generiert einen C#-Enum-Typ namens Color mit demselben Satz von Werten. Die Namen der Enum-Werte werden umgewandelt, um sie für C#-Entwickler idiomatischer zu machen

  • Wenn der ursprüngliche Name mit der großgeschriebenen Form des Enum-Namens selbst beginnt, wird dieser entfernt
  • Das Ergebnis wird in Pascal-Case konvertiert

Die obige Color Proto-Enum würde daher zu folgendem C#-Code werden

enum Color
{
  Unspecified = 0,
  Red = 1,
  Green = 5,
  Blue = 1234
}

Diese Namensumwandlung beeinflusst nicht den Text, der innerhalb der JSON-Darstellung von Nachrichten verwendet wird.

Beachten Sie, dass die .proto-Sprache erlaubt, dass mehrere Enum-Symbole denselben numerischen Wert haben. Symbole mit demselben numerischen Wert sind Synonyme. Diese werden in C# genau auf dieselbe Weise dargestellt, mit mehreren Namen, die demselben numerischen Wert entsprechen.

Eine nicht verschachtelte Aufzählung führt zur Generierung eines C#-Enums als neuem Namespace-Member; eine verschachtelte Aufzählung führt zur Generierung eines C#-Enums in der verschachtelten Types-Klasse innerhalb der Klasse, die der Nachricht entspricht, in der die Aufzählung verschachtelt ist.

Services

Der C#-Code-Generator ignoriert Services vollständig.