C# 使用表达式树实现高性能对象复制

栗子
栗子
发布时间:2026-10-16| 6人浏览过
原创

针对传统对象映射工具(如 AutoMapper)在简单场景下可能存在的反射性能开销,本节展示如何通过 C# 的表达式树技术构建轻量级的 FastCopy 库。该方法利用运行时特性生成 Lambda 表达式并编译为委托,实现极快的对象赋值。

需求背景与设计目标

在进行对象复制或 DTO 与 Model 之间的数据映射时,主要需要解决以下性能与逻辑痛点:

  • 性能优化:传统的 PropertyInfo.SetValue 等反射操作在高频调用下会成为瓶颈。
  • 空值处理:需支持控制源对象的 null 值是否覆盖目标对象现有的非空数据。
  • 类型兼容:自动处理基础值类型到其可空版本(如 int 赋给 int?)的类型转换,无需手动拆箱装箱。

方案一:通用类型字典缓存模式

该模式适用于源对象和目标对象类型不固定的通用复制场景。核心思路是利用 ConcurrentDictionary 存储不同参数组合(类型+配置)生成的委托,避免重复编译。

第1步:定义测试数据结构

首先需要构建包含不同类型属性的测试类,以验证嵌套引用类型和可空值类型的处理能力。

using Common;
using System;

class Program
{
    static void Main(string[] args)
    {
        // 初始化源对象 A,注意 PropA 是复杂的引用类型
        TestClassA classA = new TestClassA()
        {
            PropA = new TestClass() { Name = "cs1" },
            PropB = "c1",
            PropC = 1
        };
        
        // 初始化目标对象 B
        TestClassA classB = new TestClassA()
        {
            PropA = new TestClass() { Name = "cs2" },
            PropB = "c2",
            PropC = 2
        };

        // 执行 Copy,第二个参数 false 表示不复制 Null 值
        FastCopy.Copy(classA, classB, false);
        Console.WriteLine(classB.PropA?.Name + ":" + classB.PropB + ":" + classB.PropC);

        // 测试场景2:当源对象中的 PropA.Name 存在,但其他字段缺失时的行为
        TestClassA classC = new TestClassA() { PropA = new TestClass() { Name = "cs1" } };
        TestClassA classD = new TestClassA()
        {
            PropA = new TestClass() { Name = "cs2" },
            PropB = "c2",
            PropC = 2
        };
        FastCopy.Copy(classC, classD, false);
        Console.WriteLine(classD.PropA?.Name + ":" + classD.PropB + ":" + classD.PropC);
    }
}

public class TestClassA
{
    public TestClass PropA { get; set; }
    public string PropB { get; set; }
    public int? PropC { get; set; } // 可空值类型
}

public class TestClass
{
    public string Name { get; set; }
}

第2步:构建通用的 FastCopy 核心引擎

此步骤的核心在于 CreateCopier 方法,它遍历公共属性,构建复杂的条件赋值表达式树。

  1. 属性匹配:只处理同名且可读写(Read/Write)的属性。
  2. 类型校验:支持类型完全一致、继承关系、以及值类型到可空类型的转换。
  3. 表达式分支:
    - 对于值类型,若目标为非空类型而源为对应的基础类型,自动插入 Convert 表达式节点。
    - 对于引用类型,生成类似 `if (source == null && copyNull == true) assign else assign` 的逻辑。
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using static System.Linq.Expressions.Expression;

namespace Common
{
    public static class FastCopy
    {
        // 使用线程安全字典缓存已编译的委托
        static ConcurrentDictionary<string, object> copiers = new ConcurrentDictionary<string, object>();

        /// <summary>
        /// 复制两个对象同名属性值
        /// </summary>
        /// <typeparam name="S"></typeparam>
        /// <typeparam name="T"></typeparam>
        /// <param name="source">源对象</param>
        /// <param name="target">目标对象</param>
        /// <param name="copyNull">源对象属性值为null时,是否将值复制给目标对象</param>
        public static void Copy<S, T>(S source, T target, bool copyNull = true)
        {
            // 构造唯一键名,区分源类型、目标类型及 Null 处理策略
            string name = string.Format("{0}_{1}_{2}", typeof(S), typeof(T), copyNull);
            object targetCopier;
            if (!copiers.TryGetValue(name, out targetCopier))
            {
                Action<S, T> copier = CreateCopier<S, T>(copyNull);
                // TryAdd 保证多线程环境下只编译一次
                copiers.TryAdd(name, copier);
                targetCopier = copier;
            }
            Action<S, T> action = (Action<S, T>)targetCopier;
            action(source, target);
        }

        private static Action<S, T> CreateCopier<S, T>(bool copyNull)
        {
            ParameterExpression source = Parameter(typeof(S));
            ParameterExpression target = Parameter(typeof(T));
            var sourceProps = typeof(S).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.CanRead).ToList();
            var targetProps = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.CanWrite).ToList();
            
            // 查找可进行赋值的属性:名称一致,且类型兼容
            var copyProps = targetProps.Where(tProp => sourceProps.Where(sProp => sProp.Name == tProp.Name && (
                sProp.PropertyType == tProp.PropertyType || // 类型完全一致
                sProp.PropertyType.IsAssignableFrom(tProp.PropertyType) || // 继承关系
                (tProp.PropertyType.IsValueType && sProp.PropertyType.IsValueType && // 值类型转换,如 int -> int?
                 ((tProp.PropertyType.GenericTypeArguments.Length > 0 ? tProp.PropertyType.GenericTypeArguments[0] : tProp.PropertyType) == sProp.PropertyType))
            )).Count() > 0);

            List<Expression> expressionList = new List<Expression>();
            foreach (var prop in copyProps)
            {
                if (prop.PropertyType.IsValueType)
                {
                    PropertyInfo sProp = typeof(S).GetProperty(prop.Name);
                    PropertyInfo tProp = typeof(T).GetProperty(prop.Name);
                    if (sProp.PropertyType == tProp.PropertyType)
                    {
                        // 相同类型直接赋值
                        var assign = Assign(Property(target, prop.Name), Property(source, prop.Name));
                        expressionList.Add(assign);
                    }
                    else if (sProp.PropertyType.GenericTypeArguments.Length <= 0 && tProp.PropertyType.GenericTypeArguments.Length > 0)
                    {
                        // 类型不一致且目标为可空类型(例如 int? num = int num)
                        var convert = Convert(Expression.Property(source, prop.Name), tProp.PropertyType);
                        var cvAssign = Assign(Expression.Property(target, prop.Name), convert);
                        expressionList.Add(cvAssign);
                    }
                }
                else
                {
                    // 引用类型的赋值逻辑
                    var assign = Assign(Property(target, prop.Name), Property(source, prop.Name));
                    var sourcePropIsNull = Equal(Constant(null, prop.PropertyType), Property(source, prop.Name));
                    var setNull = IsTrue(Constant(copyNull));
                    var setNullTest = IfThen(setNull, assign);
                    var condition = IfThenElse(sourcePropIsNull, setNullTest, assign);
                    /**
                     * 对应的伪代码结构如下:
                     * if (source.{PropertyName} == null)
                     * {
                     *     if (copyNull) { target.{PropertyName} = source.{PropertyName}; }
                     * }
                     * else
                     * {
                     *     target.{PropertyName} = source.{PropertyName};
                     * }
                     */
                    expressionList.Add(condition);
                }
            }
            var block = Block(expressionList.ToArray());
            Expression<Action<S, T>> lambda = Lambda<Action<S, T>>(block, source, target);
            return lambda.Compile();
        }
    }
}

第3步:结果评估

在使用通用字典缓存模式下,完成百万次调用的平均耗时约为 270ms-300ms。虽然引入了字典查找和字符串格式化开销,但依然远高于原生反射。

方案二:全速泛型类模式

如果源对象与目标对象的类型在编译期已知(或通过强类型服务传递),可以使用泛型静态类消除字典查找和 Null 判断开销,获得极致性能。

第1步:构建无开销泛型类

通过 static Action<S, T> 成员变量缓存委托,并将 copyNull 逻辑硬编码移除,所有逻辑简化为单一直达赋值。

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Common
{
    public static class FastCopy<S, T>
    {
        // 静态构造函数在首次使用时仅触发一次编译
        static Action<S, T> action = CreateCopier();

        public static void Copy(S source, T target, bool copyNull = true)
        {
            action(source, target);
        }

        private static Action<S, T> CreateCopier()
        {
            ParameterExpression source = Expression.Parameter(typeof(S));
            ParameterExpression target = Expression.Parameter(typeof(T));
            var sourceProps = typeof(S).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.CanRead).ToList();
            var targetProps = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.CanWrite).ToList();
            
            // 查找可进行赋值的属性(此处简化逻辑,假设类型严格匹配)
            var copyProps = targetProps.Where(tProp => sourceProps.Where(sProp => sProp.Name == tProp.Name && (
                sProp.PropertyType == tProp.PropertyType
            )).Count() > 0);
            
            // 直接生成简单的 Block 赋值语句
            var block = Expression.Block(from p in copyProps select Expression.Assign(
                Expression.Property(target, p.Name), 
                Expression.Property(source, p.Name)
            ));
            Expression<Action<S, T>> lambda = Expression.Lambda<Action<S, T>>(block, source, target);
            return lambda.Compile();
        }
    }
}

第2步:性能对比

去除了复杂的条件判断后,百万次调用耗时降至 100ms 左右,速度提升显著,非常适合高性能数据映射层。

总结

C# 表达式树提供了极其强大的元编程能力。通过提前编译 Lambda 表达式,我们可以绕过大部分运行时的类型检查与反射损耗,实现接近手写代码的性能表现。开发者可根据实际业务对 Null 值处理的灵活性要求,选择基于字典缓存的通用方案或基于泛型的极速方案。

以上就是 C# 使用表达式树实现对象复制的详细内容,更多关于 C# 表达式树对象复制的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。