C#中如何将复杂数据结构保存并加载到二进制文件?
问题描述
我正在编写一个C#程序,需要按以下指定结构保存和加载对象列表:
SIGNATURE\r\n SIZE\r\n OBJECT_DATA\r\n SIZE\r\n OBJECT_DATA\r\n \r\n
SIGNATURE:带私钥的原始字符串,用来区分我的数据文件和其他文件SIZE:整数值,表示OBJECT_DATA占用的字节数OBJECT_DATA:存储数据的类实例
已尝试方案
StackOverflow上找到的方案几乎都用BinaryFormatter,但它不安全不能用。我也了解了protobuf-net,但不知道怎么应用到我的场景里。
我的思路
序列化流程(输入/输出为二进制文件)
- 打开输出文件并写入signature
- 遍历源对象列表
- 计算每个项的大小并写入文件
- 将对象数据写入二进制文件(这一步还不清楚怎么做)
- 写入
\r\n标记该项结束
反序列化流程
- 打开输入文件并读取Signature
- 若Signature与密钥匹配:
- 循环至文件末尾
- 读取SIZE值
- 读取对象并创建其类实例
- 循环至文件末尾
更新信息
OBJECT_DATA是以下抽象类的实例:
public abstract class IShape { public State Configuration { get; set; } public abstract string Name { get; } public List<Point> Points { get; set; } = []; public abstract UIElement Draw(); public abstract IShape Clone(); public BitmapImage Preview { get; set; } }
State Configuration的结构如下:
public class State { public DoubleCollection StrokeDashArray { get; set; } public SolidColorBrush Stroke { get; set; } public SolidColorBrush Fill { get; set; } public double StrokeThickness { get; set; } }
我觉得Protobuf-net是把对象序列化到Stream的好方法,但我的类包含System.Windows.Media里的类型,不确定能不能用它序列化。
解决方案
一、适配WPF类型的自定义转换器
protobuf-net默认不支持System.Windows.Media下的类型,需要为这些类型编写序列化/反序列化转换器:
- SolidColorBrush转换器
public class SolidColorBrushConverter : ITypeConverter<SolidColorBrush, uint> { public uint Convert(SolidColorBrush value) { return value.Color.R << 16 | value.Color.G << 8 | value.Color.B; } public SolidColorBrush ConvertBack(uint value) { var color = Color.FromRgb((byte)(value >> 16), (byte)(value >> 8), (byte)value); return new SolidColorBrush(color); } }
- DoubleCollection转换器
public class DoubleCollectionConverter : ITypeConverter<DoubleCollection, List<double>> { public List<double> Convert(DoubleCollection value) { return value?.Cast<double>().ToList() ?? new List<double>(); } public DoubleCollection ConvertBack(List<double> value) { return new DoubleCollection(value); } }
- BitmapImage转换器
public class BitmapImageConverter : ITypeConverter<BitmapImage, byte[]> { public byte[] Convert(BitmapImage value) { if (value == null) return null; using (var ms = new MemoryStream()) { var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(value)); encoder.Save(ms); return ms.ToArray(); } } public BitmapImage ConvertBack(byte[] value) { if (value == null) return null; using (var ms = new MemoryStream(value)) { var image = new BitmapImage(); image.BeginInit(); image.CacheOption = BitmapCacheOption.OnLoad; image.StreamSource = ms; image.EndInit(); return image; } } }
二、标记可序列化类
为抽象类和子类添加protobuf-net特性,抽象类需注册所有子类:
[ProtoContract] [ProtoInclude(1, typeof(Circle))] // 替换为你的IShape实际子类 [ProtoInclude(2, typeof(Rectangle))] public abstract class IShape { [ProtoMember(1)] public State Configuration { get; set; } [ProtoMember(2)] public abstract string Name { get; } [ProtoMember(3)] public List<Point> Points { get; set; } = []; // 抽象方法无需序列化,标记忽略 [ProtoIgnore] public abstract UIElement Draw(); [ProtoIgnore] public abstract IShape Clone(); [ProtoMember(4, Converter = typeof(BitmapImageConverter))] public BitmapImage Preview { get; set; } } [ProtoContract] public class State { [ProtoMember(1, Converter = typeof(DoubleCollectionConverter))] public DoubleCollection StrokeDashArray { get; set; } [ProtoMember(2, Converter = typeof(SolidColorBrushConverter))] public SolidColorBrush Stroke { get; set; } [ProtoMember(3, Converter = typeof(SolidColorBrushConverter))] public SolidColorBrush Fill { get; set; } [ProtoMember(4)] public double StrokeThickness { get; set; } }
三、实现指定格式的序列化/反序列化逻辑
1. 序列化方法
public static void SaveShapes(List<IShape> shapes, string filePath, string signature) { using (var fs = new FileStream(filePath, FileMode.Create)) using (var writer = new BinaryWriter(fs, Encoding.UTF8, leaveOpen: true)) { // 写入签名 writer.Write(signature); writer.Write("\r\n"); foreach (var shape in shapes) { // 序列化对象到内存流,计算字节大小 using (var ms = new MemoryStream()) { Serializer.Serialize(ms, shape); var size = (int)ms.Length; // 写入SIZE和换行符 writer.Write(size.ToString()); writer.Write("\r\n"); // 写入对象数据 ms.Seek(0, SeekOrigin.Begin); ms.CopyTo(fs); // 写入项结束标记 writer.Write("\r\n"); } } // 写入文件结束标记 writer.Write("\r\n"); } }
2. 反序列化方法
public static List<IShape> LoadShapes(string filePath, string expectedSignature) { var shapes = new List<IShape>(); using (var fs = new FileStream(filePath, FileMode.Open)) using (var reader = new BinaryReader(fs, Encoding.UTF8, leaveOpen: true)) { // 验证签名 var signatureLine = ReadLine(reader); if (signatureLine != expectedSignature) { throw new InvalidDataException("文件签名不匹配"); } while (true) { // 读取SIZE行 var sizeLine = ReadLine(reader); if (string.IsNullOrEmpty(sizeLine)) break; if (!int.TryParse(sizeLine, out var size)) { throw new InvalidDataException("无效的SIZE值"); } // 读取对象数据 var buffer = new byte[size]; var bytesRead = reader.Read(buffer, 0, size); if (bytesRead != size) { throw new InvalidDataException("读取对象数据不完整"); } // 反序列化对象 using (var ms = new MemoryStream(buffer)) { var shape = Serializer.Deserialize<IShape>(ms); shapes.Add(shape); } // 跳过项结束的\r\n ReadLine(reader); } } return shapes; } // 辅助方法:读取一行(直到\r\n) private static string ReadLine(BinaryReader reader) { var sb = new StringBuilder(); while (true) { int b; try { b = reader.ReadByte(); } catch (EndOfStreamException) { break; } if (b == '\r') { reader.ReadByte(); // 跳过\n break; } sb.Append((char)b); } return sb.ToString(); }
四、注意事项
- 所有
IShape的子类必须添加[ProtoContract]特性,并在抽象类的[ProtoInclude]中注册 System.Windows.Point类型protobuf-net默认支持,无需额外处理- 自定义转换器可按需调整(比如BitmapImage改用JPEG编码减少文件体积)
内容的提问来源于stack exchange,提问作者Queensalats
相关产品推荐
相关产品推荐

