C# 代码生成指南
在阅读本文档之前,您应该阅读 proto3 语言指南。
注意
protobuf 编译器可以从 3.10 版本开始为使用proto2
语法的定义生成 C# 接口。有关 proto2
定义的语义的详细信息,请参阅 proto2 语言指南,有关为 proto2 生成的 C# 代码的详细信息,请参阅 docs/csharp/proto2.md
(在 GitHub 上查看)。编译器调用
当使用 --csharp_out
命令行标志调用时,协议缓冲区编译器会生成 C# 输出。 --csharp_out
选项的参数是您希望编译器写入 C# 输出的目录,尽管根据 其他选项,编译器可能会创建指定目录的子目录。编译器为每个 .proto
文件输入创建一个源文件,默认扩展名为 .cs
,但可以通过编译器选项进行配置。
C# 代码生成器仅支持 proto3
消息。确保每个 .proto
文件都以以下声明开头
syntax = "proto3";
C# 特定选项
您可以使用 --csharp_opt
命令行标志向协议缓冲区编译器提供更多 C# 选项。支持的选项有
file_extension:设置生成代码的文件扩展名。默认为
.cs
,但常见的替代方案是.g.cs
,以指示该文件包含生成的代码。base_namespace:当指定此选项时,生成器会为生成的源代码创建目录层次结构,该结构对应于生成类的命名空间,并使用选项的值来指示应将命名空间的哪个部分视为输出目录的“base”。例如,使用以下命令行
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# 项目中的默认命名空间选项相对应。如果指定了该选项但值为空,则将使用生成文件中使用的完整 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 {}
协议缓冲区编译器生成一个名为 Foo
的密封 partial 类,它实现了 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# 属性。属性的确切性质取决于字段的性质:其类型,以及它是奇异字段、重复字段还是 map 字段。
奇异字段
任何奇异字段都会生成一个读/写属性。如果指定了 null 值,则 string
或 bytes
字段将生成 ArgumentNullException
;从尚未显式设置的字段中获取值将返回一个空字符串或 ByteString
。消息字段可以设置为 null 值,这实际上是清除字段。这不等同于将值设置为消息类型的“空”实例。
重复字段
每个重复字段都会生成一个类型为 Google.Protobuf.Collections.RepeatedField<T>
的只读属性,其中 T
是字段的元素类型。在大多数情况下,这类似于 List<T>
,但它有一个额外的 Add
重载,允许一次添加一个项目集合。这在对象初始值设定项中填充重复字段时很方便。此外,RepeatedField<T>
直接支持序列化、反序列化和克隆,但这通常由生成的代码而不是手动编写的应用程序代码使用。
重复字段不能包含 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 “case”,则获取该属性将返回为该属性设置的值。否则,获取该属性将返回属性类型的默认值 - 一次只能设置 oneof 的一个成员。
设置 oneof 的任何组成属性都将更改报告的 oneof “case”。与常规 奇异字段 一样,您不能将类型为 string
或 bytes
的 oneof 字段设置为 null 值。将消息类型字段设置为 null 等效于调用 oneof 特定的 Clear
方法。
Wrapper 类型字段
proto3 中的大多数常用类型不影响代码生成,但包装器类型(StringWrapper
、Int32Wrapper
等)会更改属性的类型和行为。
所有对应于 C# 值类型的包装器类型(Int32Wrapper
、DoubleWrapper
、BoolWrapper
等)都映射到 Nullable<T>
,其中 T
是相应的非空类型。例如,DoubleValue
类型的字段会生成 Nullable<double>
类型的 C# 属性。
类型为 StringWrapper
或 BytesWrapper
的字段会生成类型为 string
和 ByteString
的 C# 属性,但默认值为 null,并允许将 null 设置为属性值。
对于所有包装器类型,重复字段中不允许使用 null 值,但允许作为 map 条目的值。
枚举
给定如下枚举定义
enum Color {
COLOR_UNSPECIFIED = 0;
COLOR_RED = 1;
COLOR_GREEN = 5;
COLOR_BLUE = 1234;
}
协议缓冲区编译器将生成一个名为 Color
的 C# 枚举类型,其中包含相同的value集合。枚举值的名称会进行转换,使其更符合 C# 开发人员的习惯
- 如果原始名称以枚举名称本身的大写形式开头,则将其删除
- 结果将转换为 Pascal 大小写
因此,上面的 Color
proto 枚举将变为以下 C# 代码
enum Color
{
Unspecified = 0,
Red = 1,
Green = 5,
Blue = 1234
}
此名称转换不会影响消息的 JSON 表示形式中使用的文本。
请注意,.proto
语言允许多个枚举符号具有相同的数值。具有相同数值的符号是同义词。这些在 C# 中以完全相同的方式表示,多个名称对应于相同的数值。
非嵌套枚举会导致生成 C# 枚举作为新的命名空间成员;嵌套枚举会导致在与枚举嵌套在其中的消息对应的类中的 Types
嵌套类中生成 C# 枚举。
服务
C# 代码生成器完全忽略服务。