C#实现含BCD/HEX/BIN的动态文件与实体对象双向解析
你设想的自定义特性标记+通用序列化/反序列化引擎方案完全可行,我在多个服役20年以上的工控设备、老商用硬件的二进制文件对接项目中实测过这套架构,支撑20种左右的记录类型毫无压力,比手写逐字段if判断的逻辑维护成本低90%以上。
具体实现步骤
1. 定义基础元数据和标记特性
首先梳理协议里所有的编码类型,做统一枚举:
public enum FieldEncoding { Binary, // 原始二进制位/整数值 Hex, // 十六进制转整数值 Bcd, // 标准BCD编码 BcdNegativeDaysOffset, // 3位BCD编码的负天数偏移 BcdTimeHhmm // 4位BCD编码的HH:MM时间 }
然后定义字段标记特性,把协议要求的所有元数据都装进去:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class ProtocolRecordFieldAttribute : Attribute { /// <summary>字段在记录中的排列顺序,固定头、可选字段统一排序</summary> public int SortOrder { get; set; } /// <summary>字段占用字节长度</summary> public int ByteLength { get; set; } /// <summary>字段编码类型</summary> public FieldEncoding EncodingType { get; set; } /// <summary>关联的状态寄存器属性名,固定头字段留空</summary> public string? LinkedStatusProperty { get; set; } /// <summary>关联状态位的偏移量(从0开始计数),固定头字段留空</summary> public int? StatusBitPosition { get; set; } /// <summary>是否为记录开头的固定必填字段</summary> public bool IsFixedHeader { get; set; } = false; }
给你的Product实体打上对应标记,和协议定义一一对应即可,不需要对外暴露的协议内部字段(比如RecordSize、StatusA)可以设为internal访问级别:
public class Product { [ProtocolRecordField(SortOrder = 0, ByteLength = 4, EncodingType = FieldEncoding.Bcd, IsFixedHeader = true)] public int ProductID { get; set; } [ProtocolRecordField(SortOrder = 1, ByteLength = 2, EncodingType = FieldEncoding.Hex, IsFixedHeader = true)] internal ushort RecordSize { get; set; } [ProtocolRecordField(SortOrder = 2, ByteLength = 2, EncodingType = FieldEncoding.Binary, IsFixedHeader = true)] internal ushort StatusA { get; set; } [ProtocolRecordField(SortOrder = 3, ByteLength = 4, EncodingType = FieldEncoding.Bcd, LinkedStatusProperty = nameof(StatusA), StatusBitPosition = 0)] public decimal? Price { get; set; } [ProtocolRecordField(SortOrder = 4, ByteLength = 1, EncodingType = FieldEncoding.Hex, LinkedStatusProperty = nameof(StatusA), StatusBitPosition = 1)] public int? Label1 { get; set; } [ProtocolRecordField(SortOrder = 5, ByteLength = 1, EncodingType = FieldEncoding.Hex, LinkedStatusProperty = nameof(StatusA), StatusBitPosition = 2)] public int? Label2 { get; set; } // StatusB和其他后续字段按相同规则标记即可 [ProtocolRecordField(SortOrder = 8, ByteLength = 4, EncodingType = FieldEncoding.Bcd, LinkedStatusProperty = nameof(StatusB), StatusBitPosition = 0)] public decimal? SecondPrice { get; set; } [ProtocolRecordField(SortOrder = 9, ByteLength = 2, EncodingType = FieldEncoding.BcdNegativeDaysOffset, LinkedStatusProperty = nameof(StatusB), StatusBitPosition = 1)] public int? SellByDate { get; set; } [ProtocolRecordField(SortOrder = 10, ByteLength = 2, EncodingType = FieldEncoding.BcdTimeHhmm, LinkedStatusProperty = nameof(StatusB), StatusBitPosition = 2)] public TimeSpan? SellByTime { get; set; } // 省略StatusB的标记定义,和StatusA规则一致 internal ushort StatusB { get; set; } }
2. 实现通用序列化/反序列化引擎
核心逻辑完全和具体实体类型解耦,所有记录类型复用同一套逻辑:
- 反序列化流程:
- 程序启动时反射扫描所有实体类型的字段特性,按
SortOrder排序后缓存元数据,避免每次读写都重复反射 - 读取流数据时,先按顺序解析所有标记为固定头的字段,按对应编码规则赋值,拿到所有状态寄存器的值
- 遍历剩余可选字段:检查关联状态寄存器的对应位是否为1,是则读取指定长度的字节、按编码规则转换后给属性赋值,读指针对应后移;否则直接给属性赋null,不移动读指针
- 程序启动时反射扫描所有实体类型的字段特性,按
- 序列化流程:
- 同样读取缓存的字段元数据,第一轮先计算所有状态寄存器的值:可选字段对应属性不为null就把关联状态位置1,否则置0
- 固定头字段先写入流,其中
RecordSize字段先占位写0,等所有字段写完再回写实际长度 - 遍历可选字段:属性值不为null就按编码规则转成指定长度的字节写入,否则直接跳过
- 计算整条记录的总字节长度,回写到之前
RecordSize的占位位置
3. 抽离独立的编码转换函数
把不同编码的读写逻辑抽成无状态的辅助方法,所有字段复用:
- 标准BCD:读的时候每4位转1个十进制数字拼接为数值,写的时候反过来把数值按位拆分写入
- 负天数偏移BCD:读的时候取3位BCD值直接取负,写的时候把天数取绝对值转3位BCD
- HH:MM时间BCD:读的时候前2位BCD为小时、后2位为分钟转TimeSpan,写的时候反向转换
- 二进制/HEX整数:提前确认设备字节序(老设备多为大端序),直接按对应端序转成整数即可
落地注意事项
- 解析文件时先按固定头里的
RecordSize把单条记录的字节块切出来,再做字段解析,避免单条记录解析错位导致后续所有数据损坏 - 单元测试直接用设备导出的真实样本做双向校验:样本解析成实体后再序列化回字节,必须和原始字节完全一致才算逻辑正确
- 遇到极特殊的字段转换规则,可以在特性里加个自定义转换器的参数,特殊场景单独指定转换逻辑,不需要修改引擎核心代码
- 元数据缓存做好之后,整套逻辑的性能和手写硬编码解析几乎没有差距,每秒处理上千条记录完全没有压力
内容的提问来源于stack exchange,提问作者Jogge
相关产品推荐
相关产品推荐

