REMARQUE:Ce site est obsolète. Le site sera désactivé après le 31 janvier 2023 et le trafic redirigera vers le nouveau site à l'adresse https://protobuf.dev. En attendant, les mises à jour ne seront effectuées que sur protobuf.dev.

Code généré en C#

Restez organisé à l'aide des collections Enregistrez et classez les contenus selon vos préférences.

Cette page décrit exactement le code C# que le compilateur de tampon de protocole génère pour les définitions de protocole à l'aide de la syntaxe proto3. Lisez le guide du langage proto3 avant de lire ce document.

Appel du compilateur

Le compilateur de tampon de protocole génère une sortie C# lorsqu'elle est appelée avec l'option de ligne de commande --csharp_out. Le paramètre de l'option --csharp_out correspond au répertoire dans lequel vous souhaitez que le compilateur écrive votre sortie C#. Toutefois, en fonction d'autres options, le compilateur peut créer des sous-répertoires du répertoire spécifié. Le compilateur crée un fichier source unique pour chaque entrée de fichier .proto, défini par défaut sur une extension de .cs, mais configurable via les options du compilateur.

Seuls les messages proto3 sont pris en charge par le générateur de code C#. Assurez-vous que chaque fichier .proto commence par une déclaration de :

syntax = "proto3";

Options spécifiques à C#

Vous pouvez fournir d'autres options C# au compilateur de tampon de protocole à l'aide de l'option de ligne de commande --csharp_opt. Les options acceptées sont les suivantes :

  • file_extension : définit l'extension du fichier généré. La valeur par défaut est .cs, mais .g.cs est une alternative courante pour indiquer que le fichier contient du code généré.
  • base_namespace : lorsque cette option est spécifiée, le générateur crée une hiérarchie de répertoires pour le code source généré correspondant aux espaces de noms des classes générées, en utilisant la valeur de l'option pour indiquer quelle partie de l'espace de noms doit être considérée comme la "base" du répertoire de sortie. Par exemple, avec la ligne de commande suivante :
    protoc --proto_path=bar --csharp_out=src --csharp_opt=base_namespace=Example player.proto
    , où player.proto possède l'option csharp_namespace Example.Game, le compilateur de tampon de protocole génère un fichier src/Game/Player.cs en cours de création. Cette option correspond généralement à l'option Espace de noms par défaut d'un projet C# dans Visual Studio. Si l'option est spécifiée, mais qu'elle est vide, l'espace de noms C# complet utilisé dans le fichier généré sera utilisé pour la hiérarchie de répertoires. Si cette option n'est pas spécifiée, les fichiers générés sont simplement écrits dans le répertoire spécifié par --csharp_out, sans qu'aucune hiérarchie ne soit créée.
  • internal_access : lorsque cette option est spécifiée, le générateur crée des types avec le modificateur d'accès internal au lieu de public.
  • sérialisable: lorsque cette option est spécifiée, le générateur ajoute l'attribut [Serializable] aux classes de message générées.

Vous pouvez spécifier plusieurs options en les séparant par une virgule, comme dans l'exemple suivant :

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

Structure des fichiers

Le nom du fichier de sortie est dérivé du nom de fichier .proto en le convertissant en Pascal-case, en traitant les traits de soulignement comme des séparateurs de mots. Ainsi, par exemple, un fichier nommé player_record.proto donnera lieu à un fichier de sortie appelé PlayerRecord.cs (où l'extension de fichier peut être spécifiée à l'aide de --csharp_opt, comme indiqué ci-dessus).

Chaque fichier généré se présente sous la forme de membres publics. (L'implémentation n'est pas présentée ici.)

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

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

La namespace est déduite de la package du proto, en utilisant les mêmes règles de conversion que le nom du fichier. Par exemple, un package proto de example.high_score générerait un espace de noms Example.HighScore. Vous pouvez remplacer l'espace de noms généré par défaut pour un fichier .proto particulier à l'aide de l'option de fichier csharp_namespace.

Chaque énumération et message de niveau supérieur entraîne la déclaration d'une énumération ou d'une classe comme membres de l'espace de noms. De plus, une seule classe partielle statique est toujours générée pour le descripteur de fichier. Elle est utilisée pour les opérations basées sur la réflexion. La classe de descripteur porte le même nom que le fichier, sans l'extension. Toutefois, si un message porte le même nom (comme cela est très courant), la classe de descripteur est placée dans un espace de noms Proto imbriqué pour éviter la collision avec le message.

Pour illustrer toutes ces règles, considérons le fichier timestamp.proto fourni dans Protocol Buffers. Voici un exemple de version réduite de timestamp.proto :

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

message Timestamp { ... }

Le fichier Timestamp.cs généré présente la structure suivante:

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

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

Messages

Voici une déclaration de message simple :

message Foo {}

Le compilateur de tampon de protocole génère une classe scellée partielle appelée Foo, qui implémente l'interface IMessage<Foo>, comme indiqué ci-dessous avec les déclarations de membres. Pour en savoir plus, consultez les commentaires intégrés.

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);
}

Notez que tous ces membres sont toujours présents. L'option optimize_for n'a pas d'incidence sur la sortie du générateur de code C#.

Types imbriqués

Un message peut être déclaré dans un autre message. Exemple :

message Foo {
  message Bar {
  }
}

Dans ce cas, ou si un message contient une énumération imbriquée, le compilateur génère une classe Types imbriquée, puis une classe Bar dans la classe Types. Le code généré entièrement serait alors :

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

Bien que la classe intermédiaire Types ne soit pas pratique, elle est requise pour traiter le scénario courant d'un type imbriqué avec un champ correspondant dans le message. Dans le cas contraire, vous obtenez une propriété et un type portant le même nom imbriqués dans la même classe. Cette valeur n'est donc pas valide.

Champs

Le compilateur de tampon de protocole génère une propriété C# pour chaque champ défini dans un message. La nature exacte de la propriété dépend de la nature du champ: son type, et si le champ est au singulier, répété ou à mapper.

Champs singuliers

Tout champ unique génère une propriété de lecture/écriture. Un champ string ou bytes génère une valeur ArgumentNullException si une valeur nulle est spécifiée. La récupération d'une valeur à partir d'un champ qui n'a pas été explicitement défini renvoie une chaîne vide ou ByteString. Les champs de message peuvent être définis sur des valeurs nulles, ce qui efface le champ. Cela ne revient pas à définir la valeur sur une instance "empty" du type de message.

Champs répétés

Chaque champ répété génère une propriété en lecture seule de type Google.Protobuf.Collections.RepeatedField<T>, où T est le type d'élément du champ. Dans la plupart des cas, ceci agit comme List<T>, mais comporte une surcharge Add supplémentaire pour permettre l'ajout d'une collection d'éléments en une seule fois. Cette méthode est pratique lors du remplissage d'un champ répété dans un initialiseur d'objet. En outre, RepeatedField<T> est directement compatible avec la sérialisation, la désérialisation et le clonage, mais il est généralement utilisé par le code généré au lieu du code d'application écrit manuellement.

Les champs répétés ne peuvent pas contenir de valeurs nulles, même pour des types de message, à l'exception des types de wrappers pouvant être vides décrits ci-dessous.

Champs de carte

Chaque champ de mappage génère une propriété en lecture seule de type Google.Protobuf.Collections.MapField<TKey, TValue>, où TKey est le type de clé du champ et TValue est le type de valeur du champ. Dans la plupart des cas, ceci agit comme Dictionary<TKey, TValue>, mais comporte une surcharge Add supplémentaire pour permettre l'ajout d'un autre dictionnaire en une seule fois. Cette méthode est pratique lors du remplissage d'un champ répété dans un initialiseur d'objet. En outre, MapField<TKey, TValue> est directement compatible avec la sérialisation, la désérialisation et le clonage, mais il est généralement utilisé par le code généré au lieu du code d'application écrit manuellement. Les clés du mappage ne peuvent pas être nulles. Des valeurs peuvent être définies si le type de champ singulier correspondant serait compatible avec des valeurs nulles.

Un champ

Chaque champ de l'un d'eux possède une propriété distincte, comme un champ unique standard. Toutefois, le compilateur génère également une propriété supplémentaire pour déterminer quel champ de l'énumération a été défini, ainsi qu'une énumération et une méthode pour effacer l'un. Par exemple, pour cette définition de champ

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

Le compilateur générera les membres publics suivants:

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; }

Si une propriété est la "case" actuelle, l'extraction de la propriété renvoie la valeur définie pour cette propriété. Sinon, la récupération de la propriété renvoie la valeur par défaut pour le type de la propriété. Un seul membre peut être défini à la fois.

Si vous définissez une propriété constitutive de l'un des deux éléments, le "cas" signalé pour celui-ci sera modifié. Comme pour un champ unique standard, vous ne pouvez pas définir un champ de type string ou bytes sur une valeur nulle. Définir un champ de type message sur null équivaut à appeler la méthode Clear spécifique.

Champs de type d'enveloppe

La plupart des types connus dans proto3 n'affectent pas la génération du code, mais les types de wrapper (StringWrapper, Int32Wrapper, etc.) modifient le type et le comportement des propriétés.

Tous les types de wrappers qui correspondent aux types de valeurs C# (Int32Wrapper, DoubleWrapper, BoolWrapper, etc.) sont mappés sur Nullable<T>, où T est le type ne pouvant pas être vide. Par exemple, un champ de type DoubleValue génère une propriété C# de type Nullable<double>.

Les champs de type StringWrapper ou BytesWrapper génèrent des propriétés C# de type string et ByteString, mais avec une valeur par défaut "null" qui permet de définir "null" comme valeur de propriété.

Pour tous les types de wrappers, les valeurs nulles ne sont pas autorisées dans un champ répété, mais sont autorisées en tant que valeurs pour les entrées de carte.

Énumérations

Selon une définition d'énumération, telle que:

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

Le compilateur de tampon de protocole va générer une énumération C# de type CColor appelée avec le même ensemble de valeurs. Les noms des valeurs d'énumération sont convertis pour les rendre plus idiomatiques pour les développeurs C# :

  • Si le nom d'origine commence par la première lettre du nom d'énumération lui-même, il est supprimé.
  • Le résultat est converti en casse Pascal.
L'énumération proto Color ci-dessus deviendrait donc le code C# suivant :
enum Color
{
  Red = 0,
  Green = 5,
  Blue = 1234
}

Cette transformation de nom n'affecte pas le texte utilisé dans la représentation JSON des messages.

Notez que la langue .proto permet à plusieurs symboles d'énumération d'avoir la même valeur numérique. Les symboles ayant la même valeur numérique sont des synonymes. Ils sont représentés en C# de la même manière, avec plusieurs noms correspondant à la même valeur numérique.

Une énumération non imbriquée entraîne la génération d'une énumération C# en tant que nouveau membre d'un espace de noms. Une énumération imbriquée conduit à la génération d'une énumération C# dans la classe imbriquée Types dans la classe correspondant au message dans lequel l'énumération est imbriquée.

Services

Le générateur de code C# ignore complètement les services.