UWAGA: witryna została wycofana. Po 31 stycznia 2023 roku witryna zostanie wyłączona, a ruch będzie kierowany do nowej witryny na https://protobuf.dev. Do tego czasu aktualizacje będą dotyczyć tylko protobuf.dev.

Kod wygenerowany przez C#

Zadbaj o dobrą organizację dzięki kolekcji Zapisuj i kategoryzuj treści zgodnie ze swoimi preferencjami.

Na tej stronie możesz dokładnie sprawdzić, jaki kod C# jest generowany przez kompilator bufora protokołu w przypadku definicji protokołów za pomocą składni proto3. Zanim przeczytasz ten dokument, przeczytaj przewodnik po języku proto3.

Wywołanie kompilatora

Kompilator bufora protokołu generuje wynik w języku C# po wywołaniu flagą --csharp_out. Parametr opcji --csharp_out to katalog, w którym kompilator może zapisać dane wyjściowe C#. W zależności od innych opcji kompilator może utworzyć podkatalogi w danym katalogu. Kompilator tworzy jeden plik źródłowy na każdy wprowadzony plik .proto, którego domyślna wartość to .cs, ale można to zmienić za pomocą opcji kompilacji.

Generator kodu C# obsługuje tylko wiadomości proto3. Każdy plik .proto powinien zaczynać się od deklaracji:

syntax = "proto3";

Opcje specyficzne dla języka C#

Możesz podać dalsze opcje C# kompilatorowi bufora protokołu za pomocą flagi wiersza poleceń --csharp_opt. Obsługiwane opcje:

  • file_extension: określa rozszerzenie pliku wygenerowanego kodu. Domyślna wartość to .cs, ale popularną alternatywą jest .g.cs, która wskazuje, że plik zawiera wygenerowany kod.
  • base_namespace: gdy ta opcja jest określona, generator tworzy hierarchię katalogów dla wygenerowanych kodów źródłowych odpowiadających przestrzeniom nazw wygenerowanych klas, używając wartości opcji do określenia, która część przestrzeni nazw powinna być traktowana jako „podstawowa” katalogu wyjściowego. Na przykład w wierszu poleceń
    protoc --proto_path=bar --csharp_out=src --csharp_opt=base_namespace=Example player.proto
    , gdzie player.proto ma opcję csharp_namespace wynoszącą Example.Game, kompilator bufora protokołu generuje plik src/Game/Player.cs. Ta opcja zwykle odpowiada opcji domyślnej przestrzeni nazw w projekcie C# w Visual Studio. Jeśli ta opcja jest określona, ale ma pustą wartość, w hierarchii katalogu będzie używana pełna przestrzeń nazw C# używana w wygenerowanym pliku. Jeśli ta opcja nie jest określona, wygenerowane pliki są po prostu zapisywane w katalogu określonym przez --csharp_out bez tworzenia hierarchii.
  • internal_access: gdy ta opcja jest określona, generator tworzy typy z modyfikatorem dostępu internal zamiast public.
  • serializable (serializable): gdy ta opcja jest określona, generator dodaje atrybut [Serializable] do wygenerowanych klas wiadomości.

Możesz podać kilka opcji, rozdzielając je przecinkami, tak jak w tym przykładzie:

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

Struktura pliku

Nazwa pliku wyjściowego pochodzi z nazwy pliku .proto. W tym celu należy przekonwertować go na format Pascal i traktować znaki podkreślenia jako separatory słów. Na przykład plik o nazwie player_record.proto spowoduje utworzenie pliku wyjściowego o nazwie PlayerRecord.cs (w którym rozszerzenie można określić za pomocą --csharp_opt, tak jak pokazano wyżej).

W przypadku członków publicznych każdy wygenerowany plik ma następującą formę. (Nie pokazano tutaj implementacji).

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

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

namespace jest wnioskowany na podstawie package proto, używając tych samych reguł konwersji co nazwa pliku. Na przykład pakiet proto example.high_score ma wartość Example.HighScore. Możesz zastąpić domyślną wygenerowaną przestrzeń nazw konkretnego pliku .proto, korzystając z opcji pliku csharp_namespace.

Każda enum i wiadomość najwyższego poziomu są określane jako wyliczenie lub klasa jako członkowie przestrzeni nazw. Dodatkowo dla deskryptora pliku zawsze jest generowana pojedyncza klasa częściowa. Służy do operacji opartych na odbiciu. Klasa deskryptora ma taką samą nazwę jak plik, bez rozszerzenia. Jeśli jednak wiadomość ma taką samą nazwę (jak to często bywa), deskryptor jest umieszczany w zagnieżdżonej przestrzeni nazw Proto, aby uniknąć zderzenia z tą wiadomością.

Jako przykład wszystkich tych reguł możesz użyć pliku timestamp.proto, który jest częścią buforów protokołów. Wersja skrócona wersji timestamp.proto wygląda tak:

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

message Timestamp { ... }

Wygenerowany plik Timestamp.cs ma taką strukturę:

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

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

Wiadomości

Oto prosta deklaracja wiadomości:

message Foo {}

Kompilator bufora protokołu generuje zaszyfrowaną, częściową klasę o nazwie Foo, która implementuje interfejs IMessage<Foo>, jak pokazano poniżej z deklaracjami użytkowników. Więcej informacji znajdziesz w tekście komentarzy.

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

Wszystkie te elementy są zawsze obecne. Opcja optimize_for nie wpływa na dane wyjściowe generatora kodów C#.

Typy zagnieżdżone

Daną wiadomość można zadeklarować w innej wiadomości. Na przykład:

message Foo {
  message Bar {
  }
}

W tym przypadku – lub jeśli wiadomość zawiera zagnieżdżone wyliczenie – kompilator generuje zagnieżdżoną klasę Types, a następnie klasę Bar w klasie Types, więc pełny kod będzie wyglądać tak:

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

Mimo że klasa pośrednia Types jest niewygodna, należy posługiwać się typowym scenariuszem typu zagnieżdżonego, który zawiera odpowiednie pole w wiadomości. W przeciwnym razie otrzymano zarówno właściwość, jak i typ o tej samej nazwie zagnieżdżonej w tej samej klasie – a to byłby nieprawidłowy kod C#.

Pola

Kompilator bufora protokołu generuje właściwość C# dla każdego pola określonego w wiadomości. Dokładny charakter właściwości zależy od rodzaju pola: jego typu oraz od tego, czy pole jest pojedyncze, powtarzane czy mapy.

Pola pojedyncze

Każde pojedyncze pole generuje właściwość do odczytu i zapisu. Pole string lub bytes generuje wartość ArgumentNullException, jeśli określona jest wartość null. Pobranie wartości z pola, które nie zostało ustawione wprost, zwróci pusty ciąg lub ByteString. Pola wiadomości można ustawić na puste wartości, co skutkuje wyczyszczeniem pola. Nie jest to równoważne ustawieniu wartości na „pustą” instancję typu wiadomości.

Pola powtarzane

Każde powtórzone pole generuje właściwość tylko do odczytu typu Google.Protobuf.Collections.RepeatedField<T>, gdzie T to typ elementu pola. W większości przypadków działa to jak List<T>, ale ma też dodatkowe przeciążenie Add, które umożliwia dodanie zbioru elementów za jednym razem. Jest to przydatne, gdy wypełniasz pole powtarzane w inicjatorze obiektu. Dodatkowo RepeatedField<T> obsługuje bezpośrednie serializację, deserryzację i klonowanie, ale zwykle używa go wygenerowany kod zamiast ręcznego kodu aplikacji.

Powtórzone pola nie mogą zawierać wartości null (nawet typów wiadomości), z wyjątkiem pustych typów kodów objaśnionych poniżej.

Pola mapy

Każde pole mapy generuje właściwość tylko do odczytu Google.Protobuf.Collections.MapField<TKey, TValue>, gdzie TKey to typ klucza, a TValue to typ wartości pola. W większości przypadków działa to tak: Dictionary<TKey, TValue>, ale ma dodatkowe przeciążenie Add, które umożliwia dodawanie kolejnego słownika za jednym razem. Jest to wygodne, gdy wypełniasz pole powtarzane w inicjatorze obiektów. Dodatkowo MapField<TKey, TValue> obsługuje bezpośrednie serializację, deserryzację i klonowanie, ale zwykle używa go wygenerowany kod zamiast ręcznego kodu aplikacji. Klucze na mapie nie mogą mieć wartości null. Wartości mogą się pojawić, jeśli odpowiedni pojedynczy typ pola obsługuje wartości null.

Pola uniwersalne

Każde pole w jednym z nich ma osobną usługę, taką jak zwykłe pojedyncze pole. Kompilator generuje jednak również dodatkową właściwość, która określa, które pole wyliczenia zostało ustawione, wraz z wyliczeniem i metodą czyszczenia tego pola. Na przykład w przypadku tej definicji pola oneof

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

Kompilator wygeneruje tych członków publicznych:

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

Jeśli usługa jest w jednym z pozostałych przypadków, po pobraniu zwróci wartość ustawioną dla tej właściwości. W przeciwnym razie zwracana jest wartość domyślna typu usługi – w danym momencie można ustawić tylko jednego członka.

Ustawienie właściwości składowej jego właściwości zmienia wielkość liter w jego przypadku. Tak jak w przypadku zwykłego pojedynczego pola, nie można ustawić jednego pola z typem string lub bytes na wartość null. Ustawienie w polu „message-type” wartości null jest równoważne wywołaniu jednej z Clear metody.

Pola typu opakowań

Większość znanych typów protokołu w proto3 nie ma wpływu na generowanie kodu, ale zmiany kodów (StringWrapper, Int32Wrapper itp.) zmieniają typ i działanie właściwości.

Wszystkie typy kodów towarzyszących, które odpowiadają typom wartości C# (Int32Wrapper, DoubleWrapper, BoolWrapper itd.), są zmapowane na Nullable<T>, gdzie T jest odpowiednikiem niedopuszczającym wartości pustych. Na przykład pole typu DoubleValue powoduje utworzenie właściwości C# typu Nullable<double>.

Pola typu StringWrapper lub BytesWrapper generują generowane właściwości C# typu string i ByteString, ale z domyślną wartością null, co pozwala ustawić wartość null jako wartość.

W przypadku wszystkich typów kodów puste wartości są niedozwolone w polu powtarzanym, ale są dozwolone jako wartości dla wpisów na mapie.

Wyliczenia

Biorąc pod uwagę definicję wyliczenia, taką jak:

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

Kompilator bufora protokołu wygeneruje typ enum C# o nazwie Color z tym samym zestawem wartości. Nazwy wartości wyliczeniowych są konwertowane, aby były bardziej idiomatyczne dla programistów C#:

  • Jeśli oryginalna nazwa zaczyna się od wielkiej litery wyliczenia, jest ona usuwana
  • Wynik zostanie przekonwertowany na wielkość liter Pascal
Powyżej wyliczenie proto Color stanie się następującym kodem C#:
enum Color
{
  Red = 0,
  Green = 5,
  Blue = 1234
}

Ta zmiana nazwy nie ma wpływu na tekst używany w prezentacji JSON w przypadku wiadomości.

Pamiętaj, że w języku .proto wiele symboli enum może mieć tę samą wartość liczbową. Symbole o tej samej wartości liczbowej to synonimy. Są one przedstawione w C# w dokładnie taki sam sposób, przy czym kilka nazw odpowiada tej samej wartości liczbowej.

Niezagnieżdżone wyliczenie prowadzi do generowania wyliczenia C# jako nowego członka przestrzeni nazw. Zagnieżdżone wyliczenie prowadzi do wyliczenia C# w ramach zagnieżdżonej klasy Types w klasy odpowiadającej komunikatowi, w którym zagnieżdżono wyliczenie.

Usługi

Generator kodu C# całkowicie ignoruje usługi.