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 推斷出來的,使用與檔案名稱相同的轉換規則。例如,example.high_score 的 proto 套件會產生 Example.HighScore 的命名空間。您可以使用 csharp_namespace 檔案選項覆寫特定 .proto 的預設產生命名空間。

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

作為所有這些規則的範例,請考慮 timestamp.proto 檔案,它是 Protocol Buffers 的一部分。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 編譯器會產生一個名為 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 的包裝函式型別除外。

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 方法。

包裝型別欄位

proto3 中大多數的知名類型不會影響程式碼產生,但是包裝類型 (StringWrapperInt32Wrapper 等) 會更改屬性的類型和行為。

所有對應 C# 值類型的包裝類型 (Int32WrapperDoubleWrapperBoolWrapper 等) 都會映射到 Nullable<T>,其中 T 是對應的不可為 null 的類型。例如,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# 列舉作為新的命名空間成員;巢狀列舉會導致產生一個 C# 列舉,位於對應於巢狀列舉所在訊息的類別中的 Types 巢狀類別內。

服務

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