c# lambda expression-trees nullreferenceexception lifting

c# - ¿Cómo desgloso una cadena de expresiones de acceso de miembros?



lambda expression-trees (1)

La Versión Corta (TL; DR):

Supongamos que tengo una expresión que es solo una cadena de operadores de acceso de miembros:

Expression<Func<Tx, Tbaz>> e = x => x.foo.bar.baz;

Puede pensar en esta expresión como una composición de subexpresiones, cada una de las cuales comprende una operación de acceso de miembro:

Expression<Func<Tx, Tfoo>> e1 = (Tx x) => x.foo; Expression<Func<Tfoo, Tbar>> e2 = (Tfoo foo) => foo.bar; Expression<Func<Tbar, Tbaz>> e3 = (Tbar bar) => bar.baz;

Lo que quiero hacer es desglosar estas subexpresiones de componentes para poder trabajar con ellas individualmente.

La versión aún más corta:

Si tengo la expresión x => x.foo.bar , ya sé cómo separar x => x.foo . ¿Cómo puedo sacar la otra foo => foo.bar , foo => foo.bar ?

Por qué estoy haciendo esto:

Estoy intentando simular el "levantamiento" del operador de acceso miembro en C #, como el operador de acceso existencial de CoffeeScript ?. . Eric Lippert ha declarado que un operador similar fue considerado para C #, pero no había presupuesto para implementarlo.

Si tal operador existiera en C #, podría hacer algo como esto:

value = target?.foo?.bar?.baz;

Si alguna parte de la cadena target.foo.bar.baz resultó ser nula, entonces todo esto se evaluaría como nulo, evitando así una NullReferenceException.

Quiero un método de extensión Lift que pueda simular este tipo de cosas:

value = target.Lift(x => x.foo.bar.baz); //returns target.foo.bar.baz or null

Lo que he intentado:

Tengo algo que compila, y tipo de trabajo. Sin embargo, está incompleto porque solo sé cómo mantener el lado izquierdo de una expresión de acceso de miembro. Puedo convertir x => x.foo.bar.baz en x => x.foo.bar , pero no sé cómo mantener bar => bar.baz .

Entonces termina haciendo algo como esto (pseudocódigo):

return (x => x)(target) == null ? null : (x => x.foo)(target) == null ? null : (x => x.foo.bar)(target) == null ? null : (x => x.foo.bar.baz)(target);

Esto significa que los pasos más a la izquierda en la expresión se evalúan una y otra vez. Tal vez no sea un gran problema si solo son propiedades en objetos POCO, pero conviértalos en llamadas de método y la ineficiencia (y los posibles efectos secundarios) se vuelven mucho más evidentes:

//still pseudocode return (x => x())(target) == null ? null : (x => x().foo())(target) == null ? null : (x => x().foo().bar())(target) == null ? null : (x => x().foo().bar().baz())(target);

El código:

static TResult Lift<T, TResult>(this T target, Expression<Func<T, TResult>> exp) where TResult : class { //omitted: if target can be null && target == null, just return null var memberExpression = exp.Body as MemberExpression; if (memberExpression != null) { //if memberExpression is {x.foo.bar}, then innerExpression is {x.foo} var innerExpression = memberExpression.Expression; var innerLambda = Expression.Lambda<Func<T, object>>( innerExpression, exp.Parameters ); if (target.Lift(innerLambda) == null) { return null; } else { ////This is the part I''m stuck on. Possible pseudocode: //var member = memberExpression.Member; //return GetValueOfMember(target.Lift(innerLambda), member); } } //For now, I''m stuck with this: return exp.Compile()(target); }

Esto fue libremente inspirado por esta respuesta .

Alternativas a un método de elevación, y por qué no puedo usarlas:

La mónada Tal vez

value = x.ToMaybe() .Bind(y => y.foo) .Bind(f => f.bar) .Bind(b => b.baz) .Value; Pros:

  1. Utiliza un patrón existente que es popular en la programación funcional.
  2. Tiene otros usos además del acceso de miembro levantado.
Contras:
  1. Es demasiado detallado. No quiero una cadena masiva de llamadas a funciones cada vez que quiero desglosar a algunos miembros. Incluso si implemento SelectMany y utilizo la sintaxis de consulta, IMHO que se verá más desordenado, no menos.
  2. Tengo que volver a escribir manualmente x.foo.bar.baz como sus componentes individuales, lo que significa que tengo que saber qué son en tiempo de compilación. No puedo usar una expresión de una variable como result = Lift(expr, obj); .
  3. Realmente no está diseñado para lo que estoy tratando de hacer, y no se siente como un ajuste perfecto.

ExpressionVisitor

Modifiqué el método LiftMemberAccessToNull de Ian Griffith en un método de extensión genérico que se puede usar como se describe. El código es demasiado largo para incluirlo aquí, pero publicaré un Gist si alguien está interesado.

Pros:
  1. Sigue la result = target.Lift(x => x.foo.bar.baz) del result = target.Lift(x => x.foo.bar.baz)
  2. Funciona muy bien si cada paso en la cadena devuelve un tipo de referencia o un tipo de valor no anulable
Contras:
  1. Se ahoga si algún miembro de la cadena es un tipo de valor anulable, lo que realmente limita su utilidad para mí. Necesito que funcione para los Nullable<DateTime> .

Trata de atraparlo

try { value = x.foo.bar.baz; } catch (NullReferenceException ex) { value = null; }

Esta es la forma más obvia, y es la que usaré si no puedo encontrar una forma más elegante.

Pros:
  1. Es sencillo.
  2. Es obvio para qué es el código.
  3. No tengo que preocuparme por los casos de borde.
Contras:
  1. Es feo y detallado
  2. El bloque try / catch es un hit de rendimiento nontrivial*
  3. Es un bloque de instrucciones, así que no puedo hacer que emita un árbol de expresiones para LINQ
  4. Se siente como admitir la derrota

No voy a mentir; "no admitir la derrota" es la razón principal por la que soy tan terco. Mis instintos dicen que debe haber una manera elegante de hacer esto, pero encontrarlo ha sido un desafío. No puedo creer que sea tan fácil acceder al lado izquierdo de una expresión, pero el lado derecho es casi inalcanzable.

Realmente tengo dos problemas aquí, así que aceptaré cualquier cosa que resuelva cualquiera de los dos:

  • Descomposición de expresión que preserva ambos lados, tiene un rendimiento razonable y funciona en cualquier tipo
  • Acceso de miembro de propagación nula

Actualizar:

El acceso de miembro de propagación nula está previsto para incluirse en C # 6.0 . Sin embargo, todavía me gustaría una solución para la descomposición de la expresión.


Si es solo una simple cadena de expresiones de acceso de miembros, hay una solución fácil:

public static TResult Lift<T, TResult>(this T target, Expression<Func<T, TResult>> exp) where TResult : class { return (TResult) GetValueOfExpression(target, exp.Body); } private static object GetValueOfExpression<T>(T target, Expression exp) { if (exp.NodeType == ExpressionType.Parameter) { return target; } else if (exp.NodeType == ExpressionType.MemberAccess) { var memberExpression = (MemberExpression) exp; var parentValue = GetValueOfExpression(target, memberExpression.Expression); if (parentValue == null) { return null; } else { if (memberExpression.Member is PropertyInfo) return ((PropertyInfo) memberExpression.Member).GetValue(parentValue, null); else return ((FieldInfo) memberExpression.Member).GetValue(parentValue); } } else { throw new ArgumentException("The expression must contain only member access calls.", "exp"); } }

EDITAR

Si desea agregar soporte para llamadas a métodos, use este método actualizado:

private static object GetValueOfExpression<T>(T target, Expression exp) { if (exp == null) { return null; } else if (exp.NodeType == ExpressionType.Parameter) { return target; } else if (exp.NodeType == ExpressionType.Constant) { return ((ConstantExpression) exp).Value; } else if (exp.NodeType == ExpressionType.Lambda) { return exp; } else if (exp.NodeType == ExpressionType.MemberAccess) { var memberExpression = (MemberExpression) exp; var parentValue = GetValueOfExpression(target, memberExpression.Expression); if (parentValue == null) { return null; } else { if (memberExpression.Member is PropertyInfo) return ((PropertyInfo) memberExpression.Member).GetValue(parentValue, null); else return ((FieldInfo) memberExpression.Member).GetValue(parentValue); } } else if (exp.NodeType == ExpressionType.Call) { var methodCallExpression = (MethodCallExpression) exp; var parentValue = GetValueOfExpression(target, methodCallExpression.Object); if (parentValue == null && !methodCallExpression.Method.IsStatic) { return null; } else { var arguments = methodCallExpression.Arguments.Select(a => GetValueOfExpression(target, a)).ToArray(); // Required for comverting expression parameters to delegate calls var parameters = methodCallExpression.Method.GetParameters(); for (int i = 0; i < parameters.Length; i++) { if (typeof(Delegate).IsAssignableFrom(parameters[i].ParameterType)) { arguments[i] = ((LambdaExpression) arguments[i]).Compile(); } } if (arguments.Length > 0 && arguments[0] == null && methodCallExpression.Method.IsStatic && methodCallExpression.Method.IsDefined(typeof(ExtensionAttribute), false)) // extension method { return null; } else { return methodCallExpression.Method.Invoke(parentValue, arguments); } } } else { throw new ArgumentException( string.Format("Expression type ''{0}'' is invalid for member invoking.", exp.NodeType)); } }