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

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 类型:直接赋值

核心思想:

  • 预处理阶段 :为每个实体类型生成字段/属性到委托的映射字典,委托负责赋值操作。
  • 运行时 :直接通过字段名称查找委托并执行赋值,无需每次反射查找字段。

优点:

  • 高性能 :委托编译后执行速度与直接代码接近。
  • 类型安全 :通过表达式树确保类型转换正确。
  • 灵活 :支持字段和属性,可扩展。

表达式树(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 在并发访问下线程安全
日志记录 :记录映射过程,便于调试与维护

相关文章:

  • 机器学习之聚类Kmeans算法
  • MongoDB 入门指南:安装、配置与 Navicat 连接教程
  • 冒泡排序C++实现
  • Linux系统部署KES
  • MySQL中text,longtext,mediumtext区别
  • CSS设置元素的宽度根据其内容自动调整
  • 如何将数据从 iPhone 传输到 Android?
  • 网页版便签应用开发:HTML5本地存储与拖拽交互实践
  • c++默认类模板参数
  • iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版​分享
  • 黑客利用iMessage零点击漏洞攻击iPhone用户
  • 一文读懂 Docker Compose(白话版)
  • CentOS 7 部署 Samba 使用虚拟用户笔记
  • NLP学习路线图(三十五): 情感分析
  • 8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
  • NLP学习路线图(三十六): 机器翻译
  • 蓝桥杯第十届国B 质数拆分
  • 基于Uniapp开发HarmonyOS 5.0旅游应用技术实践
  • 苍穹外卖--缓存菜品
  • 技术栈RabbitMq的介绍和使用
  • 校园网设计方案/北京seo外包 靠谱
  • 双城网站建设哪家好/东莞百度推广优化排名
  • 寮步网站建设/百度推广靠谱吗
  • net的网站建设/流量宝
  • 广告点击网站源码/网络营销岗位技能
  • 转转怎么做钓鱼网站/网络营销策划内容