2010-05-15 23 views
7

Es relativamente fácil crear una función lambda que devolverá el valor de una propiedad de un objeto, incluso incluyendo propiedades profundas ...Crear acción lambda de expresión de función

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

y esto puede ser llamado como de la siguiente manera ...

string categoryName = getCategoryName(this.category); 

Pero, dado única la función resultante por encima (o la expresión utilizada originalmente para crear la función), ¿alguien puede proporcionar una manera fácil crear la acción de oposición ...

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

... que permitirán el mismo valor de la propiedad que desea establecer como sigue?

setCategoryName(this.category, ""); 

en cuenta que estoy buscando una manera de crear la acción mediante programación de la función o expresión - espero que he demostrado que yo ya sé cómo crear manualmente.

Estoy abierto a respuestas que funcionen tanto en .net 3.5 como en 4.0.

Gracias.

ACTUALIZACIÓN:

Tal vez no estoy siendo claro en mi pregunta, así que vamos a tratar y demostrar más claramente lo que estoy tratando de hacer.

tengo el siguiente método (que he creado a los efectos de esta pregunta) ...

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); 

} 

Lo que estoy buscando es ¿cómo puedo crear el "assignmentExpression" dentro del código para que Puedo compilarlo en setValue? Me imagino que está relacionado con Expression.Assign, pero simplemente no puedo calcular la combinación correcta de parámetros para completar el código.

El resultado final es ser capaz de llamar a

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

y esto a su vez creará un captador y definidor de la propiedad "Nombre" del objeto Categoría.

La versión anterior compila, pero cuando llamo a setValue() da como resultado una ArgumentException con "La expresión debe poder escribirse".

Gracias de nuevo.

+0

Realmente no entiendo a qué se refiere al hacerlo se opuso de forma automática a manual. Sin embargo, si desea establecer propiedades y decidir qué propiedad desea establecer en tiempo de ejecución, debe usar el reflejo. Además, puede usar el árbol de expresiones para construir expresiones lambda en tiempo de ejecución. – Henri

Respuesta

0

Esto debería ser posible usando expression trees, que se puede crear a partir de expresiones lambda, modificar y luego compilar en un delegado.

+5

Este es el tipo de cosa que estaba buscando, ¡solo esperaba un ejemplo! –

3

Ok, el código Busco algo como esto ...

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(); 

Este código funciona, pero sólo para las propiedades superficiales (es decir, las propiedades del objeto inmediato), pero no funciona para propiedades profundas, aunque la función getter puede funcionar para propiedades profundas. Sería interesante saber si alguien puede ayudarme a modificar esto para trabajar con propiedades profundas, pero plantearé esto como una pregunta separada.

+2

Este código solo funciona en .NET 4 ya que requiere el soporte mejorado para árboles de expresiones introducido en .NET 4. –

2

Como señaló Martin anteriormente, las propiedades "profundas" rompen su solución. La razón por la que no funciona es la siguiente expresión:

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

La razón de que no es inmediatamente obvio: la clase derivada declara su propia propiedad de captador, que redirige a la clase base, pero no define un regulador correspondiente .

Para solucionar este problema, debe ubicar la propiedad mediante reflexión, buscar su tipo de declaración y, a continuación, obtener la propiedad correspondiente del declarante. A diferencia de la propiedad que obtienes en la clase derivada, la propiedad del declarante tiene un getter y un setter, y por lo tanto es asignable. (Esto asume que un setter de la propiedad está declarado en absoluto: si el declarante no proporciona un setter, entonces nadie más podría proporcionarlo).

Aquí hay una implementación esquemática que resuelve este problema. Reemplace la llamada anterior al Expression.Property con la llamada de GetPropertyOrField a continuación, y la solución de Martin va a funcionar.

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 
     ) 
    ); 
} 
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); 
} 
+0

¿Podría explicarnos qué hace su código? – Math

+0

+1, tan simple como eso. – nawfal

+1

+1. Usando este código también puedo establecer valores para 'private fields' y' nested private properties'. El tipo de campo/propiedad también podría ser una 'struct' y simplemente funciona. Me encantaría una explicación de cómo funciona esto. –

Cuestiones relacionadas