从函数表达式创建lambda动作

时间:2010-05-15 16:02:24

标签: .net lambda

创建一个将从对象返回属性值的lambda函数相对容易,甚至包括深层属性......

Func<Category, string> getCategoryName = new Func<Category, string>(c => c.Name);

这可以如下调用......

string categoryName = getCategoryName(this.category);

但是,鉴于 上面生成的函数(或最初用于创建函数的表达式),任何人都可以提供一种简单的方法来创建相反的操作。

Action<Category, string> setCategoryName = new Action<Category, string>((c, s) => c.Name = s);

...这将使相同的属性值设置如下?

setCategoryName(this.category, "");

请注意,我正在寻找一种从函数或表达式以编程方式创建操作的方法 - 我希望我已经证明我已经知道如何手动创建它。

我愿意接受在.net 3.5和4.0中都有效的答案。

感谢。

更新:

也许我的问题并不清楚,所以让我试着更清楚地展示我想要做的事情。

我有以下方法(我为此问题而创建的)...

void DoLambdaStuff<TObject, TValue>(TObject obj, Expression<Func<TObject, TValue>> expression) {

    Func<TObject, TValue> getValue = expression.Compile();
    TValue stuff = getValue(obj);

    Expression<Action<TObject, TValue>> assignmentExpression = (o, v) => Expression<TObject>.Assign(expression, Expression.Constant(v, typeof(TValue)));
    Action<TObject, TValue> setValue = assignmentExpression.Compile();

    setValue(obj, stuff);

}

我在寻找的是如何在代码中创建“assignmentExpression”以便我可以将其编译成setValue?我认为它与Expression.Assign有关,但我根本无法找出正确的参数组合来完成代码。

最终结果是能够调用

Category category = *<get object from somewhere>*;
this.DoLambdaStuff(category, c => c.Name);

然后这将为Category对象的“Name”属性创建一个getter和setter。

上面的版本编译,但是当我调用setValue()时,会导致ArgumentException,“Expression必须是可写的”。

再次感谢。

4 个答案:

答案 0 :(得分:5)

void DoLambdaStuff<TObject, TValue>(TObject obj, Expression<Func<TObject, TValue>> expression) {

    Func<TObject, TValue> getValue = expression.Compile();
    TValue stuff = getValue(obj);

    var p = Expression.Parameter(typeof(TValue), "v");
    Expression<Action<TObject, TValue>> assignmentExpression = 
        Expression.Lambda<Action<TObject, TValue>>(Expression.Assign(expression.Body, p), expression.Parameters[0], p);

    Action<TObject, TValue> setValue = assignmentExpression.Compile();

    setValue(obj, stuff);
}

答案 1 :(得分:3)

好的,我正在寻找的代码是这样的......

ParameterExpression objectParameterExpression = Expression.Parameter(
  typeof(TObject)),
  valueParameterExpression = Expression.Parameter(typeof(TValue)
);
Expression<Action<TObject, TValue>> setValueExpression = Expression.Lambda<Action<TObject, TValue>>(
  Expression.Block(
    Expression.Assign(
      Expression.Property(
        objectParameterExpression,
        ((MemberExpression) expression.Body).Member.Name
      ),
      valueParameterExpression
    )
  ),
  objectParameterExpression,
  valueParameterExpression
);
Action<TObject, TValue> setValue = setValueExpression.Compile();

此代码有效,但仅适用于浅属性(即直接对象的属性),但不适用于深层属性 - 尽管getter函数可用于深层属性。知道是否有人可以帮助我修改它以使用深层属性会很有趣,但我会将其作为一个单独的问题提出来。

答案 2 :(得分:2)

正如马丁所说,“深层”属性打破了他的解决方案。它不起作用的原因是这个表达式:

Expression.Property(
    objectParameterExpression
,  ((MemberExpression)expression.Body).Member.Name
)

它的原因并不是很明显:派生类声明了自己的属性getter,它重定向到基类,但没有定义相应的setter。

要解决此问题,您需要通过反射找到属性,查找其声明类型,然后从声明器中获取相应的属性。与派生类上的属性不同,声明器中的属性具有getter和setter,因此可以赋值。 (这假定根本声明了属性的setter:如果声明者没有提供setter,那么没有其他人可以提供它。)

这是一个解决此问题的骨架实现。通过以下Expression.Property的调用将上述调用替换为GetPropertyOrField,并且Martin的解决方案将起作用。

private static MemberExpression GetPropertyOrField(Expression baseExpr, string name) {
    if (baseExpr == null) {
        throw new ArgumentNullException("baseExpr");
    }
    if (string.IsNullOrWhiteSpace(name)) {
        throw new ArgumentException("name");
    }
    var type = baseExpr.Type;
    var properties = type
        .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
        .Where(p => p.Name.Equals(name))
        .ToArray();
    if (properties.Length == 1) {
        var res = properties[0];
        if (res.DeclaringType != type) {
            // Here is the core of the fix:
            var tmp = res
                .DeclaringType
                .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                .Where(p => p.Name.Equals(name))
                .ToArray();
            if (tmp.Length == 1) {
                return Expression.Property(baseExpr, tmp[0]);
            }
        }
        return Expression.Property(baseExpr, res);
    }
    if (properties.Length != 0) {
        throw new NotSupportedException(name);
    }
    var fields = type
        .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
        .Where(p => p.Name.Equals(name))
        .ToArray();
    if (fields.Length == 1) {
        return Expression.Field(baseExpr, fields[0]);
    }
    if (fields.Length != 0) {
        throw new NotSupportedException(name);
    }
    throw new ArgumentException(
        string.Format(
            "Type [{0}] does not define property/field called [{1}]"
        ,   type
        ,   name
        )
    );
}

答案 3 :(得分:0)

这应该可以使用expression trees,可以从lambda表达式创建,修改,然后编译成委托。