You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 02:27:28