C# 使用表达式树(Expression Tree)代替反射赋值
关于如何在C#中根据配置表映射一个实体对象数据,我们常用有反射,虽然灵活但性能可能较低,尤其是在高频繁操作时。比如在数据采集的时候,开几个线程去跑数据,这个性能开销不是一星半点消耗了。
我们采用 表达式树(Expression Tree)
+ 委托缓存机制
的方式,确保每次转换时无需重复构建表达式逻辑,从而显著提升性能。表达式树调用比反射快 20 倍以上!
1、需求分析
- 字段配置表(PartList)
PartName是装置的名称
PartAddress:是PLC的地址
PartType:与实体对应的字段名称
{"PartList": [{"PartName": "绕线装置-升降1","PartAddress": "D2344","PartType": "RX_UpDown_1"},{"PartName": "绕线装置-升降2","PartAddress": "D2380","PartType": "RX_UpDown_2"},{"PartName": "抓线装置-升降1","PartAddress": "D2346","PartType": "ZX_UpDown_1"},{"PartName": "抓线装置-升降2","PartAddress": "D2382","PartType": "ZX_UpDown_2"}]
}
- 配置项(与配置表)
// 装置配置项public class PlcPart{public string PartName { get; set; }public string PartAddress { get; set; }public string PartType { get; set; }public int PartAddressValue(){var match = _regex.Match(PartAddress);return match.Success ? int.Parse(match.Groups[1].Value) : Convert.ToInt16(PartAddress.Substring(1));//获取元素地址//return Convert.ToInt16(Element.Substring(2));//获取元素地址}}
- 实体对象
public class RXJ_ActionData
{public bool RX_UpDown_1 { get; set; }public bool RX_UpDown_2 { get; set; }public int ZX_UpDown_1 { get; set; }public int ZX_UpDown_2 { get; set; }
}
2、预期效果
- 根据 PartList 中的 PartType字段 映射到 RXJ_ActionData 的属性中。
- 为了多类型匹配,采用泛型
- 从 PartAddress 获取原始值(如 int 类型)。
- 对 bool 类型字段,进行 rawValue != 0 的转换。0代表
flase
,1代表true
- 使用 表达式树 构建赋值逻辑。
- 使用 委托缓存 提升性能,避免重复构建。
3、实现方式
预编译委托(Expression Tree)
- 构建一个 Func<Func<string, int>, RXJ_ActionData> 类型的委托。
- 该委托接受一个 Func<string, int> 参数,用于根据地址获取原始值。
- 使用 Expression.MemberInit 构建对象初始化表达式。
- 每个字段根据类型进行不同的转换逻辑:
- bool 类型:rawValue != 0(0代表
flase
,1代表true
) - int 类型:直接赋值
- bool 类型:rawValue != 0(0代表
核心思想:
- 预处理阶段 :为每个实体类型生成字段/属性到委托的映射字典,委托负责赋值操作。
- 运行时 :直接通过字段名称查找委托并执行赋值,无需每次反射查找字段。
优点:
- 高性能 :委托编译后执行速度与直接代码接近。
- 类型安全 :通过表达式树确保类型转换正确。
- 灵活 :支持字段和属性,可扩展。
表达式树(Expression Tree)
namespace DataCapture.Helper;using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;public static class MapperCache
{public static Func<Func<string, int>, T> GetMapper<T>(Dictionary<string, Delegate> mapperDelegates, PlcPart[] partList){var key = GenerateKey(partList);lock (mapperDelegates){if (mapperDelegates.TryGetValue(key, out var del)){return (Func<Func<string, int>, T>)del;}del = BuildMapper<T>(partList);mapperDelegates[key] = del;return (Func<Func<string, int>, T>)del;}}private static string GenerateKey(PlcPart[] partList){return string.Join(";", partList.Select(p => $"{p.PartType}:{p.PartAddress}"));}private static Delegate BuildMapper<T>(PlcPart[] partList){var getDataParam = Expression.Parameter(typeof(Func<string, int>), "getData");var newExpr = Expression.New(typeof(T));var bindings = new List<MemberBinding>();foreach (var part in partList){var propInfo = typeof(T).GetProperty(part.PartType);if (propInfo == null || propInfo.GetSetMethod() == null)continue;var addressConst = Expression.Constant(part.PartAddress, typeof(string));var rawValueExpr = Expression.Invoke(getDataParam, addressConst);Expression convertedExpr;if (propInfo.PropertyType == typeof(bool)){// int -> bool: value != 0convertedExpr = Expression.NotEqual(rawValueExpr, Expression.Constant(0));}else{convertedExpr = rawValueExpr;}var bind = Expression.Bind(propInfo, convertedExpr);bindings.Add(bind);}var memberInit = Expression.MemberInit(newExpr, bindings);var lambda = Expression.Lambda<Func<Func<string, int>, T>>(memberInit,getDataParam);return lambda.Compile();}
}
缓存机制
- 使用 Dictionary<string, Delegate> 缓存已生成的委托。
- 缓存键可基于 PartList 的内容(如 JSON 序列化后的哈希值)。
- 避免重复构建表达式树,提升性能
using System;
using System.Collections.Generic;
namespace DataCapture.DeviceDataSave;
/// <summary>
/// 设备绕线机数据(根据自身定义)
/// </summary>
public class DeviceRXJ
{protected readonly Dictionary<string, Delegate> _cachedDelegates;protected readonly Dictionary<string, int> _plcData = new Dictionary<string, int>();//数据缓存private Func<Func<string, int>, RXJ_PLC_ActionDataModel> mapper;//表达式树public DeviceRXJ( ElementConfig elementConfig) {//elementConfig是配置表内容(根据自身定义)List<PlcPart> plcParts = new List<PlcPart>();foreach (var moduleConfig in elementConfig.ModuleConfig){plcParts.AddRange(moduleConfig.PartList);}mapper = MapperCache.GetMapper<RXJ_PLC_ActionDataModel>(_cachedDelegates, plcParts.ToArray());}/// <summary>/// 添加数据/// </summary>/// <param name="address"></param>/// <param name="value"></param>public void AddData(string address, int value){_plcData[address] = value;}/// <summary>/// 获取数据/// </summary>/// <param name="address"></param>/// <returns></returns>public int GetData(string address){//Console.WriteLine($"GetData: {address}");return _plcData.TryGetValue(address, out var value) ? value : 0;}
}
PLC 读取模拟(可替换为真实通信)
public static class PlcReader
{private static readonly Dictionary<string, int> _plcData = new Dictionary<string, int>{{ "D2344", 1 }, // RX_UpDown_1 = true{ "D2380", 0 }, // RX_UpDown_2 = false{ "D2346", 123 }, // ZX_UpDown_1 = 123{ "D2382", 456 } // ZX_UpDown_2 = 456};public static int ReadInt(string address){return _plcData.TryGetValue(address, out var value) ? value : 0;}
}
使用案例
using System;
using System.Linq;public class Program
{public static void Main(){// 1. 解析字段配置表(模拟从 JSON 中读取)var partList = new[]{new PlcPart{ PartName = "绕线装置-升降1", PartAddress = "D2344", PartType = "RX_UpDown_1" },new PlcPart{ PartName = "绕线装置-升降2", PartAddress = "D2380", PartType = "RX_UpDown_2" },new PlcPart{ PartName = "抓线装置-升降1", PartAddress = "D2346", PartType = "ZX_UpDown_1" },new PlcPart{ PartName = "抓线装置-升降2", PartAddress = "D2382", PartType = "ZX_UpDown_2" }};// 2. 获取映射器(仅在首次构建一次)var mapper = MapperCache.GetMapper(partList);// 3. 从 PLC 获取数据(模拟)该处是委托,lambda表达式,回传回来的是地址,具体需求大家可以修改参数Func<string, int> getData = address => PlcReader.ReadInt(address);// 4. 执行映射,生成实体对象var result = mapper(getData);// 5. 输出结果Console.WriteLine("映射结果:");Console.WriteLine(result);}
}
结果:
RX_UpDown_1: True, RX_UpDown_2: False, ZX_UpDown_1: 123, ZX_UpDown_2: 456
方法 | 冷启动 | 热运行 |
---|---|---|
反射 | 慢(需查找字段) | 慢(反射开销) |
预编译委托(本方案) | 较慢(生成委托) | 非常快(接近直接代码) |
冷启动 :首次运行时需生成委托,但后续调用无需重复生成。
热运行 :委托执行速度与直接代码赋值几乎相同,远快于反射。
表达式树调用比反射快 20 倍以上!
此方案在保持高性能的同时,提供了更灵活的接口设计,适用于动态配置和批量操作场景
建议:
支持更多类型映射 :如 float, short, DateTime 等
异常处理 :在 PLC 读取失败时加入重试机制或默认值
多线程安全 :确保 _cache 在并发访问下线程安全
日志记录 :记录映射过程,便于调试与维护