C# 程式碼產生指南

精確描述 protocol buffer 編譯器為使用 proto3 語法的協定定義產生的 C# 程式碼。

在閱讀本文檔之前,您應該先閱讀proto3 語言指南

編譯器調用

當使用 --csharp_out 命令列旗標調用時,protocol buffer 編譯器會產生 C# 輸出。--csharp_out 選項的參數是您希望編譯器寫入 C# 輸出的目錄,儘管根據其他選項,編譯器可能會建立指定目錄的子目錄。編譯器會為每個 .proto 檔案輸入建立單一原始碼檔案,預設副檔名為 .cs,但可透過編譯器選項設定。

C# 程式碼產生器僅支援 proto3 訊息。請確保每個 .proto 檔案都以宣告開頭:

syntax = "proto3";

C# 專用選項

您可以使用 --csharp_opt 命令列旗標,向 protocol buffer 編譯器提供更多 C# 選項。支援的選項如下:

  • file_extension:設定產生程式碼的檔案副檔名。預設為 .cs,但常見的替代方案是 .g.cs,以指示檔案包含產生的程式碼。

  • base_namespace:指定此選項後,產生器會為產生的原始碼建立目錄階層,對應於產生類別的命名空間,並使用選項的值來指示命名空間的哪一部分應被視為輸出目錄的「基礎」。例如,使用以下命令列:

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

    其中 player.protocsharp_namespace 選項為 Example.Game,protocol buffer 編譯器會產生檔案 src/Game/Player.cs。此選項通常與 Visual Studio 中 C# 專案的預設命名空間選項相對應。如果指定了此選項但值為空,則將使用產生檔案中使用的完整 C# 命名空間作為目錄階層。如果完全未指定此選項,則產生的檔案只會寫入 --csharp_out 指定的目錄中,而不會建立任何階層。

  • internal_access:指定此選項後,產生器會使用 internal 存取修飾詞而非 public 來建立類型。

  • serializable:指定此選項後,產生器會將 [Serializable] 屬性新增至產生的訊息類別。

可以透過逗號分隔來指定多個選項,如下例所示:

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

檔案結構

輸出檔案的名稱衍生自 .proto 檔案名稱,方法是將其轉換為 Pascal 大小寫,並將底線視為單字分隔符。因此,例如,名為 player_record.proto 的檔案將產生名為 PlayerRecord.cs 的輸出檔案 (其中檔案副檔名可以使用 --csharp_opt 指定,如上所示)。

就公用成員而言,每個產生的檔案都採用以下形式。(此處未顯示實作。)

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

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

namespace 是從 proto 的 package 推斷出來的,使用與檔案名稱相同的轉換規則。例如,proto 套件 example.high_score 將產生 Example.HighScore 的命名空間。您可以使用 csharp_namespace 檔案選項,覆寫特定 .proto 的預設產生命名空間。

每個最上層的列舉和訊息都會產生一個列舉或類別,宣告為命名空間的成員。此外,始終為檔案描述符產生單一靜態 partial 類別。這用於基於反射的操作。描述符類別的名稱與檔案相同,不含副檔名。但是,如果存在同名的訊息(很常見),則描述符類別會放置在巢狀 Proto 命名空間中,以避免與訊息衝突。

作為所有這些規則的範例,請考慮作為 Protocol Buffers 一部分提供的 timestamp.proto 檔案。timestamp.proto 的精簡版本如下所示:

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

message Timestamp { ... }

產生的 Timestamp.cs 檔案具有以下結構:

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

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

訊息

給定一個簡單的訊息宣告:

message Foo {}

protocol buffer 編譯器會產生一個密封的 partial 類別,名為 Foo,它實作 IMessage<Foo> 介面,如下所示,其中包含成員宣告。請參閱內嵌註解以取得更多資訊。

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

請注意,所有這些成員始終存在;optimize_for 選項不會影響 C# 程式碼產生器的輸出。

巢狀類型

訊息可以宣告在另一個訊息內。例如:

message Foo {
  message Bar {
  }
}

在這種情況下,或者如果訊息包含巢狀列舉,編譯器會產生一個巢狀 Types 類別,然後在 Types 類別中產生一個 Bar 類別,因此完整的產生程式碼將是:

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

儘管中間的 Types 類別不太方便,但它是處理巢狀類型在訊息中具有對應欄位的常見情況所必需的。否則,您最終會得到屬性和類型,它們具有相同的名稱,並巢狀在同一個類別中,這將是無效的 C#。

欄位

protocol buffer 編譯器會為訊息中定義的每個欄位產生一個 C# 屬性。屬性的確切性質取決於欄位的性質:其類型,以及它是單數、重複還是 Map 欄位。

單數欄位

任何單數欄位都會產生讀寫屬性。如果指定了 Null 值,stringbytes 欄位將產生 ArgumentNullException;從未明確設定的欄位中擷取值將傳回空字串或 ByteString。訊息欄位可以設定為 Null 值,這實際上是清除欄位。這不等同於將值設定為訊息類型的「空」實例。

重複欄位

每個重複欄位都會產生類型為 Google.Protobuf.Collections.RepeatedField<T> 的唯讀屬性,其中 T 是欄位的元素類型。在大多數情況下,這就像 List<T> 一樣運作,但它有一個額外的 Add 多載,允許一次新增項目集合。當在物件初始化器中填入重複欄位時,這很方便。此外,RepeatedField<T> 直接支援序列化、還原序列化和複製,但這通常由產生的程式碼使用,而不是手寫的應用程式碼。

重複欄位不能包含 Null 值,即使是訊息類型也是如此,除非是下文說明的可 Null wrapper 類型。

Map 欄位

每個 Map 欄位都會產生類型為 Google.Protobuf.Collections.MapField<TKey, TValue> 的唯讀屬性,其中 TKey 是欄位的鍵類型,TValue 是欄位的值類型。在大多數情況下,這就像 Dictionary<TKey, TValue> 一樣運作,但它有一個額外的 Add 多載,允許一次新增另一個字典。當在物件初始化器中填入重複欄位時,這很方便。此外,MapField<TKey, TValue> 直接支援序列化、還原序列化和複製,但這通常由產生的程式碼使用,而不是手寫的應用程式碼。Map 中的鍵不允許為 Null;如果對應的單數欄位類型支援 Null 值,則值可能為 Null。

Oneof 欄位

oneof 中的每個欄位都有一個單獨的屬性,就像常規的單數欄位一樣。但是,編譯器還會產生一個額外的屬性來判斷已設定列舉中的哪個欄位,以及一個列舉和一個清除 oneof 的方法。例如,對於此 oneof 欄位定義:

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

編譯器將產生這些公用成員:

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

如果屬性是目前的 oneof「情況」,則擷取該屬性將傳回為該屬性設定的值。否則,擷取屬性將傳回屬性類型的預設值,一次只能設定 oneof 的一個成員。

設定 oneof 的任何組成屬性都會變更報告的 oneof「情況」。與常規單數欄位一樣,您不能將類型為 stringbytes 的 oneof 欄位設定為 Null 值。將訊息類型欄位設定為 Null 等同於呼叫 oneof 專用的 Clear 方法。

Wrapper 類型欄位

proto3 中的大多數已知類型都不會影響程式碼產生,但 wrapper 類型 (StringWrapperInt32Wrapper 等) 會變更屬性的類型和行為。

對應於 C# 值類型的所有 wrapper 類型 (Int32WrapperDoubleWrapperBoolWrapper 等) 都會對應到 Nullable<T>,其中 T 是對應的不可 Null 類型。例如,DoubleValue 類型的欄位會產生 Nullable<double> 類型的 C# 屬性。

StringWrapperBytesWrapper 類型的欄位會產生 stringByteString 類型的 C# 屬性,但預設值為 Null,並允許將 Null 設定為屬性值。

對於所有 wrapper 類型,重複欄位中不允許 Null 值,但允許作為 Map 條目的值。

列舉

給定類似以下的列舉定義:

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

protocol buffer 編譯器將產生一個名為 Color 的 C# 列舉類型,其中包含相同的數值集。列舉值的名稱會轉換為更符合 C# 開發人員的習慣:

  • 如果原始名稱以列舉名稱本身的大寫形式開頭,則會移除該部分。
  • 結果會轉換為 Pascal 大小寫。

因此,上面的 Color proto 列舉將變成以下 C# 程式碼:

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

此名稱轉換不會影響訊息 JSON 表示法中使用的文字。

請注意,.proto 語言允許多個列舉符號具有相同的數值。具有相同數值的符號是同義詞。這些在 C# 中的表示方式完全相同,多個名稱對應於相同的數值。

非巢狀列舉會導致 C# 列舉作為產生的新命名空間成員;巢狀列舉會導致 C# 列舉在與列舉巢狀所在的訊息對應的類別內的 Types 巢狀類別中產生。

服務

C# 程式碼產生器完全忽略服務。