Vérification profonde des null, y a-t-il un meilleur moyen?


130

Remarque: Cette question a été posée avant l'introduction de l' .?opérateur dans C # 6 / Visual Studio 2015 .

Nous avons tous été là, nous avons une propriété profonde comme cake.frosting.berries.loader dont nous devons vérifier si elle est nulle, donc il n'y a pas d'exception. La façon de faire est d'utiliser une instruction if de court-circuit

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

Ce n'est pas vraiment élégant, et il devrait peut-être y avoir un moyen plus simple de vérifier toute la chaîne et de voir si elle se heurte à une variable / propriété nulle.

Est-il possible d'utiliser une méthode d'extension ou serait-ce une fonctionnalité de langage, ou est-ce juste une mauvaise idée?


3
J'ai souhaité cela assez souvent - mais toutes les idées que j'ai avancées étaient pires que le problème réel.
peterchen

Merci pour toutes les réponses et intéressant de voir que d'autres personnes ont eu les mêmes pensées. Je me suis demandé comment j'aimerais voir cela résolu moi-même et bien que les solutions d'Eric soient gentilles, je pense que je voudrais simplement écrire quelque chose comme ça si (IsNull (abc)), ou si (IsNotNull (abc)) mais peut-être c'est juste à mon goût :)
Homde

Lorsque vous instanciez le glaçage, il a une propriété de baies, alors à ce stade de votre constructeur, pouvez-vous simplement dire au glaçage que chaque fois qu'il est installé pour créer des baies vides (non nulles)? et chaque fois que les baies sont modifiées, le glaçage fait le contrôle de la valeur ????
Doug Chamberlain

Un peu vaguement liées, certaines des techniques ici que j'ai trouvées préférables pour le problème des "nuls profonds" que j'essayais de contourner. stackoverflow.com/questions/818642/…
AaronLS

Réponses:


223

Nous avons envisagé d'ajouter une nouvelle opération "?". à la langue qui a la sémantique souhaitée. (Et il a été ajouté maintenant; voir ci-dessous.) Autrement dit, diriez-vous

cake?.frosting?.berries?.loader

et le compilateur générerait toutes les vérifications de court-circuit pour vous.

Cela n'a pas fait la barre pour C # 4. Peut-être pour une future version hypothétique du langage.

Mise à jour (2014): l' ?.opérateur est désormais prévu pour la prochaine version du compilateur Roslyn. Notez qu'il y a encore un débat sur l'analyse syntaxique et sémantique exacte de l'opérateur.

Mise à jour (juillet 2015): Visual Studio 2015 a été publié et est livré avec un compilateur C # qui prend en charge les opérateurs conditionnels null ?.et?[] .


10
Sans le point, il devient syntaxiquement ambigu avec l'opérateur conditionnel (A? B: C). Nous essayons d'éviter les constructions lexicales qui nous obligent à "regarder en avant" arbitrairement loin dans le flux de jetons. (Bien que, malheureusement, il existe déjà de telles constructions en C #; nous préférerions ne pas en ajouter plus.)
Eric Lippert

33
@Ian: ce problème est extrêmement courant. C'est l'une des demandes les plus fréquentes que nous recevons.
Eric Lippert

7
@Ian: Je préfère également utiliser le modèle d'objet nul lorsque cela est possible, mais la plupart des gens n'ont pas le luxe de travailler avec des modèles d'objet qu'ils ont eux-mêmes conçus. De nombreux modèles d'objets existants utilisent des valeurs nulles et c'est donc le monde avec lequel nous devons vivre.
Eric Lippert

12
@John: Nous recevons cette demande de fonctionnalité presque entièrement de nos programmeurs les plus expérimentés. Les MVP le demandent tout le temps . Mais je comprends que les opinions varient; si vous souhaitez donner une suggestion de conception de langage constructive en plus de vos critiques, je suis heureux de l'examiner.
Eric Lippert

28
@lazyberezovsky: Je n'ai jamais compris la soi-disant «loi» de Déméter; tout d'abord, il semble être appelé plus précisément "La suggestion de Déméter". Et deuxièmement, le résultat de prendre "un seul accès membre" à sa conclusion logique est "les objets de Dieu" où chaque objet doit tout faire pour chaque client, plutôt que de pouvoir distribuer des objets qui savent comment faire ce que le client veut. Je préfère l'exact opposé de la loi de déméter: chaque objet résout bien un petit nombre de problèmes, et l'une de ces solutions peut être "voici un autre objet qui résout mieux votre problème"
Eric Lippert

27

Je me suis inspiré de cette question pour essayer de découvrir comment ce type de vérification profonde des null peut être effectué avec une syntaxe plus facile / plus jolie en utilisant des arbres d'expression. Bien que je sois d'accord avec les réponses indiquant que cela pourrait être une mauvaise conception si vous avez souvent besoin d'accéder à des instances au plus profond de la hiérarchie, je pense également que dans certains cas, comme la présentation des données, cela peut être très utile.

J'ai donc créé une méthode d'extension, qui vous permettra d'écrire:

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

Cela renverra les baies si aucune partie de l'expression n'est nulle. Si nul est rencontré, nul est renvoyé. Il y a cependant quelques mises en garde, dans la version actuelle, il ne fonctionnera qu'avec un accès membre simple, et il ne fonctionne que sur .NET Framework 4, car il utilise la méthode MemberExpression.Update, qui est nouvelle dans la v4. Voici le code de la méthode d'extension 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;            
        }
    }
}

Cela fonctionne en examinant l'arborescence des expressions représentant votre expression et en évaluant les parties l'une après l'autre; à chaque fois en vérifiant que le résultat n'est pas nul.

Je suis sûr que cela pourrait être étendu afin que d'autres expressions que MemberExpression soient prises en charge. Considérez cela comme du code de preuve de concept, et gardez à l'esprit qu'il y aura une pénalité de performance en l'utilisant (ce qui n'aura probablement pas d'importance dans de nombreux cas, mais ne l'utilisez pas dans une boucle serrée :-))


Je suis impressionné par vos compétences lambda :) la syntaxe semble cependant être un peu plus complexe qu'on ne le souhaiterait, au moins pour le scénario de l'instruction
if

Cool, mais il fonctionne comme 100 fois plus de code qu'un if .. &&. Cela ne vaut la peine que s'il se compile toujours en un si .. &&.
Monstieur

1
Ah et puis j'ai vu DynamicInvokelà-bas. J'évite religieusement ça :)
nawfal

24

J'ai trouvé cette extension très utile pour les scénarios d'imbrication profonde.

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

C'est une idée que j'ai dérivée de l'opérateur de fusion nul en C # et T-SQL. La bonne chose est que le type de retour est toujours le type de retour de la propriété interne.

De cette façon, vous pouvez faire ceci:

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

... ou une légère variation de ce qui précède:

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

Ce n'est pas la meilleure syntaxe que je connaisse, mais cela fonctionne.


Pourquoi "Coal", qui semble extrêmement effrayant. ;) Cependant, votre échantillon échouerait si le glaçage était nul. Cela aurait dû ressembler à ceci: var baies = cake.NullSafe (c => c.Frosting.NullSafe (f => f.Berries));
Robert Giesecke

Oh, mais vous laissez entendre que le deuxième argument n'est pas un appel à Coal, ce qu'il doit bien sûr être. C'est juste une modification pratique. Le sélecteur (x => x.berries) est passé à un appel Coal à l'intérieur de la méthode Coal qui prend deux arguments.
John Leidegren

Le nom coalescing ou coalesce vient de T-SQL, c'est là que j'ai eu l'idée. IfNotNull implique que quelque chose se produit s'il n'est pas nul, mais ce que c'est, n'est pas expliqué par l'appel de méthode IfNotNull. Le charbon est en effet un nom étrange, mais c'est en fait une méthode étrange qui mérite d'être notée.
John Leidegren

Le meilleur nom pour cela serait quelque chose comme "ReturnIfNotNull" ou "ReturnOrDefault"
John Leidegren

@flq +1 ... dans notre projet, il s'appelle aussi IfNotNull :)
Marc Sigrist

16

En plus de violer la loi de Déméter, comme Mehrdad Afshari l'a déjà souligné, il me semble que vous avez besoin d'une "vérification profonde des nullités" pour la logique de décision.

C'est le plus souvent le cas lorsque vous souhaitez remplacer des objets vides par des valeurs par défaut. Dans ce cas, vous devez envisager d'implémenter le modèle d'objet nul . Il agit comme un substitut pour un objet réel, fournissant des valeurs par défaut et des méthodes «sans action».


non, objective-c permet d'envoyer des messages à des objets nuls et renvoie la valeur par défaut appropriée si nécessaire. Aucun problème là-bas.
Johannes Rudolph

2
Ouais. C'est le but. En gros, vous émulerez le comportement ObjC avec Null Object Pattern.
Mehrdad Afshari

10

Mise à jour: à partir de Visual Studio 2015, le compilateur C # (version 6 du langage) reconnaît désormais l' ?.opérateur, ce qui simplifie la «vérification approfondie des null». Voir cette réponse pour plus de détails.

En plus de reconcevoir votre code, comme le suggère cette réponse supprimée , une autre option (bien que terrible) serait d'utiliser un try…catchbloc pour voir si une NullReferenceExceptionerreur se produit à un moment donné pendant cette recherche approfondie de propriété.

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

Personnellement, je ne ferais pas cela pour les raisons suivantes:

  • Ça n'a pas l'air sympa.
  • Il utilise la gestion des exceptions, qui doit cibler des situations exceptionnelles et non quelque chose dont vous vous attendez à ce qu'il se produise souvent au cours du fonctionnement normal.
  • NullReferenceExceptions ne devrait probablement jamais être pris explicitement. (Voir cette question .)

Est-il donc possible d'utiliser une méthode d'extension ou serait-ce une fonctionnalité de langage, [...]

Cela devrait presque certainement être une fonctionnalité de langage (qui est disponible en C # 6 sous la forme des opérateurs .?et ?[]), à moins que C # n'ait déjà une évaluation paresseuse plus sophistiquée, ou à moins que vous ne souhaitiez utiliser la réflexion (qui n'est probablement pas non plus une bonne idée pour des raisons de performances et de sécurité de type).

Puisqu'il n'y a aucun moyen de passer simplement cake.frosting.berries.loaderà une fonction (elle serait évaluée et lèverait une exception de référence nulle), vous devriez implémenter une méthode de recherche générale de la manière suivante: Il prend un objets et les noms de propriétés à Chercher:

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

(Remarque: code modifié.)

Vous voyez rapidement plusieurs problèmes avec une telle approche. Premièrement, vous n'obtenez aucune sécurité de type et possibilité d'encadrer les valeurs de propriété d'un type simple. Deuxièmement, vous pouvez soit revenir nullsi quelque chose ne va pas, et vous devrez vérifier cela dans votre fonction d'appel, ou vous lever une exception, et vous êtes de retour à votre point de départ. Troisièmement, cela pourrait être lent. Quatrièmement, cela a l'air plus laid que ce avec quoi vous avez commencé.

[...], ou est-ce juste une mauvaise idée?

Je resterais soit avec:

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

ou allez avec la réponse ci-dessus par Mehrdad Afshari.


PS: Quand j'ai écrit cette réponse, je n'ai évidemment pas considéré les arbres d'expression pour les fonctions lambda; voir par exemple la réponse de @driis pour une solution dans ce sens. Il est également basé sur une sorte de réflexion et peut donc ne pas fonctionner aussi bien qu'une solution plus simple ( if (… != null & … != null) …), mais il peut être jugé plus agréable d'un point de vue syntaxique.


2
Je ne sais pas pourquoi cela a été refusé, j'ai fait un vote positif pour l'équilibre: La réponse est correcte et apporte un nouvel aspect (et mentionne explicitement les inconvénients de cette solution ...)
MartinStettner

où est "la réponse ci-dessus de Mehrdad Afshari"?
Marson Mao

1
@MarsonMao: Cette réponse a été supprimée entre-temps. (Vous pouvez toujours le lire si votre rang SO est suffisamment élevé.) Merci d'avoir signalé mon erreur: je devrais faire référence à d'autres réponses en utilisant un lien hypertexte, sans utiliser des mots comme "voir ci-dessus" / "voir ci-dessous" (puisque les réponses n'apparaissent pas dans un ordre fixe). J'ai mis à jour ma réponse.
stakx - ne contribue plus le

5

Bien que la réponse de Driis soit intéressante, je pense que c'est un peu trop cher en termes de performances. Plutôt que de compiler de nombreux délégués, je préférerais compiler un lambda par chemin de propriété, le mettre en cache, puis le réinvoquer de nombreux types.

NullCoalesce ci-dessous fait exactement cela, il renvoie une nouvelle expression lambda avec des vérifications nulles et un retour par défaut (TResult) au cas où un chemin serait nul.

Exemple:

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

Renverra une expression

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

Code:

    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

Moi aussi, j'ai souvent souhaité une syntaxe plus simple! Il devient particulièrement laid quand vous avez méthode de retour des valeurs qui pourraient être nulle, car alors vous avez besoin des variables supplémentaires (par exemple: cake.frosting.flavors.FirstOrDefault().loader)

Cependant, voici une alternative assez décente que j'utilise: créer une méthode d'assistance Null-Safe-Chain. Je me rends compte que c'est assez similaire à la réponse de @ John ci-dessus (avec la Coalméthode d'extension) mais je trouve que c'est plus simple et moins tapant. Voici à quoi cela ressemble:

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

Voici l'implémentation:

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

J'ai également créé plusieurs surcharges (avec 2 à 6 paramètres), ainsi que des surcharges qui permettent à la chaîne de se terminer par un type valeur ou par défaut. Cela fonctionne vraiment bien pour moi!



1

Comme le suggère John Leidegren de réponse , une approche de travail autour est d'utiliser des méthodes d'extension et les délégués. Leur utilisation pourrait ressembler à ceci:

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

L'implémentation est compliquée car vous devez la faire fonctionner pour les types valeur, les types référence et les types valeur Nullable. Vous pouvez trouver une implémentation complète dans la réponse de Timwi à Quelle est la bonne façon de vérifier les valeurs nulles? .


1

Ou vous pouvez utiliser la réflexion :)

Fonction de réflexion:

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

Usage:

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

My Case (retourne DBNull.Value au lieu de null dans la fonction de réflexion):

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

1

Essayez ce code:

    /// <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

J'ai posté ceci hier soir, puis un ami m'a signalé cette question. J'espère que ça aide. Vous pouvez alors faire quelque chose comme ceci:

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

Lisez le blog complet ici .

Le même ami a également suggéré que vous regardiez ceci .


3
Pourquoi s'embêter avec un Expressionsi vous allez juste compiler et attraper? Utilisez simplement un fichier Func<T>.
Scott Rippey

0

J'ai légèrement modifié le code à partir d' ici pour le faire fonctionner pour la question posée:

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

Et oui, ce n'est probablement pas la solution optimale en raison des implications de performance try / catch, mais cela fonctionne:>

Usage:

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

0

Là où vous devez y parvenir, procédez comme suit:

Usage

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

ou

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

Implémentation de classe d'assistance

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

J'aime l'approche d'Objective-C:

"Le langage Objective-C adopte une autre approche à ce problème et n'appelle pas de méthodes sur nil mais renvoie à la place nil pour toutes ces invocations."

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

1
ce que fait un autre langage (et votre opinion sur celui-ci) est presque totalement hors de propos pour le faire fonctionner en C #. Cela n'aide personne à résoudre son problème C #
ADyson
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.