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, RXJ_ActionData> 类型的委托。
  • 该委托接受一个 Func 参数,用于根据地址获取原始值。
  • 使用 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 != 0
                convertedExpr = 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 缓存已生成的委托。
  • 缓存键可基于 PartList 的内容(如 JSON 序列化后的哈希值)。
  • 避免重复构建表达式树,提升性能
using System;
using System.Collections.Generic;
namespace DataCapture.DeviceDataSave;
/// 
/// 设备绕线机数据(根据自身定义)
/// 
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());

    }
    /// 
    /// 添加数据
    /// 
    /// 
    /// 
    public void AddData(string address, int value)
    {
        _plcData[address] = value;
    }
    /// 
    /// 获取数据
    /// 
    /// 
    /// 
    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 在并发访问下线程安全
日志记录 :记录映射过程,便于调试与维护

你可能感兴趣的:(C# 使用表达式树(Expression Tree)代替反射赋值)