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
推斷出來的,使用與檔案名稱相同的轉換規則。例如,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 值,string
或 bytes
欄位將產生 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「情況」。與常規單數欄位一樣,您不能將類型為 string
或 bytes
的 oneof 欄位設定為 Null 值。將訊息類型欄位設定為 Null 等同於呼叫 oneof 專用的 Clear
方法。
Wrapper 類型欄位
proto3 中的大多數已知類型都不會影響程式碼產生,但 wrapper 類型 (StringWrapper
、Int32Wrapper
等) 會變更屬性的類型和行為。
對應於 C# 值類型的所有 wrapper 類型 (Int32Wrapper
、DoubleWrapper
、BoolWrapper
等) 都會對應到 Nullable<T>
,其中 T
是對應的不可 Null 類型。例如,DoubleValue
類型的欄位會產生 Nullable<double>
類型的 C# 屬性。
StringWrapper
或 BytesWrapper
類型的欄位會產生 string
和 ByteString
類型的 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# 程式碼產生器完全忽略服務。