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'optioncsharp_namespace
Example.Game
, le compilateur de tampon de protocole génère un fichiersrc/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 depublic
. - 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.
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.