2009-05-12 11 views
13

¿Cómo puedo verificar nulos en una expresión profunda de lamda?¿Cómo comprobar los nulos en una expresión lambda profunda?

Digamos por ejemplo tengo una estructura de clases que se anida varias capas de profundidad, y quería ejecutar el siguiente lambda:

x => x.Two.Three.Four.Foo 

quiero que vuelva nulo si dos, tres, o cuatro eran nulas , en lugar de arrojar una System.NullReferenceException.

public class Tests 
{ 
    // This test will succeed 
    [Fact] 
    public void ReturnsValueWhenClass2NotNull() 
    { 
     var one = new One(); 
     one.Two = new Two(); 
     one.Two.Three = new Three(); 
     one.Two.Three.Four = new Four(); 
     one.Two.Three.Four.Foo = "blah"; 

     var result = GetValue(one, x => x.Two.Three.Four.Foo); 

     Assert.Equal("blah", result); 
    } 

    // This test will fail 
    [Fact] 
    public void ReturnsNullWhenClass2IsNull() 
    { 
     var one = new One(); 

     var result = GetValue(one, x => x.Two.Three.Four.Foo); 

     Assert.Equal(null, result); 
    } 

    private TResult GetValue<TModel, TResult>(TModel model, Expression<Func<TModel, TResult>> expression) 
    { 
     var func = expression.Compile(); 
     var value = func(model); 
     return value; 
    } 

    public class One 
    { 
     public Two Two { get; set; } 
    } 

    public class Two 
    { 
     public Three Three { get; set; } 
    } 

    public class Three 
    { 
     public Four Four { get; set; } 
    } 

    public class Four 
    { 
     public string Foo { get; set; } 
     public string Bar { get; set; } 
    } 
} 

ACTUALIZACIÓN:

Una solución sería para coger el NullReferenceException así:

private TResult GetValue<TModel, TResult>(TModel model, Expression<Func<TModel, TResult>> expression) 
    { 
     TResult value; 
     try 
     { 
      var func = expression.Compile(); 
      value = func(model); 
     } 
     catch (NullReferenceException) 
     { 
      value = default(TResult); 
     } 
     return value; 
    } 

pero odio a incurrir en el gasto de la captura de una excepción que no es, en mi mente, excepcional. Espero que este sea el caso con bastante frecuencia en mi dominio.

ACTUALIZACIÓN 2:

Otra solución sería modificar los captadores propiedad como esta:

public class One 
    { 
     private Two two; 
     public Two Two 
     { 
      get 
      { 
       return two ?? new Two(); 
      } 
      set 
      { 
       two = value; 
      } 
     } 
    } 

que es sobre todo bien para mi dominio, pero hay momentos en los que realmente que se puede esperar de una propiedad para volver nulo. Revisé la respuesta de Josh E como útil, ya que se aproxima bastante a lo que necesito en algunos casos.

Respuesta

12

No puede hacer eso de manera concisa. Usted puede hacer que las múltiples líneas de lambda, o utilizar los operadores ternarios anidados:

var result = GetValue(one, x => x.Two == null ? null : 
           x.Two.Three == null ? null : 
           x.Two.Three.Four == null ? null : 
           x.Two.Three.Four.Foo; 

feo, lo sé.

+2

esto es mucho más conciso. Tal vez (x => x.Two.Three.Four.Foo); ver http://maybe.codeplex.com/ – Maslow

17

Usted puede hacer esto con un método de extensión de ayuda genérica, algo así como:

public static class Get { 
    public static T IfNotNull<T, U>(this U item, Func<U, T> lambda) where U: class { 
     if (item == null) { 
      return default(T); 
     } 
     return lambda(item); 
    } 
} 

var one = new One(); 
string fooIfNotNull = one.IfNotNull(x => x.Two).IfNotNull(x => x.Three).IfNotNull(x => x.Four).IfNotNull(x => x.Foo); 
+0

En ese caso, me gustaría devolver el valor predeterminado para cualquier tipo de Foo o Bar. Lo que realmente quiero evitar es la excepción si algo más arriba en el árbol de expresiones fue nulo. – JohnRudolfLewis

+0

Edité mi respuesta y agregué un ejemplo de código, que compila bien y debería ser el truco. – Lucero

+0

¿Así de fácil? Eso elegante? +1 Y no se ejecutó el rendimiento del tiempo de ejecución debido a la reflexión. ¿Alguien ha comparado esto con la solución de Gabe o el enfoque "normal"? –

0

inicializar siempre sus propiedades antes de usarlos. Agregue un constructor a la clase Uno, Dos, Tres y Cuatro. En el constructor inicializa tus propiedades para que no sean nulas.

+0

Normalmente lo hago, pero en este dominio, las propiedades pueden establecerse como nulas algunas veces. Este es un comportamiento válido. – JohnRudolfLewis

0

Se podría modificar captadores de leer algo como:

private Two _two; 
public Two Two 
{ 
    get 
    { 
     if (null == _two) 
     return new Two(); 
     else 
     return _two; 
     } 
} 
+0

La modificación de la implementación para guardar algunas líneas en el código del cliente debe disparar todo tipo de alarmas. –

+1

Tiendo a estar en desacuerdo con que esto es un problema: yo llamaría a esto codificación defensiva.El código anterior asegura que el valor de una propiedad nunca es nulo sin compartir ese conocimiento con cualquier consumidor de esa propiedad/objeto. –

+1

Si sigo llamando a Two mientras _two es nulo, sigo obteniendo * nuevas * instancias de Two ... ew – Lucas

2

No soy experto en C#, pero tal vez hay alguna manera de poner en práctica el "andand" patrón de rubí que resuelve exactamente este problema sin contaminar la implementación.

El concepto también se conoce como Maybe Monad en Haskell.

El título del artículo this parece prometedor.

+1

Interesante, el artículo es casi idéntico a la solución que surgió al pensar en él, ver mi publicación ... – Lucero

+0

Guau, completamente perdido, creo que estaba buscando la palabra "tal vez" –

+0

http: // tal vez. codeplex.com/ puede hacerlo. – Maslow

0

Encuentro que el operador de coalescencia es útil para esto ocasionalmente. Esto solo ayuda si hay una versión predeterminada/nula equivalente del objeto que puede colocar.

Por ejemplo, a veces cuando estoy crackeando XML abierto ...

IEnumeratable<XElement> sample; 
sample.Where(S => (S.Attribute["name"] ?? new XAttribute("name","")).Value.StartsWith("Hello"))... 

Dependiendo de cómo los objetos predeterminados se recuperan esto puede ser prolijo, y el ejemplo anterior no es un gran uso, pero se entiende la idea. Para el caso particular de lectura de atributos XML, tengo un método de extensión que devuelve el valor del atributo o una cadena vacía.

7

Hacer esto de manera concisa requiere un operador aún no implementado. Consideramos agregar un operador "." a C# 4.0, que tendría su semántica deseada, pero desafortunadamente no encajaba en nuestro presupuesto. Lo consideraremos para una versión futura hipotética del idioma.

+0

¡Esto sería genial! El prisma Delphi hace lo mismo con su operador ":": http://prismwiki.codegear.com/en/Colon_Operator –

+0

Yo en segundo lugar. ¡Esto haría que muchos códigos fueran mucho más limpios! – mdonatas

+0

¡Esta función ahora está en C# 6! –

4

Ahora puede hacerlo utilizando el proyecto Maybe en codeplex.

sintaxis es:

string result = One.Maybe(o => o.Two.Three.Four.Foo); 

string cityName = Employee.Maybe(e => e.Person.Address.CityName); 
+0

¿Utiliza el rendimiento de los efectos del árbol de expresiones? –

+3

hace algo diferente cambiar las características de rendimiento? sí. ¿es suficiente que usted o un usuario lo noten? No sé tu uso, perfilalo. – Maslow

+0

¿Tiene alguna estadística de rendimiento de su (u otra persona) uso? –

0

I convierte una función que utiliza una gran cantidad de si las declaraciones para evitar los nulos al método .IFNotNull para las clases que convierten de un XSD que son 4 y 5 niveles de profundidad.

Estas son unas pocas líneas del código convertido:

ProdYear.PRIOR_CUMULATIVE_CARBON_DIOXIDE_VALUE = year.IfNotNull(x => x.PRIOR_CUMULATIVE).IfNotNull(y => y.CARBON_DIOXIDE).IfNotNull(z => z.VALUE).ToDouble(); 
ProdYear.PRIOR_CUMULATIVE_CARBON_DIOXIDE_UOM = year.IfNotNull(x => x.PRIOR_CUMULATIVE).IfNotNull(y => y.CARBON_DIOXIDE).IfNotNull(z => z.UOM); 

Aquí hay algunos datos interesantes al respecto:

1) Este nuevo método tomó 3.7409 veces más en ejecutarse que la variación con el caso Declaraciones.
2) Reduje el recuento de mi línea de función de 157 a 59.
3) CodeRush de DevExpress tiene una puntuación de "Complejidad de mantenimiento". Cuando me convertí a las declaraciones de Lambda, aumentó de 984 a 2076, que en teoría es mucho más difícil de mantener.

3

He escrito un método de extensión que le permite hacer esto:

blah.GetValueOrDefault(x => x.Two.Three.Four.Foo); 

Utiliza los árboles de expresión para construir una comprobación condicional anidada para los nulos en cada nodo antes de devolver el valor de la expresión; el árbol de expresiones creado se compila en un Func y se almacena en caché, por lo que los usos posteriores de la misma llamada deberían ejecutarse a una velocidad casi nativa.

También se puede pasar de un valor por defecto para volver si te gusta:

blah.GetValueOrDefault(x => x.Two.Three.Four.Foo, Foo.Empty); 

he escrito un blog sobre ello here.

Cuestiones relacionadas