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.Game的csharp_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 值,string 或 bytes 欄位將生成 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”。與常規的 單一欄位 一樣,您不能將具有 string 或 bytes 型別的 oneof 欄位設定為 null 值。將訊息型別欄位設定為 null 等同於呼叫 oneof 特定的 Clear 方法。
包裝器型別欄位
大多數知名型別不影響程式碼生成,但包裝器型別(如 StringWrapper 和 Int32Wrapper)會改變屬性的型別和行為。
所有對應於 C# 值型別(如 Int32Wrapper、DoubleWrapper 和 BoolWrapper)的包裝器型別都對映到 Nullable<T>,其中 T 是相應的不可空型別。例如,DoubleValue 型別的欄位會生成一個型別為 Nullable<double> 的 C# 屬性。
StringWrapper 或 BytesWrapper 型別的欄位會生成型別為 string 和 ByteString 的 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# 程式碼生成器完全忽略服務。