C# 程式碼產生指南
您應該先閱讀 proto3 語言指南,再閱讀本文件。
注意
從 3.10 版本開始,protobuf 編譯器可以為使用proto2
語法的定義產生 C# 介面。有關 proto2
定義語意的詳細資訊,請參閱 proto2 語言指南,以及參閱 docs/csharp/proto2.md
(在 GitHub 上檢視),以取得有關為 proto2 產生的 C# 程式碼的詳細資訊。編譯器調用
當使用 --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.proto
的csharp_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 值,則 string
或 bytes
欄位將產生 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 報告的「情況」。如同一般的單數欄位,您不能將具有 string
或 bytes
類型的 oneof 欄位設定為 null 值。將訊息類型欄位設定為 null 等同於呼叫 oneof 專用的 Clear
方法。
包裝型別欄位
proto3 中大多數的知名類型不會影響程式碼產生,但是包裝類型 (StringWrapper
、Int32Wrapper
等) 會更改屬性的類型和行為。
所有對應 C# 值類型的包裝類型 (Int32Wrapper
、DoubleWrapper
、BoolWrapper
等) 都會映射到 Nullable<T>
,其中 T
是對應的不可為 null 的類型。例如,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# 列舉作為新的命名空間成員;巢狀列舉會導致產生一個 C# 列舉,位於對應於巢狀列舉所在訊息的類別中的 Types
巢狀類別內。
服務
C# 程式碼產生器會完全忽略服務。