C# 使用表达式树实现高性能对象复制
针对传统对象映射工具(如 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 方法,它遍历公共属性,构建复杂的条件赋值表达式树。
- 属性匹配:只处理同名且可读写(Read/Write)的属性。
- 类型校验:支持类型完全一致、继承关系、以及值类型到可空类型的转换。
- 表达式分支:
- 对于值类型,若目标为非空类型而源为对应的基础类型,自动插入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# 表达式树对象复制的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









