使用表达式树构造具有未知成员的对象

3

我正在尝试创建一个通用函数,它将接受两个结构体实例,并使用传入实例的值创建一个新实例。我大部分做完了,但是我在构建表达式树以将新值作为参数传递给MemberInit(第一次使用表达式树)方面遇到了问题。

我正在尽可能避免创建垃圾(因此不进行装箱)。

这是我目前的进展:

private static readonly Dictionary<Type, FieldInfo[]> fieldInfoCache = new Dictionary<Type, FieldInfo[]>();
private static readonly Dictionary<FieldInfo, dynamic> compiledDelegates = new Dictionary<FieldInfo, dynamic>();
private static T Lerp<T>(T start, T end, float amount) where T : new()
{
    FieldInfo[] fields;
    var type = typeof(T);

    if(!fieldInfoCache.TryGetValue(type, out fields))
    {
        fields = typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance);

        fieldInfoCache.Add(type, fields);
    }

    var binds = new List<MemberBinding>();

    foreach(var fieldInfo in fields)
    {
        dynamic getter;

        if(!compiledDelegates.TryGetValue(fieldInfo, out getter))
        {
            var targetExp = Expression.Parameter(type, type.Name);
            var fieldExp = Expression.Field(targetExp, fieldInfo);

            getter = Expression.Lambda(typeof(Func<,>).MakeGenericType(type, fieldInfo.FieldType), fieldExp, targetExp).Compile();

            compiledDelegates.Add(fieldInfo, getter);
        }

        var startVal = getter.Invoke(start);
        var endVal = getter.Invoke(end);

        //This needs to be assigned to something
        var newVal = fieldInfo.FieldType.IsAssignableFrom(typeof(float)) ? LerpF(startVal, endVal, amount) : Lerp(startVal, endVal, amount);

        var fieldParamExp = Expression.Parameter(fieldInfo.FieldType, "newVal");
        var bind = Expression.Bind(fieldInfo, fieldParamExp);

        binds.Add(bind);
    }

    //How do I fix these two lines?
    var memberInit = Expression.MemberInit(Expression.New(type), binds);
    var result = Expression.Lambda<Func<T>>(memberInit).Compile().Invoke();

    return result;
}

我卡在的部分是如何将值输入到这最后两行中而不会导致装箱


所以,你要创建的委托应该为每个float字段调用LerpF()?如果是这样,你需要创建一个调用该方法的表达式,而不仅仅是直接调用它。 - svick
是的,没错。但为什么需要在表达式内完成呢?也许如果我把这些都放到一个表达式中,可能会好一些……但我计划使用一个表达式集来获取字段数据,并且使用另一个表达式集来构建新对象。除非这样做可以使构建部分更容易? - Telanor
1个回答

2

替代

var fieldParamExp = Expression.Parameter(fieldInfo.FieldType, "newVal");

尝试使用

var fieldParamExp = Expression.Constant(newVal);

更新:

为了实现高效的缓存,您可以使用类似以下内容的东西:

        var startPar = Expression.Parameter(typeof (T), "start");
        var endPar = Expression.Parameter(typeof (T), "end");
        var amountPar = Expression.Parameter(typeof (float), "amount");
        foreach (var fieldInfo in fields)
        {
            MethodInfo mi;
            if (fieldInfo.FieldType.IsAssignableFrom(typeof (float)))
            {
                mi = typeof (Program).GetMethod("LerpF");
            }
            else
            {
                mi = typeof (Program).GetMethod("Lerp").MakeGenericMethod(fieldInfo.FieldType);
            }

            var makeMemberAccess = Expression.Call(mi, Expression.MakeMemberAccess(startPar, fieldInfo), Expression.MakeMemberAccess(endPar, fieldInfo), amountPar);
            binds.Add(Expression.Bind(fieldInfo, makeMemberAccess));
        }

        var memberInit = Expression.MemberInit(Expression.New(type), binds);
        var expression = Expression.Lambda<Func<T, T, float, T>>(memberInit, startPar, endPar, amountPar);
        Func<T, T, float, T> resultFunc = expression.Compile();
        // can cache resultFunc

        var result = resultFunc(start, end, amount);

但我不知道您如何决定使用起始或结束参数,因此在绑定中可能存在一些更复杂的条件。


这是我一开始做的,但问题在于它不能被缓存。每次都需要编译一个新表达式,这将使使用表达式失去意义。 - Telanor
你如何决定选择startVal还是leftVal? - Vladimir
这是一个Lerp函数。StartVal和EndVal被插值在一起,产生NewVal,这就是需要传递给构造函数的值。 - Telanor
你的修改看起来假定T只有2个字段,但实际上它是一个可变数量的字段。在构造对象时,T可能需要设置1到10个字段。 - Telanor
这里还有一种变体,根据字段类型决定使用Lerp / LerpF。 - Vladimir
啊,看起来可能行得通。我测试了这个小提琴,它似乎做了我想要的事情。明天我会尝试一下,看看它是否正确地工作,然后再将其标记为已接受。 - Telanor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接