C # forma elegante de verificar si la propiedad de una propiedad es nula

97

En C #, diga que desea extraer un valor de PropertyC en este ejemplo y ObjectA, PropertyA y PropertyB pueden ser todos nulos.

ObjectA.PropertyA.PropertyB.PropertyC

¿Cómo puedo obtener PropertyC de forma segura con la menor cantidad de código?

Ahora mismo comprobaría:

if(ObjectA != null && ObjectA.PropertyA !=null && ObjectA.PropertyA.PropertyB != null)
{
    // safely pull off the value
    int value = objectA.PropertyA.PropertyB.PropertyC;
}

Sería bueno hacer algo más como esto (pseudocódigo).

int value = ObjectA.PropertyA.PropertyB ? ObjectA.PropertyA.PropertyB : defaultVal;

Posiblemente colapsó aún más con un operador de fusión nula.

EDITAR Originalmente dije que mi segundo ejemplo era como js, ​​pero lo cambié a psuedo-code ya que se señaló correctamente que no funcionaría en js.

Jon Kragh
fuente
No veo cómo funciona tu ejemplo de js. debería obtener un error de "objeto esperado" siempre que ObjectAo PropertyAsean nulos.
lincolnk

Respuestas:

117

En C # 6 puede usar el operador condicional nulo . Entonces la prueba original será:

int? value = objectA?.PropertyA?.PropertyB?.PropertyC;
Phillip Ngan
fuente
2
¿Podrías explicar qué hace esto? ¿Qué es valueigual a si PropertyCes nulo? o si PropertyBes nulo? ¿y si Object Aes nulo?
Kolob Canyon
1
Si CUALQUIERA de estas propiedades es nula, toda la declaración devuelve como null. Empieza de izquierda a derecha. Sin el azúcar sintáctico, esto es equivalente a una serie de declaraciones if donde if(propertyX == null) {value = null} else if (propertyY == null){ value = null} else if......la última expresión eventual esif(propertyZ != null) { value = propertyZ }
DetectivePikachu
27

Método de extensión corto:

public static TResult IfNotNull<TInput, TResult>(this TInput o, Func<TInput, TResult> evaluator)
  where TResult : class where TInput : class
{
  if (o == null) return null;
  return evaluator(o);
}

Utilizando

PropertyC value = ObjectA.IfNotNull(x => x.PropertyA).IfNotNull(x => x.PropertyB).IfNotNull(x => x.PropertyC);

Puede encontrar este método de extensión simple y mucho más en http://devtalk.net/csharp/chained-null-checks-and-the-maybe-monad/

EDITAR:

Después de usarlo por un momento, creo que el nombre correcto para este método debería ser IfNotNull () en lugar del original With ().

Krzysztof Morcinek
fuente
15

¿Puedes agregar un método a tu clase? Si no es así, ¿ha pensado en utilizar métodos de extensión? Puede crear un método de extensión para su tipo de objeto llamado GetPropC().

Ejemplo:

public static class MyExtensions
{
    public static int GetPropC(this MyObjectType obj, int defaltValue)
    {
        if (obj != null && obj.PropertyA != null & obj.PropertyA.PropertyB != null)
            return obj.PropertyA.PropertyB.PropertyC;
        return defaltValue;
    }
}

Uso:

int val = ObjectA.GetPropC(0); // will return PropC value, or 0 (defaltValue)

Por cierto, esto supone que está utilizando .NET 3 o superior.

Sam
fuente
11

La forma en que lo estás haciendo es correcta.

Usted podría utilizar un truco como el descrito aquí , usando expresiones LINQ:

int value = ObjectA.NullSafeEval(x => x.PropertyA.PropertyB.PropertyC, 0);

Pero es mucho más lento que verificar manualmente cada propiedad ...

Thomas Levesque
fuente
10

Refactorizar para observar la Ley de Deméter

rtalbot
fuente
No considero que un gráfico de objetos de solo tres niveles de profundidad necesite refactorización cuando solo está leyendo propiedades. Estoy de acuerdo si el OP quisiera llamar a un método en un objeto al que se hace referencia a través de PropertyC, pero no cuando es una propiedad que solo necesita verificar si es nula antes de leer. En este ejemplo, podría ser tan simple como Customer.Address.Country donde Country podría ser un tipo de referencia como KeyValuePair. ¿Cómo refactorizaría esto para evitar la necesidad de verificaciones de referencia nula?
Darren Lewis
El ejemplo de OP es en realidad 4 de profundidad. Mi sugerencia no es eliminar las comprobaciones de referencia nula, sino ubicarlas en los objetos con mayor probabilidad de poder manejarlas correctamente. Como la mayoría de las "reglas generales", hay excepciones, pero no estoy convencido de que esta sea una. ¿Podemos aceptar estar en desacuerdo?
rtalbot
3
Estoy de acuerdo con @rtalbot (aunque, para ser justos, @Daz Lewis está proponiendo un ejemplo de 4 profundidades, ya que el último elemento es un KeyValuePair). Si algo está jugando con un objeto Cliente, entonces no veo qué negocio tiene mirando a través de la jerarquía del objeto Dirección. Suponga que luego decide que un KeyValuePair no fue una idea tan buena para la propiedad Country. En ese caso, el código de todos tiene que cambiar. Ese no es un buen diseño.
Jeffrey L Whitledge
10

Actualización 2014: C # 6 tiene un nuevo operador ?.llamado 'navegación segura' o 'propagación nula'

parent?.child

Lea http://blogs.msdn.com/b/jerrynixon/archive/2014/02/26/at-last-c-is-getting-sometimes-called-the-safe-navigation-operator.aspx para obtener más detalles

Esta ha sido durante mucho tiempo una solicitud muy popular https://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/3990187-add-operator-to-c-?tracking_code=594c10a522f8e9bc987ee4a5e2c0b38d

Coronel Panic
fuente
8

Obviamente, está buscando la mónada anulable :

string result = new A().PropertyB.PropertyC.Value;

se convierte en

string result = from a in new A()
                from b in a.PropertyB
                from c in b.PropertyC
                select c.Value;

Esto devuelve null, si alguna de las propiedades que aceptan valores NULL es nula; de lo contrario, el valor de Value.

class A { public B PropertyB { get; set; } }
class B { public C PropertyC { get; set; } }
class C { public string Value { get; set; } }

Métodos de extensión LINQ:

public static class NullableExtensions
{
    public static TResult SelectMany<TOuter, TInner, TResult>(
        this TOuter source,
        Func<TOuter, TInner> innerSelector,
        Func<TOuter, TInner, TResult> resultSelector)
        where TOuter : class
        where TInner : class
        where TResult : class
    {
        if (source == null) return null;
        TInner inner = innerSelector(source);
        if (inner == null) return null;
        return resultSelector(source, inner);
    }
}
dtb
fuente
¿Por qué está aquí el método de extensión? No se está utilizando.
Mladen Mihajlovic
1
@MladenMihajlovic: el SelectManymétodo de extensión es utilizado por la from ... in ... from ... in ...sintaxis.
dtb
5

Suponiendo que tiene valores vacíos de tipos, un enfoque sería este:

var x = (((objectA ?? A.Empty).PropertyOfB ?? B.Empty).PropertyOfC ?? C.Empty).PropertyOfString;

Soy un gran fan de C #, pero una cosa muy buena en el nuevo Java (¿1.7?) Es el.? operador:

 var x = objectA.?PropertyOfB.?PropertyOfC.?PropertyOfString;
Solo otro metaprogramador
fuente
1
¿Realmente estará en Java 1.7? Se ha solicitado en C # durante mucho tiempo, pero dudo que alguna vez suceda ...
Thomas Levesque
Desafortunadamente, no tengo valores vacíos. ¡Sin embargo, esa sintaxis de Java parece dulce! Voy a votar a favor de esto, ¡solo porque quiero esa sintaxis!
Jon Kragh
3
Thomas: La última vez que revisé tech.puredanger.com/java7 , implicaba que Java lo obtendría. Pero ahora, cuando vuelvo a comprobar, dice: Nulo manejo seguro: NO. Así que revoco mi declaración y la reemplazo por una nueva: se propuso para Java 1.7 pero no lo hizo.
Solo otro metaprogramador
Un enfoque adicional es el que usa monad.net
solo otro metaprogramador
1
Parece que el?. El operador está en Visual Studio 2015 https://msdn.microsoft.com/en-us/library/dn986595.aspx
Edward
4

Este código es "la menor cantidad de código", pero no la mejor práctica:

try
{
    return ObjectA.PropertyA.PropertyB.PropertyC;
}
catch(NullReferenceException)
{
     return null;
}
Boris Modylevsky
fuente
1
He visto mucho código como este y, sin tener en cuenta la pérdida de rendimiento, el mayor problema es que complica la depuración porque la excepción real se ahoga en millones de excepciones de referencia nula inútiles.
Solo otro metaprogramador
A veces es divertido leer mi propia respuesta después de 3 años. Creo que hoy respondería de manera diferente. Diría que el código viola la ley de Demeter y recomendaría refactorizarlo para que no lo hiciera.
Boris Modylevsky
1
A partir de hoy, 7 años después de la respuesta original, me uniría a @Phillip Ngan y usaría C # 6 con la siguiente sintaxis: int? valor = objectA? .PropertyA? .PropertyB? .PropertyC;
Boris Modylevsky
4

Cuando necesito encadenar llamadas como esa, confío en un método auxiliar que creé, TryGet ():

    public static U TryGet<T, U>(this T obj, Func<T, U> func)
    {
        return obj.TryGet(func, default(U));
    }

    public static U TryGet<T, U>(this T obj, Func<T, U> func, U whenNull)
    {
        return obj == null ? whenNull : func(obj);
    }

En su caso, lo usaría así:

    int value = ObjectA
        .TryGet(p => p.PropertyA)
        .TryGet(p => p.PropertyB)
        .TryGet(p => p.PropertyC, defaultVal);
Emanuel
fuente
No creo que este código funcione. ¿Cuál es el tipo de defaultVal? var p = nueva Persona (); Assert.AreEqual (p.
Keith
El ejemplo que escribí debería leerse como tal: ObjectA.PropertyA.PropertyB.PropertyC. Su código parece estar intentando cargar una propiedad llamada "Apellido" de "Nombre", que no es el uso previsto. Un ejemplo más correcto sería algo como: var código postal = persona.PruebaGet (p => p.Address). TryGet (p => p.Postcode); Por cierto, mi método auxiliar TryGet () es muy similar a una nueva característica en C # 6.0: el operador condicional nulo. Su uso será así: var postcode = person? .Address? .Postcode; msdn.microsoft.com/en-us/magazine/dn802602.aspx
Emanuel
3

Vi algo en el nuevo C # 6.0, esto es usando '?' en lugar de comprobar

por ejemplo en lugar de usar

if (Person != null && Person.Contact!=null && Person.Contact.Address!= null && Person.Contact.Address.City != null)
{ 
  var city = person.contact.address.city;
}

simplemente usa

var city = person?.contact?.address?.city;

Espero que haya ayudado a alguien.


ACTUALIZAR:

Podrías hacer esto ahora

 var city = (Person != null)? 
           ((Person.Contact!=null)? 
              ((Person.Contact.Address!= null)?
                      ((Person.Contact.Address.City!=null)? 
                                 Person.Contact.Address.City : null )
                       :null)
               :null)
            : null;
iYazee6
fuente
1

Podrías hacer esto:

class ObjectAType
{
    public int PropertyC
    {
        get
        {
            if (PropertyA == null)
                return 0;
            if (PropertyA.PropertyB == null)
                return 0;
            return PropertyA.PropertyB.PropertyC;
        }
    }
}



if (ObjectA != null)
{
    int value = ObjectA.PropertyC;
    ...
}

O incluso mejor podría ser esto:

private static int GetPropertyC(ObjectAType objectA)
{
    if (objectA == null)
        return 0;
    if (objectA.PropertyA == null)
        return 0;
    if (objectA.PropertyA.PropertyB == null)
        return 0;
    return objectA.PropertyA.PropertyB.PropertyC;
}


int value = GetPropertyC(ObjectA);
Jeffrey L. Whitledge
fuente
1

puedes usar la siguiente extensión y creo que es realmente buena:

/// <summary>
/// Simplifies null checking
/// </summary>
public static TR Get<TF, TR>(TF t, Func<TF, TR> f)
    where TF : class
{
    return t != null ? f(t) : default(TR);
}

/// <summary>
/// Simplifies null checking
/// </summary>
public static TR Get<T1, T2, TR>(T1 p1, Func<T1, T2> p2, Func<T2, TR> p3)
    where T1 : class
    where T2 : class
{
    return Get(Get(p1, p2), p3);
}

/// <summary>
/// Simplifies null checking
/// </summary>
public static TR Get<T1, T2, T3, TR>(T1 p1, Func<T1, T2> p2, Func<T2, T3> p3, Func<T3, TR> p4)
    where T1 : class
    where T2 : class
    where T3 : class
{
    return Get(Get(Get(p1, p2), p3), p4);
}

Y se usa así:

int value = Nulify.Get(objectA, x=>x.PropertyA, x=>x.PropertyB, x=>x.PropertyC);
Tony
fuente
0

Escribiría su propio método en el tipo de PropertyA (o un método de extensión si no es su tipo) usando un patrón similar al tipo Nullable.

class PropertyAType
{
   public PropertyBType PropertyB {get; set; }

   public PropertyBType GetPropertyBOrDefault()
   {
       return PropertyB != null ? PropertyB : defaultValue;
   }
}
Steve Danner
fuente
Bueno, en ese caso, obviamente PropertyB nunca puede ser nulo.
recursivo
0

Acabo de tropezar con esta publicación.

Hace algún tiempo hice una sugerencia en Visual Studio Connect acerca de agregar un nuevo ???operador.

http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/4104392-add-as-an-recursive-null-reference-check-opera

Esto requeriría algo de trabajo por parte del equipo del marco, pero no es necesario modificar el lenguaje, solo hacer algo de magia del compilador. La idea era que el compilador debería cambiar este código (sintaxis no permitida atm)

string product_name = Order.OrderDetails[0].Product.Name ??? "no product defined";

en este código

Func<string> _get_default = () => "no product defined"; 
string product_name = Order == null 
    ? _get_default.Invoke() 
    : Order.OrderDetails[0] == null 
        ? _get_default.Invoke() 
        : Order.OrderDetails[0].Product == null 
            ? _get_default.Invoke() 
            : Order.OrderDetails[0].Product.Name ?? _get_default.Invoke()

Para comprobar nulo, esto podría verse así

bool isNull = (Order.OrderDetails[0].Product ??? null) == null;
Jürgen Steinblock
fuente
0

Escribí un método que acepta un valor predeterminado, así es como se usa:

var teacher = new Teacher();
return teacher.GetProperty(t => t.Name);
return teacher.GetProperty(t => t.Name, "Default name");

Aquí está el código:

public static class Helper
{
    /// <summary>
    /// Gets a property if the object is not null.
    /// var teacher = new Teacher();
    /// return teacher.GetProperty(t => t.Name);
    /// return teacher.GetProperty(t => t.Name, "Default name");
    /// </summary>
    public static TSecond GetProperty<TFirst, TSecond>(this TFirst item1,
        Func<TFirst, TSecond> getItem2, TSecond defaultValue = default(TSecond))
    {
        if (item1 == null)
        {
            return defaultValue;
        }

        return getItem2(item1);
    }
}
Akira Yamamoto
fuente
1
Esta solución ya se ha proporcionado en otras respuestas (repetidamente). No hay ninguna razón en absoluto para ser publicarla de nuevo .
Servicio
No vi ninguno que acepte un valor predeterminado.
Akira Yamamoto
Cuento otros 6 que utilizan un valor predeterminado definido. Aparentemente no luciste tan duro.
Servicio
0

No es posible.
ObjectA.PropertyA.PropertyBfallará si ObjectAes nulo debido a la eliminación de referencias nulas, lo cual es un error.

if(ObjectA != null && ObjectA.PropertyA... funciona debido a un cortocircuito, es decir ObjectA.PropertyA, nunca se comprobará si ObjectAes así null.

La primera forma que propongas es la mejor y la más clara con intención. En todo caso, podría intentar rediseñar sin tener que depender de tantos nulos.

DanDan
fuente
-1

Este enfoque es bastante sencillo una vez que superas el lambda gobbly-gook:

public static TProperty GetPropertyOrDefault<TObject, TProperty>(this TObject model, Func<TObject, TProperty> valueFunc)  
                                                        where TObject : class
    {
        try
        {
            return valueFunc.Invoke(model);
        }
        catch (NullReferenceException nex)
        {
            return default(TProperty);
        }
    }

Con un uso que podría verse así:

ObjectA objectA = null;

Assert.AreEqual(0,objectA.GetPropertyOrDefault(prop=>prop.ObjectB.ObjectB.ObjectC.ID));

Assert.IsNull(objectA.GetPropertyOrDefault(prop => prop.ObjectB));
BlackjacketMack
fuente
Solo tengo curiosidad por saber por qué alguien votaría en contra 8 años después de que di una respuesta (que fue años antes de que la fusión nula de C # 6 fuera una cosa).
BlackjacketMack
-3
var result = nullableproperty ?? defaultvalue;

El ??(operador de fusión nula) significa que si el primer argumento es null, devuelve el segundo en su lugar.

Aridane Álamo
fuente
2
Esta respuesta no resuelve el problema del OP. ¿Cómo aplicarías la solución con ?? operador a ObjectA.PropertyA.PropertyB cuando todas las partes de la expresión (ObjectA, PropertyA y PropertyB) pueden ser nulas?
Artemix
Es cierto, creo que ni siquiera leí la pregunta. De todos modos, imposible no es nada, simplemente no lo hagas: P static void Main (string [] args) {a ca = new a (); var valor_predeterminado = nuevo a () {b = nuevo objeto ()}; var value = (ca ?? default_value) .b ?? default_value.b; } clase a {objeto público b = nulo; }
Aridane Álamo
(ObjectA ?? DefaultMockedAtNull) .PropertyA! = Null? ObjectA.PropertyA.PropertyB: null
Aridane Álamo