Comprobación nula profunda, ¿hay una mejor manera?


130

Nota: Esta pregunta se hizo antes de la introducción del .?operador en C # 6 / Visual Studio 2015 .

Todos hemos estado allí, tenemos algunas propiedades profundas como cake.frosting.berries.loader que debemos verificar si es nula para que no haya ninguna excepción. La forma de hacerlo es usar una declaración if de cortocircuito

if (cake != null && cake.frosting != null && cake.frosting.berries != null) ...

Esto no es exactamente elegante, y quizás debería haber una forma más fácil de verificar toda la cadena y ver si se encuentra con una variable / propiedad nula.

¿Es posible usar algún método de extensión o sería una característica del lenguaje, o es solo una mala idea?


3
Lo he deseado con bastante frecuencia, pero todas las ideas que se me ocurrieron fueron peores que el problema real.
peterchen

Gracias por todas las respuestas e interesante ver que otras personas han tenido los mismos pensamientos. Pensé en cómo me gustaría ver esto resuelto y aunque las soluciones de Eric son buenas, creo que simplemente escribiría algo como esto si (IsNull (abc)), o if (IsNotNull (abc)) pero tal vez eso es solo para mi gusto :)
Homde

Cuando crea una instancia de glaseado, tiene una propiedad de bayas, por lo que en ese punto en su constructor, ¿puede decirle al glaseado que cada vez que se instale para crear bayas vacías (no nulas)? y cada vez que se modifica el glaseado de bayas, ¿se verifica el valor ????
Doug Chamberlain

Algo vagamente relacionado, algunas de las técnicas que encontré preferibles para el problema de "nulos profundos" que estaba tratando de solucionar. stackoverflow.com/questions/818642/…
AaronLS

Respuestas:


223

Hemos considerado agregar una nueva operación "?" al lenguaje que tiene la semántica que deseas. (Y se ha agregado ahora; ver más abajo). Es decir, dirías

cake?.frosting?.berries?.loader

y el compilador generaría todas las comprobaciones de cortocircuito por usted.

No hizo la barra para C # 4. Tal vez para una futura versión hipotética del lenguaje.

Actualización (2014): El ?.operador ahora está planeado para la próxima versión del compilador de Roslyn. Tenga en cuenta que todavía hay cierto debate sobre el análisis sintáctico y semántico exacto del operador.

Actualización (julio de 2015): Visual Studio 2015 se lanzó y se entrega con un compilador de C # que admite los operadores ?.y condicional nulo?[] .


10
Sin el punto, se vuelve sintácticamente ambiguo con el operador condicional (A? B: C). Tratamos de evitar construcciones léxicas que requieren que "miremos hacia adelante" arbitrariamente lejos en el flujo de tokens. (Aunque, desafortunadamente, ya existen tales construcciones en C #; preferimos no agregar más).
Eric Lippert

33
@ Ian: este problema es extremadamente común. Esta es una de las solicitudes más frecuentes que recibimos.
Eric Lippert

77
@ Ian: También prefiero usar el patrón de objetos nulos cuando sea posible hacerlo, pero la mayoría de las personas no tienen el lujo de trabajar con modelos de objetos que ellos mismos diseñaron. Muchos modelos de objetos existentes usan valores nulos y ese es el mundo con el que tenemos que vivir.
Eric Lippert

12
@ John: Recibimos esta solicitud de características casi por completo de nuestros programadores más experimentados. Los MVP piden esto todo el tiempo . Pero entiendo que las opiniones varían; si desea hacer una sugerencia constructiva de diseño de lenguaje además de sus críticas, me complace considerarlo.
Eric Lippert

28
@lazyberezovsky: Nunca he entendido la llamada "ley" de Deméter; en primer lugar, parece llamarse con mayor precisión "La sugerencia de Demeter". Y segundo, el resultado de llevar "el acceso de un solo miembro" a su conclusión lógica es "objetos de Dios", donde se requiere que cada objeto haga todo por cada cliente, en lugar de poder entregar objetos que saben cómo hacer lo que el cliente quiere. Prefiero exactamente lo contrario de la ley de demeter: cada objeto resuelve bien un pequeño número de problemas, y una de esas soluciones puede ser "aquí hay otro objeto que resuelve mejor su problema"
Eric Lippert

27

Esta pregunta me inspiró para tratar de descubrir cómo se puede realizar este tipo de comprobación nula profunda con una sintaxis más fácil / bonita utilizando árboles de expresión. Si bien estoy de acuerdo con las respuestas que indican que podría ser un mal diseño si a menudo necesita acceder a instancias en lo profundo de la jerarquía, también creo que en algunos casos, como la presentación de datos, puede ser muy útil.

Así que creé un método de extensión, que te permitirá escribir:

var berries = cake.IfNotNull(c => c.Frosting.Berries);

Esto devolverá las Bayas si ninguna parte de la expresión es nula. Si se encuentra nulo, se devuelve nulo. Sin embargo, hay algunas advertencias, en la versión actual solo funcionará con acceso simple de miembros, y solo funciona en .NET Framework 4, porque usa el método MemberExpression.Update, que es nuevo en v4. Este es el código para el método de extensión IfNotNull:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace dr.IfNotNullOperator.PoC
{
    public static class ObjectExtensions
    {
        public static TResult IfNotNull<TArg,TResult>(this TArg arg, Expression<Func<TArg,TResult>> expression)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");

            if (ReferenceEquals(arg, null))
                return default(TResult);

            var stack = new Stack<MemberExpression>();
            var expr = expression.Body as MemberExpression;
            while(expr != null)
            {
                stack.Push(expr);
                expr = expr.Expression as MemberExpression;
            } 

            if (stack.Count == 0 || !(stack.Peek().Expression is ParameterExpression))
                throw new ApplicationException(String.Format("The expression '{0}' contains unsupported constructs.",
                                                             expression));

            object a = arg;
            while(stack.Count > 0)
            {
                expr = stack.Pop();
                var p = expr.Expression as ParameterExpression;
                if (p == null)
                {
                    p = Expression.Parameter(a.GetType(), "x");
                    expr = expr.Update(p);
                }
                var lambda = Expression.Lambda(expr, p);
                Delegate t = lambda.Compile();                
                a = t.DynamicInvoke(a);
                if (ReferenceEquals(a, null))
                    return default(TResult);
            }

            return (TResult)a;            
        }
    }
}

Funciona examinando el árbol de expresión que representa su expresión y evaluando las partes una tras otra; cada vez comprobando que el resultado no es nulo.

Estoy seguro de que esto podría extenderse para que otras expresiones que no sean MemberExpression sean compatibles. Considere esto como un código de prueba de concepto, y tenga en cuenta que habrá una penalización de rendimiento al usarlo (lo que probablemente no importará en muchos casos, pero no lo use en un ciclo cerrado :-))


Estoy impresionado por sus habilidades lambda :) Sin embargo, la sintaxis parece ser un poco más compleja de lo que uno quisiera, al menos para el escenario de declaración
if

Genial, pero funciona como 100 veces más código que un if .. &&. Solo vale la pena si todavía se compila en un if .. &&.
Monstieur

1
Ah y luego vi DynamicInvokeallí. Religiosamente evito eso :)
nawfal

24

He encontrado que esta extensión es bastante útil para escenarios de anidación profunda.

public static R Coal<T, R>(this T obj, Func<T, R> f)
    where T : class
{
    return obj != null ? f(obj) : default(R);
}

Es una idea que obtuve del operador de fusión nula en C # y T-SQL. Lo bueno es que el tipo de retorno es siempre el tipo de retorno de la propiedad interna.

De esa manera puedes hacer esto:

var berries = cake.Coal(x => x.frosting).Coal(x => x.berries);

... o una ligera variación de lo anterior:

var berries = cake.Coal(x => x.frosting, x => x.berries);

No es la mejor sintaxis que conozco, pero funciona.


¿Por qué "Carbón", que se ve muy espeluznante. ;) Sin embargo, su muestra fallaría si el glaseado fuera nulo. Debería haber parecido así: var berries = cake.NullSafe (c => c.Frosting.NullSafe (f => f.Berries));
Robert Giesecke,

Ah, pero estás insinuando que el segundo argumento no es una llamada al carbón, que por supuesto tiene que ser. Es solo una alteración conveniente. El selector (x => x.berries) se pasa a una llamada de Coal dentro del método Coal que toma dos argumentos.
John Leidegren

El nombre de fusión o fusión se tomó de T-SQL, de ahí surgió la idea. IfNotNull implica que algo ocurre si no es nulo, sin embargo, lo que es eso no se explica por la llamada al método IfNotNull. De hecho, el carbón es un nombre extraño, pero de hecho es un método extraño que vale la pena tener en cuenta.
John Leidegren

El mejor nombre literal para esto sería algo así como "ReturnIfNotNull" o "ReturnOrDefault"
John Leidegren

@flq +1 ... en nuestro proyecto también se llama IfNotNull :)
Marc Sigrist

16

Además de violar la Ley de Deméter, como ya ha señalado Mehrdad Afshari, me parece que necesita una "verificación nula profunda" para la lógica de decisión.

Este suele ser el caso cuando desea reemplazar objetos vacíos con valores predeterminados. En este caso, debería considerar implementar el Patrón de objetos nulos . Actúa como un sustituto de un objeto real, proporcionando valores predeterminados y métodos de "no acción".


no, Objective-c permite enviar mensajes a objetos nulos y devuelve el valor predeterminado apropiado si es necesario. No hay problemas allí.
Johannes Rudolph

2
Si. Ese es el punto. Básicamente, emulará el comportamiento de ObjC con un patrón de objeto nulo.
Mehrdad Afshari

10

Actualización: a partir de Visual Studio 2015, el compilador de C # (versión de lenguaje 6) ahora reconoce al ?.operador, lo que hace que la "comprobación nula profunda" sea muy sencilla. Ver esta respuesta para más detalles.

Además de rediseñar su código, como sugiere esta respuesta eliminada , otra opción (aunque terrible) sería usar un try…catchbloque para ver si NullReferenceExceptionocurre en algún momento durante esa búsqueda profunda de propiedades.

try
{
    var x = cake.frosting.berries.loader;
    ...
}
catch (NullReferenceException ex)
{
    // either one of cake, frosting, or berries was null
    ...
}

Yo personalmente no haría esto por las siguientes razones:

  • No se ve bien.
  • Utiliza el manejo de excepciones, que debe enfocarse en situaciones excepcionales y no en algo que usted espera que suceda a menudo durante el curso normal de la operación.
  • NullReferenceExceptions probablemente nunca debería ser atrapado explícitamente. (Ver esta pregunta )

Entonces, ¿es posible usar algún método de extensión o sería una función de idioma, [...]

Es casi seguro que tendría que ser una función de lenguaje (que está disponible en C # 6 en forma de .?y?[] operadores), a menos que C # ya tenía más sofisticado evaluación perezosa, o menos que desee utilizar la reflexión (que probablemente no es también una buena idea por razones de rendimiento y seguridad de tipo).

Como no hay forma de pasar simplemente cake.frosting.berries.loadera una función (se evaluaría y arrojaría una excepción de referencia nula), tendría que implementar un método de búsqueda general de la siguiente manera: toma objetos y los nombres de propiedades para buscar:

static object LookupProperty( object startingPoint, params string[] lookupChain )
{
    // 1. if 'startingPoint' is null, return null, or throw an exception.
    // 2. recursively look up one property/field after the other from 'lookupChain',
    //    using reflection.
    // 3. if one lookup is not possible, return null, or throw an exception.
    // 3. return the last property/field's value.
}

...

var x = LookupProperty( cake, "frosting", "berries", "loader" );

(Nota: código editado).

Rápidamente ve varios problemas con este enfoque. Primero, no obtienes ningún tipo de seguridad y posible boxeo de valores de propiedad de un tipo simple. En segundo lugar, puede regresar nullsi algo sale mal, y tendrá que verificar esto en su función de llamada, o lanzará una excepción y volverá a donde comenzó. Tercero, puede ser lento. Cuarto, parece más feo de lo que comenzaste.

[...], ¿o es solo una mala idea?

O me quedaría con:

if (cake != null && cake.frosting != null && ...) ...

o vaya con la respuesta anterior de Mehrdad Afshari.


PD: Cuando escribí esta respuesta, obviamente no consideraba los árboles de expresión para las funciones lambda; vea, por ejemplo, la respuesta de @driis para una solución en esta dirección. También se basa en un tipo de reflexión y, por lo tanto, podría no funcionar tan bien como una solución más simple ( if (… != null & … != null) …), pero puede considerarse mejor desde el punto de vista de la sintaxis.


2
No sé por qué esto fue rechazado, hice un voto a favor para el equilibrio: la respuesta es correcta y trae un nuevo aspecto (y menciona explícitamente los inconvenientes de esta solución ...)
MartinStettner

¿Dónde está "la respuesta anterior de Mehrdad Afshari"?
Marson Mao

1
@MarsonMao: Esa respuesta ha sido eliminada mientras tanto. (Aún puede leerlo si su rango SO es lo suficientemente alto). Gracias por señalar mi error: debería referirme a otras respuestas usando un hipervínculo, no usando palabras como "ver arriba" / "ver abajo" (ya que las respuestas no aparecen en un orden fijo). He actualizado mi respuesta.
stakx - ya no contribuye

5

Si bien la respuesta de driis es interesante, creo que es un rendimiento demasiado costoso. En lugar de compilar muchos delegados, preferiría compilar una lambda por ruta de propiedad, almacenarla en caché y luego volver a invocarla en muchos tipos.

NullCoalesce a continuación hace exactamente eso, devuelve una nueva expresión lambda con comprobaciones nulas y un retorno de default (TResult) en caso de que alguna ruta sea nula.

Ejemplo:

NullCoalesce((Process p) => p.StartInfo.FileName)

Devolverá una expresión

(Process p) => (p != null && p.StartInfo != null ? p.StartInfo.FileName : default(string));

Código:

    static void Main(string[] args)
    {
        var converted = NullCoalesce((MethodInfo p) => p.DeclaringType.Assembly.Evidence.Locked);
        var converted2 = NullCoalesce((string[] s) => s.Length);
    }

    private static Expression<Func<TSource, TResult>> NullCoalesce<TSource, TResult>(Expression<Func<TSource, TResult>> lambdaExpression)
    {
        var test = GetTest(lambdaExpression.Body);
        if (test != null)
        {
            return Expression.Lambda<Func<TSource, TResult>>(
                Expression.Condition(
                    test,
                    lambdaExpression.Body,
                    Expression.Default(
                        typeof(TResult)
                    )
                ),
                lambdaExpression.Parameters
            );
        }
        return lambdaExpression;
    }

    private static Expression GetTest(Expression expression)
    {
        Expression container;
        switch (expression.NodeType)
        {
            case ExpressionType.ArrayLength:
                container = ((UnaryExpression)expression).Operand;
                break;
            case ExpressionType.MemberAccess:
                if ((container = ((MemberExpression)expression).Expression) == null)
                {
                    return null;
                }
                break;
            default:
                return null;
        }
        var baseTest = GetTest(container);
        if (!container.Type.IsValueType)
        {
            var containerNotNull = Expression.NotEqual(
                container,
                Expression.Default(
                    container.Type
                )
            );
            return (baseTest == null ?
                containerNotNull :
                Expression.AndAlso(
                    baseTest,
                    containerNotNull
                )
            );
        }
        return baseTest;
    }


3

¡Yo también he deseado a menudo una sintaxis más simple! Se vuelve especialmente feo cuando tienes valores de retorno de método que pueden ser nulos, porque entonces necesitas variables adicionales (por ejemplo:cake.frosting.flavors.FirstOrDefault().loader )

Sin embargo, aquí hay una alternativa bastante decente que utilizo: crear un método auxiliar Null-Safe-Chain. Me doy cuenta de que esto es bastante similar a la respuesta de @ John anterior (con el Coalmétodo de extensión), pero creo que es más sencillo y menos tipeado. Así es como se ve:

var loader = NullSafe.Chain(cake, c=>c.frosting, f=>f.berries, b=>b.loader);

Aquí está la implementación:

public static TResult Chain<TA,TB,TC,TResult>(TA a, Func<TA,TB> b, Func<TB,TC> c, Func<TC,TResult> r) 
where TA:class where TB:class where TC:class {
    if (a == null) return default(TResult);
    var B = b(a);
    if (B == null) return default(TResult);
    var C = c(B);
    if (C == null) return default(TResult);
    return r(C);
}

También creé varias sobrecargas (con 2 a 6 parámetros), así como sobrecargas que permiten que la cadena termine con un tipo de valor o predeterminado. ¡Esto funciona muy bien para mí!



1

Como se sugiere en la respuesta de John Leidegren , un enfoque para evitar esto es utilizar métodos de extensión y delegados. Usarlos podría verse más o menos así:

int? numberOfBerries = cake
    .NullOr(c => c.Frosting)
    .NullOr(f => f.Berries)
    .NullOr(b => b.Count());

La implementación es desordenada porque necesita que funcione para los tipos de valor, los tipos de referencia y los tipos de valores anulables. Puede encontrar una implementación completa en la respuesta de Timwi a ¿Cuál es la forma correcta de verificar los valores nulos? .


1

O puedes usar la reflexión :)

Función de reflexión:

public Object GetPropValue(String name, Object obj)
    {
        foreach (String part in name.Split('.'))
        {
            if (obj == null) { return null; }

            Type type = obj.GetType();
            PropertyInfo info = type.GetProperty(part);
            if (info == null) { return null; }

            obj = info.GetValue(obj, null);
        }
        return obj;
    }

Uso:

object test1 = GetPropValue("PropertyA.PropertyB.PropertyC",obj);

Mi caso (devuelve DBNull.Value en lugar de nulo en la función de reflexión):

cmd.Parameters.AddWithValue("CustomerContactEmail", GetPropValue("AccountingCustomerParty.Party.Contact.ElectronicMail.Value", eInvoiceType));

1

Prueba este código:

    /// <summary>
    /// check deep property
    /// </summary>
    /// <param name="obj">instance</param>
    /// <param name="property">deep property not include instance name example "A.B.C.D.E"</param>
    /// <returns>if null return true else return false</returns>
    public static bool IsNull(this object obj, string property)
    {
        if (string.IsNullOrEmpty(property) || string.IsNullOrEmpty(property.Trim())) throw new Exception("Parameter : property is empty");
        if (obj != null)
        {
            string[] deep = property.Split('.');
            object instance = obj;
            Type objType = instance.GetType();
            PropertyInfo propertyInfo;
            foreach (string p in deep)
            {
                propertyInfo = objType.GetProperty(p);
                if (propertyInfo == null) throw new Exception("No property : " + p);
                instance = propertyInfo.GetValue(instance, null);
                if (instance != null)
                    objType = instance.GetType();
                else
                    return true;
            }
            return false;
        }
        else
            return true;
    }

0

Publiqué esto anoche y luego un amigo me señaló esta pregunta. Espero eso ayude. Entonces puedes hacer algo como esto:

var color = Dis.OrDat<string>(() => cake.frosting.berries.color, "blue");


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace DeepNullCoalescence
{
  public static class Dis
  {
    public static T OrDat<T>(Expression<Func><T>> expr, T dat)
    {
      try
      {
        var func = expr.Compile();
        var result = func.Invoke();
        return result ?? dat; //now we can coalesce
      }
      catch (NullReferenceException)
      {
        return dat;
      }
    }
  }
}

Lea la publicación completa del blog aquí .

El mismo amigo también sugirió que veas esto .


3
¿Por qué molestarse con un Expressionsi solo vas a compilar y atrapar? Solo usa a Func<T>.
Scott Rippey

0

Modifiqué ligeramente el código desde aquí para que funcione para la pregunta que se hace:

public static class GetValueOrDefaultExtension
{
    public static TResult GetValueOrDefault<TSource, TResult>(this TSource source, Func<TSource, TResult> selector)
    {
        try { return selector(source); }
        catch { return default(TResult); }
    }
}

Y sí, probablemente esta no sea la solución óptima debido a las implicaciones de rendimiento de prueba / captura, pero funciona:>

Uso:

var val = cake.GetValueOrDefault(x => x.frosting.berries.loader);

0

Donde necesites lograr esto, haz esto:

Uso

Color color = someOrder.ComplexGet(x => x.Customer.LastOrder.Product.Color);

o

Color color = Complex.Get(() => someOrder.Customer.LastOrder.Product.Color);

Implementación de clase auxiliar

public static class Complex
{
    public static T1 ComplexGet<T1, T2>(this T2 root, Func<T2, T1> func)
    {
        return Get(() => func(root));
    }

    public static T Get<T>(Func<T> func)
    {
        try
        {
            return func();
        }
        catch (Exception)
        {
            return default(T);
        }
    }
}

-3

Me gusta el enfoque adoptado por Objective-C:

"El lenguaje Objective-C adopta otro enfoque para este problema y no invoca métodos en nulo, sino que devuelve nulo para todas esas invocaciones".

if (cake.frosting.berries != null) 
{
    var str = cake.frosting.berries...;
}

1
lo que hace otro lenguaje (y su opinión al respecto) es casi totalmente irrelevante para que funcione en C #. No ayuda a nadie resolver su problema de C #
ADyson
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.