C#基于Lambda表达式实现可复用泛型属性读写包装类
问题本身逻辑十分清晰,以下首先说明业务背景与现存痛点,若需直接查看核心问题,可跳转至末尾代码片段阅读其中的注释。
业务背景
我们采用由结构化键值对构成的后端模型,在这些数据之上搭建了大量视图,要求视图支持直读(read-through)与直写(write-through)能力。
当前实现支持使用表达式语法定义属性,代码示例如下:
public class Car { Car(Record car_record) { Color = new Color<ColorEnum>(()=>car_record.GetFactoryAsset("Manufacturer","Color").Value); } }
该实现的核心优势是Color类无需感知获取对应底层参数的各类不同逻辑。所有包装属性均继承自同一基类,基类构造函数接收传入的表达式,编译生成用于读写底层字符串的Action与Func委托,基类实现代码如下:
public class WrappedPropBase { protected Action<string> Setter; protected Func<string> Getter; public WrappedPropBase(Expression<Func<string>> expr) { var memberExpression = (MemberExpression)expr.Body; var instanceExpression = memberExpression.Expression; var parameter = Expression.Parameter(typeof(string)); if (memberExpression.Member is PropertyInfo propertyInfo) { Setter = Expression.Lambda<Action<string>>(Expression.Call(instanceExpression, propertyInfo.GetSetMethod(), parameter), parameter).Compile(); Getter = Expression.Lambda<Func<string>>(Expression.Call(instanceExpression, propertyInfo.GetGetMethod())).Compile(); } else if (memberExpression.Member is FieldInfo fieldInfo) { Setter = Expression.Lambda<Action<string>>(Expression.Assign(memberExpression, parameter), parameter).Compile(); Getter = Expression.Lambda<Func<string>>(Expression.Field(instanceExpression, fieldInfo)).Compile(); } } }
现存问题
该实现十分优雅,能够让模型定义保持简洁可读,但存在明显的性能缺陷:在规模较大的模型结构中,每个实例的每个属性都会执行两次编译操作,带来的性能损耗已经到了必须重构的程度。
最容易想到的重构方案是在实例化模型时直接传入Action与Func委托,改造后的Car模型代码如下:
public class Car { Car(Record car_record) { Color = new Color<ColorEnum>(()=>car_record.GetFactoryAsset("Manufacturer","Color").Value, s =>car_record.GetFactoryAsset("Manufacturer","Color").Value=s) } }
该改造看似改动不大,但会产生本可避免的重复代码,导致代码冗余杂乱。
重构目标
让运行时编译操作不再针对每个Car实例执行,而是仅针对Car模型类型执行一次:
- 不再接收形如
()=> car.GetTheCorrectRecord().Value的无参表达式,逐实例编译生成绑定实例的Action<string>和Func<string>委托 - 转而实现以目标模型为类型参数的泛型包装类,接收形如
car => car.GetTheCorrectRecord().Value的带实例参数的表达式,一次性生成可复用于所有同类型实例的Func<TModelType,string>读委托和Action<TModelType, string>写委托
期望实现的泛型基类结构如下:
public class WrappedPropBase<TModelType> { protected Action<TModelType,string> Setter; protected Func<TModelType,string> Getter; public WrappedPropBase(Expression<Func<TModelType,string>> expr) { // 在此处实现逻辑,生成可复用的读写委托 } }
核心问题:该泛型基类的构造函数中应当如何实现,才能正确生成传入表达式所引用字符串属性的可复用读写委托?
实现代码
构造函数可按如下逻辑实现:
public WrappedPropBase(Expression<Func<TModelType, string>> expr) { // 读委托直接编译原表达式即可,所有同类型实例通用 Getter = expr.Compile(); var valueParam = Expression.Parameter(typeof(string), "value"); var modelParam = expr.Parameters[0]; var memberExpr = expr.Body as MemberExpression; if (memberExpr == null) throw new ArgumentException("传入表达式必须指向实例的字符串属性或字段", nameof(expr)); // 处理属性写入场景 if (memberExpr.Member is PropertyInfo propInfo) { var setMethod = propInfo.GetSetMethod(); if (setMethod == null) throw new InvalidOperationException($"属性 {propInfo.Name} 无公开set访问器,不支持写入"); var setCall = Expression.Call(memberExpr.Expression, setMethod, valueParam); Setter = Expression.Lambda<Action<TModelType, string>>(setCall, modelParam, valueParam).Compile(); } // 处理字段写入场景 else if (memberExpr.Member is FieldInfo fieldInfo) { if (fieldInfo.IsInitOnly) throw new InvalidOperationException($"字段 {fieldInfo.Name} 为init-only只读字段,不支持写入"); var assignExpr = Expression.Assign(memberExpr, valueParam); Setter = Expression.Lambda<Action<TModelType, string>>(assignExpr, modelParam, valueParam).Compile(); } else { throw new ArgumentException("传入表达式指向的成员不是可写的属性或字段", nameof(expr)); } }
实现说明
- 读委托不需要额外拆解表达式,传入的表达式本身就是以
TModelType为入参、返回目标字符串值的函数结构,直接编译后即可复用于所有同类型实例,传入不同实例就能读取对应实例的目标值。 - 写委托需要基于原表达式扩展:保留原表达式的模型实例参数,新增一个字符串类型的入参作为要写入的值,构造对目标成员的赋值逻辑,最终编译为接收「模型实例+待写入字符串」两个参数的Action委托,同样不绑定特定实例,全局复用一次编译的结果。
- 增加了必要的参数校验,遇到不可写的成员时提前抛出清晰的异常,避免运行时出现无意义的编译错误。
该实现完全保留了原方案代码简洁的优势,同时将每个属性的表达式编译次数从「每实例一次」降低到「每个泛型类型闭合构造一次」,彻底解决了原实现的性能问题。
内容的提问来源于stack exchange,提问作者Tormod
相关产品推荐
相关产品推荐

