C# 生成程式碼指南

精確描述了協議緩衝區編譯器使用版本語法為協議定義生成的 C# 程式碼。

在閱讀本文件之前,您應該閱讀 proto2 語言指南proto3 語言指南版本語言指南

編譯器呼叫

當使用 --csharp_out 命令列標誌呼叫時,協議緩衝區編譯器會生成 C# 輸出。--csharp_out 選項的引數是您希望編譯器寫入 C# 輸出的目錄,儘管根據 其他選項,編譯器可能會在指定的目錄中建立子目錄。編譯器為每個 .proto 檔案輸入建立一個原始檔,預設副檔名為 .cs,但可透過編譯器選項進行配置。

C# 特定選項

您可以使用 --csharp_opt 命令列標誌向協議緩衝區編譯器提供進一步的 C# 選項。支援的選項有

  • file_extension:設定生成程式碼的副檔名。預設值為 .cs,但一個常見的替代方案是 .g.cs,表示檔案包含生成程式碼。

  • base_namespace:當指定此選項時,生成器會為生成的原始碼建立目錄層次結構,該層次結構與生成類的名稱空間相對應,並使用選項的值來指示名稱空間的哪個部分應被視為輸出目錄的“基”。例如,使用以下命令列

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

    其中 player.proto 具有 Example.Gamecsharp_namespace 選項,協議緩衝區編譯器將生成一個檔案 src/Game/Player.cs。此選項通常對應於 Visual Studio 中 C# 專案的 default namespace 選項。如果指定了該選項但值為為空,則生成的檔案中使用的完整 C# 名稱空間將用於目錄層次結構。如果根本未指定該選項,則生成的檔案將簡單地寫入 --csharp_out 指定的目錄,而不會建立任何層次結構。

  • internal_access:當指定此選項時,生成器會使用 internal 訪問修飾符而不是 public 來建立型別。

  • serializable:當指定此選項時,生成器會將 [Serializable] 屬性新增到生成的 Message 類。

可以使用逗號分隔多個選項,如以下示例所示

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 推斷,使用與檔名相同的轉換規則。例如,example.high_score 的 proto 包將導致 Example.HighScore 名稱空間。您可以使用 csharp_namespace 檔案選項 覆蓋特定 .proto 的預設生成名稱空間。

每個頂級列舉和訊息都會導致在名稱空間成員中宣告一個列舉或類。此外,總是會為檔案描述符生成一個靜態部分類。這用於基於反射的操作。描述符類被賦予與檔案相同的名稱,不帶副檔名。但是,如果存在同名的訊息(這種情況很常見),描述符類會放置在巢狀的 Proto 名稱空間中以避免與訊息衝突。

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

edition = "2023";

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

協議緩衝區編譯器會生成一個密封的部分類 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#。

欄位

協議緩衝區編譯器為訊息中定義的每個欄位生成一個 C# 屬性。屬性的具體性質取決於欄位的性質:其型別,以及它是單一欄位、重複欄位還是對映欄位。

單一欄位

任何單一欄位都會生成一個讀/寫屬性。如果指定了 null 值,stringbytes 欄位將生成 ArgumentNullException;從未明確設定的欄位中獲取值將返回空字串或 ByteString。訊息欄位可以設定為 null 值,這實際上是清除欄位。這不等同於將值設定為空訊息型別例項。

重複欄位

每個重複欄位都會生成一個型別為 Google.Protobuf.Collections.RepeatedField<T> 的只讀屬性,其中 T 是欄位的元素型別。在大多數情況下,它的行為類似於 List<T>,但它有一個額外的 Add 過載,允許一次新增一組項。這在物件初始化器中填充重複欄位時很方便。此外,RepeatedField<T> 直接支援序列化、反序列化和克隆,但這通常由生成程式碼使用,而不是手動編寫的應用程式程式碼。

重複欄位不能包含 null 值,即使是訊息型別,除了 下面解釋的 可空包裝器型別。

對映欄位

每個對映欄位都會生成一個型別為 Google.Protobuf.Collections.MapField<TKey, TValue> 的只讀屬性,其中 TKey 是欄位的鍵型別,TValue 是欄位的值型別。在大多數情況下,它的行為類似於 Dictionary<TKey, TValue>,但它有一個額外的 Add 過載,允許一次新增另一個字典。這在物件初始化器中填充重複欄位時很方便。此外,MapField<TKey, TValue> 直接支援序列化、反序列化和克隆,但這通常由生成程式碼使用,而不是手動編寫的應用程式程式碼。對映中的鍵不允許為 null;如果相應的單一欄位型別支援 null 值,則值可以為 null。

Oneof 欄位

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“case”,則獲取該屬性將返回為該屬性設定的值。否則,獲取該屬性將返回該屬性型別的預設值——oneof 的成員一次只能設定一個。

設定 oneof 的任何組成屬性都將更改 oneof 報告的“case”。與常規的 單一欄位 一樣,您不能將具有 stringbytes 型別的 oneof 欄位設定為 null 值。將訊息型別欄位設定為 null 等同於呼叫 oneof 特定的 Clear 方法。

包裝器型別欄位

大多數知名型別不影響程式碼生成,但包裝器型別(如 StringWrapperInt32Wrapper)會改變屬性的型別和行為。

所有對應於 C# 值型別(如 Int32WrapperDoubleWrapperBoolWrapper)的包裝器型別都對映到 Nullable<T>,其中 T 是相應的不可空型別。例如,DoubleValue 型別的欄位會生成一個型別為 Nullable<double> 的 C# 屬性。

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

對於所有包裝器型別,重複欄位中不允許使用 null 值,但允許作為對映條目的值。

列舉

給定一個列舉定義,例如

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

協議緩衝區編譯器將生成一個名為 Color 的 C# 列舉型別,具有相同的值集。列舉值的名稱將進行轉換,使其更符合 C# 開發人員的習慣用法

  • 如果原始名稱以列舉名稱本身的大寫形式開頭,則將其刪除
  • 結果將轉換為 Pascal 大小寫

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

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

這種名稱轉換不影響訊息 JSON 表示中使用的文字。

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

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

服務(Services)

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