如何在C#中实现无第三方库的自定义数据序列化与反序列化?
在C#中不依赖第三方库实现自定义二进制序列化/反序列化(处理复杂层级结构与对象关系)
我正在做一个项目,需要在C#里不依赖任何第三方库实现自定义的序列化和反序列化逻辑,核心要处理复杂的数据结构并保留对象关系。目前我有一组带层级结构的自定义类,需要把这些类的实例转换成自定义二进制格式,用来高效存储和传输。
以下是简化后的类层级示例:
public class Person { public string Name { get; set; } public int Age { get; set; } public List<Address> Addresses { get; set; } } public class Address { public string Street { get; set; } public string City { get; set; } }
想请教下针对这类场景实现序列化/反序列化的最佳实践、适用的设计模式以及可以利用的C#特性。
一、核心最佳实践
- 固定二进制格式规范:先明确每个类型的二进制布局,比如基础类型(int、string)的存储规则,集合的前缀标识(先存集合长度,再逐个存元素)。例如string可先存int类型的字节长度,再存UTF-8字节数组;
List<T>先存元素个数,再依次序列化每个T实例。 - 处理对象引用与循环引用:如果存在重复引用或循环引用(比如Person实例引用另一个Person),给每个对象分配唯一ID,序列化时先存ID;反序列化时维护对象缓存字典,用ID映射已创建的对象,避免重复创建或死循环。
- 版本兼容性:在二进制流开头加入版本号字段,后续格式变更时,反序列化可根据版本号适配不同解析逻辑,避免旧数据无法读取。
- 验证与错误处理:序列化/反序列化过程中加入校验(比如校验集合长度是否合法、字符串字节数是否匹配),抛出明确异常信息,方便调试。
- 性能优化:使用
BinaryWriter/BinaryReader替代手动字节操作,降低出错概率;对频繁序列化的类型,提前缓存反射信息(如PropertyInfo、FieldInfo),减少反射开销;性能要求极高时,可生成IL代码动态实现序列化逻辑。
二、适用的设计模式
- 策略模式:为不同类型(如Person、Address、
List<T>)定义各自的序列化/反序列化策略类,核心序列化器根据类型选择对应策略执行,便于扩展新类型。 - 工厂模式:创建序列化器工厂,根据类型或标识生成对应的处理实例,解耦类型与处理逻辑。
- 备忘录模式:序列化本质是将对象状态保存为二进制“备忘录”,反序列化是恢复状态,该模式天然适配序列化场景。
三、可利用的C#特性
BinaryWriter/BinaryReader:.NET基础库自带的二进制流读写类,可直接处理基础类型(int、bool、byte[]等),比手动操作MemoryStream更高效安全。- 反射(Reflection):通过反射自动遍历类的属性/字段,无需为每个类写硬编码序列化逻辑,适配复杂层级结构。可用
Type.GetProperties()或Type.GetFields()获取成员,结合自定义特性标记需要序列化的成员。 - 自定义特性(Custom Attributes):定义
[SerializableMember]特性标记需序列化的属性/字段,过滤无关成员;还可通过特性指定成员的二进制顺序、版本兼容信息等。 - 泛型(Generics):针对
List<T>、Dictionary<TKey, TValue>等集合类型编写泛型序列化方法,无需为每个集合类型单独实现逻辑。 IDisposable:BinaryWriter/BinaryReader和MemoryStream均实现IDisposable,用using语句确保资源正确释放,避免内存泄漏。
示例实现片段
以下是针对Person和Address的极简序列化/反序列化示例:
序列化逻辑
public static byte[] SerializePerson(Person person) { using var ms = new MemoryStream(); using var writer = new BinaryWriter(ms); // 序列化Name:先存长度,再存字节 var nameBytes = Encoding.UTF8.GetBytes(person.Name); writer.Write(nameBytes.Length); writer.Write(nameBytes); // 序列化Age writer.Write(person.Age); // 序列化Addresses:先存数量,再逐个序列化 writer.Write(person.Addresses.Count); foreach (var addr in person.Addresses) { var streetBytes = Encoding.UTF8.GetBytes(addr.Street); writer.Write(streetBytes.Length); writer.Write(streetBytes); var cityBytes = Encoding.UTF8.GetBytes(addr.City); writer.Write(cityBytes.Length); writer.Write(cityBytes); } return ms.ToArray(); }
反序列化逻辑
public static Person DeserializePerson(byte[] data) { using var ms = new MemoryStream(data); using var reader = new BinaryReader(ms); var person = new Person(); // 反序列化Name var nameLength = reader.ReadInt32(); var nameBytes = reader.ReadBytes(nameLength); person.Name = Encoding.UTF8.GetString(nameBytes); // 反序列化Age person.Age = reader.ReadInt32(); // 反序列化Addresses var addrCount = reader.ReadInt32(); person.Addresses = new List<Address>(addrCount); for (int i = 0; i < addrCount; i++) { var addr = new Address(); var streetLength = reader.ReadInt32(); var streetBytes = reader.ReadBytes(streetLength); addr.Street = Encoding.UTF8.GetString(streetBytes); var cityLength = reader.ReadInt32(); var cityBytes = reader.ReadBytes(cityLength); addr.City = Encoding.UTF8.GetString(cityBytes); person.Addresses.Add(addr); } return person; }
内容的提问来源于stack exchange,提问作者Mehran Khan
相关产品推荐
相关产品推荐

