C # manière élégante de vérifier si la propriété d'une propriété est nulle


98

En C #, disons que vous souhaitez extraire une valeur de PropertyC dans cet exemple et ObjectA, PropertyA et PropertyB peuvent tous être null.

ObjectA.PropertyA.PropertyB.PropertyC

Comment puis-je obtenir PropertyC en toute sécurité avec le moins de code possible?

En ce moment, je vérifierais:

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

Ce serait bien de faire quelque chose de plus comme ça (pseudo-code).

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

Peut-être encore plus réduit avec un opérateur de fusion nul.

EDIT À l'origine, j'ai dit que mon deuxième exemple était comme js, mais je l'ai changé en psuedo-code car il a été correctement indiqué que cela ne fonctionnerait pas dans js.


je ne vois pas comment fonctionne votre exemple js. vous devriez obtenir une erreur "objet attendu" chaque fois que ObjectAou PropertyAsont nuls.
lincolnk

Réponses:


120

En C # 6, vous pouvez utiliser l' opérateur conditionnel Null . Le test original sera donc:

int? value = objectA?.PropertyA?.PropertyB?.PropertyC;

2
pouvez-vous expliquer ce que cela fait? Qu'est-ce qui est valueégal à si PropertyCest nul? ou si PropertyBest nul? et si Object Aest nul?
Kolob Canyon

1
Si AUCUNE de ces propriétés est nulle, l'instruction entière est renvoyée sous la forme null. Cela commence de gauche à droite. Sans le sucre syntaxique, cela équivaut à une série d'instructions if où if(propertyX == null) {value = null} else if (propertyY == null){ value = null} else if......la dernière expression éventuelle étantif(propertyZ != null) { value = propertyZ }
DetectivePikachu

27

Méthode d'extension courte:

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

En utilisant

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

Cette méthode d'extension simple et bien plus encore, vous pouvez trouver sur http://devtalk.net/csharp/chained-null-checks-and-the-maybe-monad/

ÉDITER:

Après l'avoir utilisé pendant un moment, je pense que le nom correct de cette méthode devrait être IfNotNull () au lieu de l'original With ().


15

Pouvez-vous ajouter une méthode à votre classe? Sinon, avez-vous pensé à utiliser des méthodes d'extension? Vous pouvez créer une méthode d'extension pour votre type d'objet appelé GetPropC().

Exemple:

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

Usage:

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

En passant, cela suppose que vous utilisez .NET 3 ou supérieur.


11

La façon dont vous le faites est correcte.

Vous pouvez utiliser une astuce comme celle décrite ici , en utilisant des expressions Linq:

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

Mais c'est beaucoup plus lent que de vérifier manuellement chaque propriété ...


10

Refactor pour observer la loi de Déméter


Je ne considère pas qu'un graphique d'objet avec seulement trois niveaux de profondeur ait besoin d'être refactorisé lorsque vous ne lisez que des propriétés. Je serais d'accord si l'OP voulait appeler une méthode sur un objet référencé via PropertyC mais pas quand c'est une propriété qui n'a besoin que de vérifier la valeur null avant de lire. Dans cet exemple, cela pourrait être aussi simple que Customer.Address.Country où Country pourrait être un type de référence tel que KeyValuePair. Comment refactoriser cela pour éviter la nécessité des vérifications de référence nulles?
Darren Lewis

L'exemple OP est en fait 4 profond. Ma suggestion n'est pas de supprimer les vérifications de référence nulles mais de les localiser dans les objets les plus susceptibles de les gérer correctement. Comme la plupart des «règles empiriques», il y a des exceptions, mais je ne suis pas convaincu que ce soit une. Pouvons-nous accepter d'être en désaccord?
rtalbot

4
Je suis d'accord avec @rtalbot (cependant, pour être honnête, @Daz Lewis propose un exemple à 4 niveaux, puisque le dernier élément est un KeyValuePair). Si quelque chose ne va pas avec un objet Client, alors je ne vois pas quelle activité il a en regardant à travers l'hérarchie d'objet Address. Supposons que vous décidiez plus tard qu'un KeyValuePair n'était pas une si bonne idée pour la propriété Country. Dans ce cas, le code de tout le monde doit changer. Ce n'est pas une bonne conception.
Jeffrey L Whitledge

10

Mise à jour 2014: C # 6 a un nouvel opérateur ?.appelé `` navigation sûre '' ou `` propagation nulle ''

parent?.child

Lisez http://blogs.msdn.com/b/jerrynixon/archive/2014/02/26/at-last-c-is-getting-sometimes-called-the-safe-navigation-operator.aspx pour plus de détails

Cela a longtemps été une demande extrêmement populaire https://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/3990187-add-operator-to-c-?tracking_code=594c10a522f8e9bc987ee4a5e2c0b38d


8

Vous recherchez évidemment la Monade Nullable :

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

devient

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

Cela renvoie null, si l'une des propriétés Nullable est NULL; sinon, la valeur de Value.

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

Méthodes d'extension 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);
    }
}

Pourquoi la méthode d'extension est-elle ici? Il n'est pas utilisé.
Mladen Mihajlovic

1
@MladenMihajlovic: la SelectManyméthode d'extension est utilisée par la from ... in ... from ... in ...syntaxe.
dtb le

5

En supposant que vous ayez des valeurs vides de types, une approche serait la suivante:

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

Je suis un grand fan de C # mais une très bonne chose dans le nouveau Java (1.7?) Est le.? opérateur:

 var x = objectA.?PropertyOfB.?PropertyOfC.?PropertyOfString;

1
Est-ce que ça va vraiment être dans Java 1.7? Cela a été demandé en C # pendant longtemps, mais je doute que cela se produise un jour ...
Thomas Levesque

Malheureusement, je n'ai pas de valeurs vides. Cette syntaxe Java semble douce cependant! Je vais voter pour ça, juste parce que je veux cette syntaxe!
Jon Kragh

3
Thomas: La dernière fois que j'ai vérifié tech.puredanger.com/java7, cela impliquait que Java l'obtiendrait. Mais maintenant, quand je revérifie, il dit: Manipulation sûre nulle: NON. Je révoque donc ma déclaration et la remplace par une nouvelle: elle a été proposée pour Java 1.7 mais ne l'a pas été.
Juste un autre métaprogrammeur

Une approche supplémentaire est celle utilisée par monad.net
Juste un autre métaprogrammeur

1
On dirait que le?. l'opérateur est dans Visual Studio 2015 https://msdn.microsoft.com/en-us/library/dn986595.aspx
Edward

4

Ce code est "la moindre quantité de code", mais pas la meilleure pratique:

try
{
    return ObjectA.PropertyA.PropertyB.PropertyC;
}
catch(NullReferenceException)
{
     return null;
}

1
J'ai vu beaucoup de code comme celui-ci et sans tenir compte de la perte de performances, le plus gros problème est qu'il complique le débogage car la véritable exception se noie dans des millions d'exceptions de référence nulles inutiles.
Juste un autre métaprogrammeur

Parfois, c'est amusant de lire ma propre réponse après 3 ans. Je pense que je répondrais différemment aujourd'hui. Je dirais que le code viole la loi de Demeter et je recommanderais de le refactoriser pour qu'il ne le fasse pas.
Boris Modylevsky

1
À partir d'aujourd'hui, 7 ans après la réponse originale, je rejoindrais @Phillip Ngan et utiliserais C # 6 avec la syntaxe suivante: int? valeur = objetA? .PropertyA? .PropertyB? .PropertyC;
Boris Modylevsky

4

Quand j'ai besoin d'enchaîner des appels comme ça, je me fie à une méthode d'aide que j'ai créée, 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);
    }

Dans votre cas, vous l'utiliseriez comme ceci:

    int value = ObjectA
        .TryGet(p => p.PropertyA)
        .TryGet(p => p.PropertyB)
        .TryGet(p => p.PropertyC, defaultVal);

Je ne pense pas que ce code fonctionne. Quel est le type de defaultVal? var p = nouvelle personne (); Assert.AreEqual (p.TryGet (x => x.FirstName) .TryGet (x => x.LastName) .TryGet (x => x.NickName, "foo"), "foo");
Keith

L'exemple que j'ai écrit doit être lu comme tel: ObjectA.PropertyA.PropertyB.PropertyC. Votre code semble essayer de charger une propriété appelée "LastName" à partir de "FirstName", ce qui n'est pas l'usage prévu. Un exemple plus correct serait quelque chose comme: var postcode = person.TryGet (p => p.Address) .TryGet (p => p.Postcode); À propos, ma méthode d'assistance TryGet () est très similaire à une nouvelle fonctionnalité de C # 6.0 - l'opérateur conditionnel nul. Son utilisation sera la suivante: var postcode = person? .Address? .Postcode; msdn.microsoft.com/en-us/magazine/dn802602.aspx
Emanuel

3

J'ai vu quelque chose dans le nouveau C # 6.0, c'est en utilisant '?' au lieu de vérifier

par exemple au lieu d'utiliser

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

vous utilisez simplement

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

J'espère que cela a aidé quelqu'un.


METTRE À JOUR:

Tu pourrais faire comme ça maintenant

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

1

Vous pouvez faire ceci:

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;
    ...
}

Ou encore mieux pourrait être ceci:

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

1

vous pouvez utiliser l'extension suivante et je pense que c'est vraiment bien:

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

Et il est utilisé comme ceci:

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

0

J'écrirais votre propre méthode dans le type de PropertyA (ou une méthode d'extension si ce n'est pas votre type) en utilisant le modèle similaire au type Nullable.

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

   public PropertyBType GetPropertyBOrDefault()
   {
       return PropertyB != null ? PropertyB : defaultValue;
   }
}

Eh bien, dans ce cas, évidemment PropertyB ne peut jamais être nul.
récursif

0

Je viens de trébucher sur ce post.

Il y a quelque temps, j'ai fait une suggestion sur Visual Studio Connect concernant l'ajout d'un nouvel ???opérateur.

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

Cela nécessiterait un peu de travail de la part de l'équipe du framework mais pas besoin de modifier le langage, mais juste de faire de la magie du compilateur. L'idée était que le compilateur devrait changer ce code (syntaxe non autorisée atm)

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

dans ce code

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

Pour une vérification nulle, cela pourrait ressembler à

bool isNull = (Order.OrderDetails[0].Product ??? null) == null;

0

J'ai écrit une méthode qui accepte une valeur par défaut, voici comment l'utiliser:

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

Voici le code:

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

1
Cette solution a déjà été fournie dans d'autres réponses (à plusieurs reprises). Il n'y a aucune raison de le publier à nouveau .
Servy

Je n'en ai vu aucun qui accepte une valeur par défaut.
Akira Yamamoto

Je compte 6 autres qui utilisent une valeur par défaut définie. Apparemment, vous n'avez pas l'air si difficile.
Servy

0

Ce n'est pas possible.
ObjectA.PropertyA.PropertyBéchouera si ObjectAest nul en raison d'un déréférencement nul, ce qui est une erreur.

if(ObjectA != null && ObjectA.PropertyA... fonctionne en raison d'un court-circuit, c'est-à-dire ObjectA.PropertyAne sera jamais vérifié si ObjectAc'est le cas null.

La première façon que vous proposez est la meilleure et la plus claire avec l'intention. Si quoi que ce soit, vous pourriez essayer de repenser sans avoir à vous fier à autant de valeurs nulles.


-1

Cette approche est assez simple une fois que vous avez surmonté le 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);
        }
    }

Avec une utilisation qui pourrait ressembler à:

ObjectA objectA = null;

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

Assert.IsNull(objectA.GetPropertyOrDefault(prop => prop.ObjectB));

Je suis juste curieux de savoir pourquoi quelqu'un refuserait de voter 8 ans après avoir fourni une réponse (ce qui était des années avant que la fusion nulle de C # 6 ne soit une chose).
BlackjacketMack

-3
var result = nullableproperty ?? defaultvalue;

L' ??opérateur (null-coalescing) signifie que si le premier argument est null, renvoie le second à la place.


2
Cette réponse ne résout pas le problème du PO. Comment appliquez-vous la solution avec ?? opérateur à ObjectA.PropertyA.PropertyB lorsque toutes les parties de l'expression (ObjectA, PropertyA et PropertyB) peuvent être nulles?
Artemix

C'est vrai, je pense que je n'ai même pas lu la question du tout. Quoi qu'il en soit, impossible n'est rien mais ne le faites pas: P static void Main (string [] args) {a ca = new a (); var default_value = new a () {b = new object ()}; var value = (ca ?? valeur_par défaut) .b ?? default_value.b; } classe a {objet public b = null; }
Aridane Álamo

(ObjectA ?? DefaultMockedAtNull) .PropertyA! = Null? ObjectA.PropertyA.PropertyB: null
Aridane Álamo
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.