protobuf C 如何启用实验性 proto2 支持并使用 HasXYZ/ClearXYZ 与 extension API
2026/9/12 1:43:23 网站建设 项目流程

protobuf C# 如何启用实验性 proto2 支持并使用 HasXYZ/ClearXYZ 与 extension API

【免费下载链接】protobufProtocol Buffers - Google's data interchange format项目地址: https://gitcode.com/GitHub_Trending/pr/protobuf

如果你的 C# 项目需要与遗留系统交换 proto2 格式的数据(required/optional字段、字段 presence、扩展字段),就需要在 Google.Protobuf 中启用 proto2 支持。自 Google.Protobuf 3.10 版本起,proto2 支持以实验性(experimental)方式发布:它不破坏现有 proto3 用法,但 proto2 相关生成代码和公共 API 可能随反馈新增、删除或调整,源码中IExtendableMessage<T>接口也明确标注 "experimental and is subject to change"(见 IExtendableMessage.cs)。本文按 docs/csharp/proto2.md 与 csharp/README.md 说明如何生成并使用这部分 API。

准备条件:安装 NuGet 包与 protoc

按照 csharp/README.md 的说明,C# 运行时最简使用方式是通过 NuGet 包:

  • Google.Protobuf:运行时库。其目标框架为 .NET 4.5+(net45)、.NET Standard 1.1 和 2.0(netstandard1.1/netstandard2.0)、.NET 5+(net50);不支持 .NET 3.5。
  • Google.Protobuf.Tools:包含预编译的protoc.exe以及包内tools目录下的 well known.proto文件副本。

生成 C# 代码时,对.proto文件调用protoc并加上--csharp_out选项。另外,用旧编译器(C# 7.2 之前)编译生成代码时,需要在项目中定义GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE符号,使生成类不实现使用ref struct类型的IBufferMessage接口。

启用 proto2:在 .proto 文件中声明 syntax

proto2 特性只在带syntax = "proto2";声明的 proto2 文件中生效,与其他语言的用法一致。文档同时强调 proto3 仍是推荐版本,proto2 支持面向遗留系统互操作(legacy system interop)和高级用途。对这样的文件运行protoc --csharp_out后,生成代码会额外包含字段 presence 和扩展相关的成员。

使用 HasXYZ/ClearXYZ 处理 optional/required 字段

proto2 中的 message 与 proto3 类似,提供普通的属性读写,同时为字段 presence 增加了属性和方法:对optional/required字段XYZ,生成代码包含用于检查 presence 的HasXYZ属性和用于清除值的ClearXYZ方法(来源:docs/csharp/proto2.md)。

文档中的 proto 示例:

message Foo { optional Bar bar = 1; required Baz baz = 2; }

对应的文档示例代码(示例结果,展示 presence 行为):

var foo = new Foo(); Assert.IsNull(foo.Bar); Assert.False(foo.HasBar); foo.Bar = new Bar(); Assert.True(foo.HasBar); foo.ClearBar();

即:未赋值时HasBar为 false,赋值后为 true,ClearBar()清除后可回到未设置状态。

使用 extension API:IExtendableMessage 与生成的扩展容器

定义了 extension range 的 message 会实现IExtendableMessage<T>接口,提供以下方法(完整定义见 IExtendableMessage.cs):

  • GetExtension<TValue>(Extension<T, TValue>):读取单值扩展;扩展不存在时返回默认值。
  • GetExtension<TValue>(RepeatedExtension<T, TValue>):读取 repeated 扩展;未设置时返回null以避免不必要的分配。
  • GetOrInitializeExtension<TValue>(RepeatedExtension<T, TValue>):读取 repeated 扩展并初始化,不会返回null
  • SetExtension<TValue>(Extension<T, TValue>, TValue):设置扩展值。
  • HasExtension<TValue>(Extension<T, TValue>):判断扩展是否已设置。
  • ClearExtension(单值与 repeated 两个重载):从消息中移除扩展。

扩展会生成为静态容器,方便取用。文档示例:文件foo.proto中定义在文件作用域的扩展会生成FooExtensions类;嵌套在 message 内的extend则会生成嵌套的Extensions类。可以用using static把所有扩展带入作用域:

option csharp_namespace = "FooBar"; extend Foo { optional Baz foo_ext = 124; } message Baz { extend Foo { repeated Baz repeated_foo_ext = 125; } }

生成的 C# 结构(/* initialization */为文档中省略的初始化代码):

public static partial class FooExtensions { public static readonly Extension<Foo, Baz> FooExt = /* initialization */; } public partial class Baz { public partial static class Extensions { public static readonly RepeatedExtension<Foo, Baz> RepeatedFooExt = /* initialization */; } }

使用示例:

using static FooBar.FooExtensions; using static FooBar.Baz.Extensions; var foo = new Foo(); foo.SetExtension(FooExt, new Baz()); foo.GetOrInitializeExtension(RepeatedFooExt).Add(new Baz());

解析带扩展的消息:ExtensionRegistry

从输入流解析消息时,扩展不会自动生效,需要构造ExtensionRegistry并交给解析器。文档示例(其中HasExtension的断言用于验证注册与否的差异):

var registry = new ExtensionRegistry() { Baz.Extensions.FooExt }; var foo = Foo.Factory.WithExtensionRegistry(registry).ParseFrom(input); Assert.True(foo.HasExtension(Baz.Extensions.FooExt)); var fooNoRegistry = Foo.Factory.ParseFrom(input); Assert.False(fooNoRegistry.HasExtension(Baz.Extensions.FooExt));

WithExtensionRegistry返回一个新的、配置了该注册表的解析器(见 MessageParser.cs 中的MessageParser.WithExtensionRegistryMessageParser<T>.WithExtensionRegistry)。上面的示例同时给出了验证方式:注册了 registry 的解析结果HasExtension为 true,未注册的为 false。

如果不想手工罗列扩展,可以用文档中扩展后的反射 API 动态构建注册表:

var extensions = Baz.Descriptor.Extensions.GetExtensionsInDeclarationOrder(Foo.Descriptor); var registry = new ExtensionRegistry(); registry.AddRange(extensions.Select(f => f.Extension)); var baz = Foo.Descriptor.Parser.WithExtensionRegistry(registry).ParseFrom(input); foreach (var field in extensions) { if (field.Accessor.HasValue(baz)) { Console.WriteLine($"{field.Name}: {field.Accessor.GetValue(baz)}"); } }

配套的新增反射成员包括:FieldDescriptor.Extension(取扩展字段背后的扩展标识符,可加入ExtensionRegistry)、FieldDescriptor.IsExtensionFieldDescriptor.ExtendeeTypeIFieldAccessor.HasValue(判断字段值是否已设置;对 proto3 字段会抛InvalidOperationException)、FileDescriptor.SyntaxFileDescriptor.ExtensionsMessageDescriptor.Extensions

required 字段与 IsInitialized 检查

proto2 消息中存在 required 字段时,"初始化"指 required 字段(包括子消息中的)是否全部已设置。这个实现中,解析器和输入流不会自行检查消息的初始化状态或抛错——处理缺失 required 字段的方式由你自行决定。文档给出的检查方法是MessageExtensions中的IsInitialized扩展方法(见 MessageExtensions.cs),在解析后手动调用即可判断消息是否完整。

迁移限制与注意事项

  • proto2 生成代码与公共 API 是实验性的,后续版本可能新增、删除或调整,迁移时不要假定当前 API 形状长期稳定。
  • CustomOptionsAPI 已被弃用。文档建议改用新生成的扩展标识符,通过GetOptionAPI 安全地访问自定义选项(repeated 字段和 message 等可克隆值会深拷贝)。
  • 反射访问扩展时注意IFieldAccessor.HasValue仅适用于 proto2 字段,对 proto3 字段会抛InvalidOperationException

完成上述步骤后,可验证的结果是:生成的 proto2 消息类具备HasXYZ/ClearXYZ成员,using static引入的扩展可通过SetExtension/GetExtension读写,且只有经过WithExtensionRegistry配置的解析器才能把线上扩展解析进消息(以HasExtension的返回值为判断依据)。

【免费下载链接】protobufProtocol Buffers - Google's data interchange format项目地址: https://gitcode.com/GitHub_Trending/pr/protobuf

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询