在 ORM、JSON 序列化、对象映射等场景中,我们经常需要动态读写对象属性。传统反射方案虽然灵活,但性能堪忧。本文将从 SwitchData 项目的真实代码出发,深入讲解如何利用 表达式树(Expression Tree)编译 技术,把反射式的属性访问编译成原生委托,实现接近直接调用的性能。
一、问题背景:反射访问的性能瓶颈
先看一个典型的 ORM 映射场景:从数据库 IDataReader 读取数据,填充到实体对象。最直接的写法是使用反射:
csharp // 传统反射方式 —— 性能较差 public static void SetValue<T>(T entity, string propertyName, object value) { PropertyInfo prop = typeof(T).GetProperty(propertyName); prop.SetValue(entity, value); }
反射的问题在于: - GetProperty 每次都在元数据中查找,开销大 - SetValue 内部涉及运行时类型检查、装箱拆箱 - 没有任何 JIT 优化机会
Benchmark 显示,反射 SetValue 的速度大约是直接调用 entity.Name = value 的 1/100 ~ 1/1000。当需要映射上百条记录、几十万个属性时,这个差距会被放大到不可接受。
二、核心思路:用表达式树编译委托
解决方案的核心思想很简单:在首次访问某个类型的某个属性时,用表达式树构建一段”直接赋值”的代码,编译成委托缓存起来,后续调用直接走委托。
首次访问 PropertyInfo | v 构建 Expression Tree | v Expression.Compile 编译 | v 委托缓存到 ConcurrentDictionary | v 后续调用直接从缓存取委托
表达式树的优势在于它能被 JIT 编译成与手写代码等价的 IL,性能几乎与直接调用一致。
三、实战代码:从零构建 Getter/Setter
以下代码来自 SwitchData 项目的 MetadataCache 类,是一个完整可用的生产级实现。
3.1 Getter 编译:属性读取委托
目标:将 PropertyInfo.GetValue(obj) 编译成 Func<object, object>。
`csharp using System.Linq.Expressions;
///private static Func<object, object> CreateGetter(PropertyInfo property) { // 参数:object obj ParameterExpression instance = Expression.Parameter(typeof(object), “obj”);
// 转型:(T)obj
UnaryExpression cast = Expression.Convert(instance, property.DeclaringType);
// 访问属性:((T)obj).Property
MemberExpression propertyAccess = Expression.Property(cast, property);
// 装箱:object 是引用类型或值类型都要转成 object
UnaryExpression box = Expression.Convert(propertyAccess, typeof(object));
// 构建 Lambda:obj => (object)((T)obj).Property
Expression<Func<object, object>> lambda =
Expression.Lambda<Func<object, object>>(box, instance);
return lambda.Compile();
} `
来看这段代码做了什么。假设实体类型是 User,属性是 Name(string 类型),那么构建出的表达式树等价于:
csharp Func<object, object> getter = (obj) => (object)((User)obj).Name;
这就是一段可被 JIT 完全优化的原生代码——没有反射查找,没有运行时类型检查,就是一次类型转换 + 一次属性访问 + 一次装箱(如果是值类型)。
3.2 Setter 编译:属性写入委托
Setter 稍微复杂一点,涉及到写入操作。目标:将 PropertyInfo.SetValue(obj, value) 编译成 Action<object, object>。
`csharp ///private static Action<object, object> CreateSetter(PropertyInfo property) { // init-only 属性(C# 9 init 关键字)不能写 if (!property.CanWrite || IsInitOnly(property)) { return (, ) => { }; // 返回空操作委托 }
// 参数:object obj, object value
ParameterExpression instance = Expression.Parameter(typeof(object), "obj");
ParameterExpression value = Expression.Parameter(typeof(object), "value");
// 转型:(T)obj
UnaryExpression instanceCast = Expression.Convert(instance, property.DeclaringType);
// 属性访问:((T)obj).Property
MemberExpression propertyAccess = Expression.Property(instanceCast, property);
// 转型:(TProperty)value
UnaryExpression valueCast = Expression.Convert(value, property.PropertyType);
// 赋值:((T)obj).Property = (TProperty)value
BinaryExpression assign = Expression.Assign(propertyAccess, valueCast);
// 构建 Lambda:(obj, value) => ((T)obj).Property = (TProperty)value
Expression<Action<object, object>> lambda =
Expression.Lambda<Action<object, object>>(assign, instance, value);
return lambda.Compile();
}
///private static bool IsInitOnly(PropertyInfo property) { MethodInfo setter = property.SetMethod; if (setter == null) return false;
// init-only setter 的 ReturnParameter 带有 IsExternalInit 修饰符
return setter.ReturnParameter
.GetRequiredCustomModifiers()
.Contains(typeof(System.Runtime.CompilerServices.IsExternalInit));
} `
Setter 里有几个要点:
- Expression.Assign 是表达式树中唯一能构建赋值操作的 API,它的 Left 必须是可写的表达式(属性访问、字段访问、索引器)
- init-only 检测:C# 9 的 init 关键字生成的 setter 不能被普通委托调用,必须跳过
- 同样,最终生成的代码等价于手写:((User)obj).Name = (string)value
3.3 元数据结构体设计
仅仅编译委托还不够,我们需要一个结构体来承载完整的字段元数据:
`csharp public sealed class ColumnMetadata { // 属性名(C# 属性) public required string PropertyName { get; init; }
// 字段名(数据库列)
public required string ColumnName { get; init; }
// 属性类型
public required Type PropertyType { get; init; }
// 数据库类型
public required DbType DbType { get; init; }
// 是否可空
public required bool IsNullable { get; init; }
// 是否主键
public required bool IsPrimaryKey { get; init; }
// 是否自增长
public bool IsIdentity { get; init; }
// Getter 委托:Func<object, object>
public required Func<object, object> Getter { get; init; }
// Setter 委托:Action<object, object>
public required Action<object, object> Setter { get; init; }
} `
3.4 缓存设计:ConcurrentDictionary + 单例初始化
最后,用 ConcurrentDictionary 按类型缓存元数据,确保线程安全且只编译一次:
`csharp public static class MetadataCache { private static readonly ConcurrentDictionary<Type, TableMetadata> Cache = new();
public static TableMetadata GetTableMetadata(Type type)
{
// GetOrAdd 保证多线程下只编译一次
return Cache.GetOrAdd(type, CreateMetadata);
}
private static TableMetadata CreateMetadata(Type type)
{
// 读取 DbTable / DbColumn 特性
DbTableAttribute tableAttr = type.GetCustomAttribute<DbTableAttribute>();
List<ColumnMetadata> columns = [];
foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
DbColumnAttribute colAttr = property.GetCustomAttribute<DbColumnAttribute>();
if (colAttr == null) continue; // 无特性跳过
columns.Add(new ColumnMetadata
{
PropertyName = property.Name,
ColumnName = colAttr.Name,
PropertyType = property.PropertyType,
DbType = Database.ConvertToDbType(property.PropertyType),
IsNullable = property.PropertyType.IsNullable(),
IsPrimaryKey = property.IsDefined(typeof(DbPrimaryKeyAttribute)),
IsIdentity = property.IsDefined(typeof(DbIdentityAttribute)),
Getter = CreateGetter(property), // 编译
Setter = CreateSetter(property) // 编译
});
}
return new TableMetadata
{
Type = type,
Columns = columns,
PropertyMap = columns.ToDictionary(x => x.PropertyName, StringComparer.OrdinalIgnoreCase),
ColumnMap = columns.ToDictionary(x => x.ColumnName, StringComparer.OrdinalIgnoreCase)
};
}
} `
四、实际使用:ORM 对象映射
有了 Getter/Setter 委托,对象映射就变得极其高效。以下是 SwitchData 中 ObjectMapper.SetFrom 方法的实现:
`csharp public static T SetFrom
for (int i = 0; i < record.FieldCount; i++)
{
string columnName = record.GetName(i);
object value = record.GetValue(i);
// 从 ColumnMap 找到对应的 ColumnMetadata,直接调用 Setter 委托
if (metadata.ColumnMap.TryGetValue(columnName, out ColumnMetadata column))
{
if (value is DBNull)
{
if (!column.IsNullable)
throw new InvalidOperationException($"字段 {column.ColumnName} 不允许为空");
column.Setter(entity, null);
}
else if (!column.PropertyType.IsInstanceOfType(value))
{
// 类型不匹配时做一次 Convert
column.Setter(entity, Convert.ChangeType(value, column.PropertyType));
}
else
{
column.Setter(entity, value);
}
}
}
// 回调钩子
if (entity is IDbObject callback) callback.OnLoaded();
return entity;
} `
整个映射过程中,没有一次反射调用。属性访问全部走预编译的委托。
五、性能对比
用 Benchmark 验证一下。假设有一个 10 属性的 User 实体,做 1000000 次属性读写:
| 方案 | 100万次 SetValue | 100万次 GetValue | 特点 |
|---|---|---|---|
| 直接调用 user.Name = value | ~20ms | ~15ms | 无反射,最快 |
| 表达式树编译委托 | ~25ms | ~20ms | 接近直接调用 |
| 反射 prop.SetValue | ~2000ms | ~1200ms | 慢 80-100 倍 |
| FastMember 库 | ~40ms | ~35ms | 第三方库,较快 |
表达式树编译的委托性能几乎与直接调用持平,这是因为 JIT 把它当成普通方法调用优化了。
六、常见坑点与避坑指南
6.1 init-only 属性检测不完整
只判断 !property.CanWrite 不够——C# 9 的 init 关键字生成的 setter 是 CanWrite == true 的,但在外部代码中无法赋值。正确检测方式:
`csharp // 错误:漏了 init-only if (!property.CanWrite) return (, ) => { };
// 正确:检测 IsExternalInit 修饰符 MethodInfo setter = property.SetMethod; bool isInitOnly = setter?.ReturnParameter .GetRequiredCustomModifiers() .Contains(typeof(IsExternalInit)) ?? false; `
6.2 struct 值类型的装箱
Getter 返回 object 时,如果属性是值类型(int、ool 等),表达式树会自动插入 Convert 产生装箱。这是必要的——因为委托签名固定了返回 object。
csharp // int 属性的 getter 实际上是: (int value) => (object)value // 装箱
如果需要极致性能,可以为值类型单独编译非装箱的委托。但在大多数 ORM 场景中,这种装箱开销完全可以接受。
6.3 ConcurrentDictionary 内存泄漏
ConcurrentDictionary<Type, TableMetadata> 用 Type 做 key 是安全的,因为 CLR 保证每个 AppDomain 中 Type 实例不会被回收。但要注意:
- 不要用字符串类型名做 key(不同 Assembly 可能重名)
- 不要用 AssemblyQualifiedName 做 key(会导致同一类型被多次缓存)
6.4 泛型类型的元数据隔离
List
七、进阶拓展
SwitchData 还把表达式树编译拓展到了更多场景。比如 Database.ConvertToDbType 用 switch expression 做类型映射:
`csharp public static DbType ConvertToDbType(Type type) { type = Nullable.GetUnderlyingType(type) ?? type; if (type.IsEnum) type = Enum.GetUnderlyingType(type);
return type switch
{
Type t when t == typeof(int) => DbType.Int32,
Type t when t == typeof(string) => DbType.String,
Type t when t == typeof(DateTime) => DbType.DateTime,
_ => throw new NotSupportedException()
};
} `
以及 ObjectMapper 里 IsInstanceOfType + Convert.ChangeType 的容错组合,配合预编译 Setter,就能在保持高性能的同时应对数据库类型与 CLR 类型的细微差异。
八、总结
表达式树编译属性访问器是一个经典的 “用空间换时间” 优化策略:
- 首次成本:扫描特性、构建表达式树、编译委托(毫秒级)
- 持续收益:所有后续属性访问走 JIT 优化后的原生代码,无反射开销
- 实现复杂度:中等,核心代码不到 100 行
在 ORM、序列化、动态配置等场景中,这个模式几乎是标配。理解它不仅能帮你写出高性能的动态代码,更能深入体会 .NET 表达式树和编译器的协作方式。