当前位置: 首页 > news >正文

从零到一上手 Protocol Buffers用 C# 打造可演进的通讯录

一、为什么是 Protobuf(而不是 XML/自定义字符串/.NET 二进制序列化)

在需要把结构化对象持久化或跨进程/跨语言传输时,常见方案各有痛点:

  • BinaryFormatter 等 .NET 二进制序列化:对类型签名与版本极其脆弱、体积偏大,且跨语言互通性差
  • 自定义分隔字符串:一次性编码/解析成本高,健壮性与可读性差。
  • XML:可读但冗长、编解码开销大。

Protocol Buffers(Protobuf) 的优势在于:
用一个 .proto 文件声明消息结构,protoc 生成高效的 C# 类型,提供自动的二进制编码/解码;并且天然支持演进(老代码可读新消息、反之亦然)。

二、工程与示例骨架

本文示例是一个命令行通讯录工具:

  • 可新增联系人并写入文件;
  • 可读取文件并打印联系人列表。

完整示例(Program.csaddressbook.proto 等)可按本文步骤自行创建;若你使用官方示例仓库,也能在 examplescsharp/src/AddressBook 目录中找到等价实现。

三、定义协议:addressbook.proto

syntax = "proto3";
package tutorial;import "google/protobuf/timestamp.proto";// 覆盖 C# 默认命名空间(否则取 package 名)
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";message Person {string name = 1;int32  id   = 2;   // 唯一 IDstring email = 3;enum PhoneType {PHONE_TYPE_UNSPECIFIED = 0;PHONE_TYPE_MOBILE = 1;PHONE_TYPE_HOME   = 2;PHONE_TYPE_WORK   = 3;}message PhoneNumber {string   number = 1;PhoneType type  = 2;}repeated PhoneNumber phones = 4;google.protobuf.Timestamp last_updated = 5;
}message AddressBook {repeated Person people = 1;
}

要点速记

  1. package 防冲突;csharp_namespace 可定制命名空间。
  2. = 1/2/…标签号(wire tag),1–15 编码更省字节,优先留给常用repeated 字段。
  3. 未赋值字段使用类型默认值(数字 0、布尔 false、字符串空串、枚举 0 值)。
  4. repeated有序动态数组;集合类型在 C# 中为 RepeatedField<T>(只读集合,支持增删元素)。
  5. 嵌套 messageenum 提升结构清晰度;可以引入标准类型(如 Timestamp)。

四、编译生成 C# 代码

安装好 protoc 后执行:

protoc -I=$SRC_DIR --csharp_out=$DST_DIR $SRC_DIR/addressbook.proto

输出 Addressbook.cs,其中包含:

  • 静态 Addressbook(元数据);
  • AddressBookPersonPerson.Types.PhoneNumber 类;
  • Person.Types.PhoneType 枚举。

注意RepeatedField<T> 是只读集合属性,不能整体替换,只能调用 Add/Remove/Clear 等方法修改元素。

五、写与读:序列化 / 反序列化

1)写入

using System.IO;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Google.Protobuf.Examples.AddressBook;
using static Google.Protobuf.Examples.AddressBook.Person.Types;var john = new Person {Id = 1234,Name = "John Doe",Email = "jdoe@example.com",LastUpdated = Timestamp.FromDateTime(DateTime.UtcNow)
};
john.Phones.Add(new PhoneNumber { Number = "555-4321", Type = PhoneType.HOME });var book = new AddressBook();
book.People.Add(john);using var output = File.Create("addressbook.binpb");
book.WriteTo(output); // 二进制高效写入

2)读取

using var input = File.OpenRead("addressbook.binpb");
var parsed = AddressBook.Parser.ParseFrom(input);foreach (var p in parsed.People) {Console.WriteLine($"{p.Id} | {p.Name} | {p.Email}");foreach (var ph in p.Phones) {Console.WriteLine($"  - {ph.Type}: {ph.Number}");}
}

六、生成代码的常见模式

  • 属性访问:普通字段直接 get/set;repeated 用集合操作。
  • using static 简化枚举/嵌套名using static Person.Types; 之后可写 PhoneType.HOME
  • Debug 与 JSON:调试可用 ToString()(等同 JsonFormatter 的简版),生产落盘/传输请使用二进制 WriteTo/ParseFrom不要把调试输出当数据格式使用。

七、向前/向后兼容的演进法则

演进 .proto 需遵守:

  1. 不要修改已有字段的标签号
  2. 可以删除字段;
  3. 可以新增字段,但必须使用从未使用过的新标签号(包括曾被删除的号也不能复用)。

这样:

  • 旧程序读新消息:会忽略看不懂的新字段;
  • 新程序读旧消息:新加字段会呈现其默认值(比如空串/0/false)。

小贴士:把最常用/可能 repeated 的字段优先安排在 1–15,可观节省体积。

八、反射(Reflection)一键遍历任意消息

当你需要写通用逻辑(如通用打印、对比、转其它文本格式)时,反射很有用:

using Google.Protobuf;
using Google.Protobuf.Reflection;static void DumpTopLevel(IMessage msg) {var desc = msg.Descriptor;foreach (var f in desc.Fields.InDeclarationOrder()) {var val = f.Accessor.GetValue(msg);Console.WriteLine($"#{f.FieldNumber} {f.Name} = {val}");}
}

九、最佳实践与易踩坑

  • 不要把生成的 Protobuf 类当领域模型的“上帝类”:它们是数据载体。复杂业务建议用封装类包一层,便于隐藏实现细节与组合额外行为。
  • 文件扩展名约定:二进制 .binpb、文本 .txtpb、JSON .json,保持项目内一致性。
  • 调试输出 ≠ 数据格式:日志用调试格式即可;要传输/落盘请用二进制或显式 TextFormat/JSON
  • 版本演进前先留余量:给可能增长的 repeated 预留 1–15;对未来“也许有用”的字段先不上线,避免随后改 tag。
  • 单元测试:比较对象请用值相等Equals/字段比对)而非字节流相等;序列化非确定性,不同运行时/版本字节序可能不同但语义相同

十、把示例跑起来(最短路径)

  1. 安装 Google.Protobuf NuGet 与 protoc
  2. 写好 addressbook.proto
  3. 执行 protoc --csharp_out 生成 Addressbook.cs
  4. 在项目中引用生成文件与 Google.Protobuf
  5. 按第 节代码写入/读取,验证通讯录功能。

文章转载自:

http://hRzjnRIs.czgtt.cn
http://8Xy37ixk.czgtt.cn
http://uiKsTBu1.czgtt.cn
http://ew7392r8.czgtt.cn
http://FyKomGd7.czgtt.cn
http://ARrM9b61.czgtt.cn
http://JQtHA2PX.czgtt.cn
http://jbFzPgWn.czgtt.cn
http://MH5qPh7I.czgtt.cn
http://JtfJi4Km.czgtt.cn
http://nQzNqVPC.czgtt.cn
http://viSI1MaR.czgtt.cn
http://y8V5DSix.czgtt.cn
http://nQ7hfWDb.czgtt.cn
http://3iHpAg4d.czgtt.cn
http://TPgBRftb.czgtt.cn
http://sasiHUgH.czgtt.cn
http://XiDTit7Y.czgtt.cn
http://PPqFGll9.czgtt.cn
http://8EHdI5z8.czgtt.cn
http://Dtub805w.czgtt.cn
http://5eIVQ7pU.czgtt.cn
http://rquN8SRI.czgtt.cn
http://2nfFRVqF.czgtt.cn
http://nQcYZNHn.czgtt.cn
http://xT3OnUCV.czgtt.cn
http://atbENf2p.czgtt.cn
http://aSd4PpJS.czgtt.cn
http://FLfrI4gS.czgtt.cn
http://9pH8gAvu.czgtt.cn
http://www.dtcms.com/a/378626.html

相关文章:

  • 当DDoS穿上马甲:CC攻击的本质
  • 【ThreeJs】【自带依赖】Three.js 自带依赖指南
  • STM32短按,长按,按键双击实现
  • Flutter与原生混合开发:实现完美的暗夜模式同步方案
  • AT_abc422_f [ABC422F] Eat and Ride 题解
  • 面试问题详解十八:QT中自定义控件的三种实现方式
  • sql 中的 over() 窗口函数
  • Nginx优化与 SSL/TLS配置
  • Git远程操作(三)
  • 深入解析Spring AOP核心原理
  • 虫情测报仪:通过自动化、智能化的手段实现害虫的实时监测与预警
  • Python快速入门专业版(二十二):if语句进阶:嵌套if与条件表达式(简洁写法技巧)
  • 研发文档分类混乱如何快速查找所需内容
  • Java Web实现“十天内免登录”功能
  • CH347使用笔记:CH347在Vivado下的使用教程
  • 【linux内存管理】【基础知识 1】【pgd,p4d,pud,pmd,pte,pfn,pg,ofs,PTRS概念介绍】
  • 详解mcp以及agent java应用架构设计与实现
  • 硬件开发2-ARM裸机开发2-IMX6ULL
  • 电商网站被DDoS攻击了怎么办?
  • Java NIO的底层原理
  • QT 常用控件(概述、QWidget核心属性、按钮类控件、显示类控件、输入类控件、多元素控件、容器类控件、布局管理器)
  • MATLAB2-结构化编程和自定义函数-台大郭彦甫视频
  • 鸿蒙的编程软件的介绍
  • 鸿蒙审核问题——Scroll中嵌套了List/Grid时滑动问题
  • REDPlayer 鸿蒙原生视频播放库组件介绍与使用指南
  • HarmonyOS 应用开发深度解析:ArkUI 声明式 UI 与现代化状态管理最佳实践
  • redis 入门-1
  • Json-rpc通信项目(基于C++ Jsoncpp muduo库)
  • TODO的面试(dw三面、sqb二面、ks二面)
  • Vibe Coding实战项目:用Qwen3-Coder做了个AI跳舞视频生成器