web-dev-qa-db-fra.com

Vérification nulle profonde, existe-t-il une meilleure façon?

Remarque: Cette question a été posée avant l'introduction de le .? 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 court-circuitant

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

Ce n'est pas exactement élégant, et il devrait peut-être y avoir un moyen plus simple de vérifier la chaîne entière 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 langue, ou est-ce juste une mauvaise idée?

128
Homde

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

cake?.frosting?.berries?.loader

et le compilateur générerait pour vous tous les contrôles de court-circuit.

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

Mise à jour (2014): Le ?. L'opérateur est maintenant prév 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 à condition nulle ?. et ?[] .

219
Eric Lippert

Je me suis inspiré de cette question pour essayer de découvrir comment ce type de vérification profonde des valeurs nulles 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 profondes de la hiérarchie, je pense également que dans certains cas, tels que 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 null est rencontré, null est retourné. Il y a cependant quelques mises en garde, dans la version actuelle, cela ne fonctionnera qu'avec un accès simple aux membres, et cela ne fonctionnera 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;            
        }
    }
}

Il fonctionne en examinant l'arbre d'expression représentant votre expression et en évaluant les parties l'une après l'autre; vérifiant à chaque fois 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 un 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 en boucle serrée :-))

27
driis

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 coalescence nulle 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 le faire:

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.

24
John Leidegren

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 nulle profonde" 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 devriez envisager d'implémenter le Null Object Pattern . Il agit comme un remplaçant pour un objet réel, fournissant des valeurs par défaut et des méthodes "sans action".

16
Johannes Rudolph

Mise à jour: À partir de Visual Studio 2015, le compilateur C # (version linguistique 6) reconnaît désormais l'opérateur ?., Ce qui rend "deep null vérifier "une brise. Voir cette réponse pour plus de détails.

En plus de reconcevoir votre code, comme cette réponse supprimée suggérée, une autre option (bien que terrible) serait d'utiliser un bloc try…catch Pour voir si un NullReferenceException se produit au cours de cette recherche de propriété profonde.

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

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

  • Ça n'a pas l'air sympa.
  • Il utilise la gestion des exceptions, qui devrait cibler des situations exceptionnelles et non quelque chose que vous attendez souvent pendant le fonctionnement normal.
  • NullReferenceExceptions ne devrait probablement jamais être intercepté explicitement. (Voir cette question .)

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

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à eu une évaluation paresseuse plus sophistiquée, ou à moins que vous veulent utiliser la réflexion (ce qui n'est probablement pas une bonne idée pour des raisons de performances et de sécurité de type).

Puisqu'il n'y a aucun moyen de simplement passer 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: Elle prend un objet et les noms des propriétés à rechercher:

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. Tout d'abord, vous n'obtenez aucune sécurité de type et aucune boxe possible des valeurs de propriété d'un type simple. Deuxièmement, vous pouvez soit retourner null si 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 à l'endroit où vous avez commencé. Troisièmement, cela pourrait être lent. Quatrièmement, cela semble plus laid que ce que vous aviez 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 de Mehrdad Afshari.


P.S.: À l'époque où j'ai écrit cette réponse, je ne pensais évidemment pas aux arbres d'expression pour les fonctions lambda; voir par exemple Réponse de @driis pour une solution dans cette direction. 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.

10
stakx

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ère 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 retourne 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)

Renvoie 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;
    }
5
Double Down

Une option consiste à utiliser le Null Object Patten, donc au lieu d'avoir null lorsque vous n'avez pas de gâteau, vous avez un NullCake qui renvoie un NullFosting etc. Désolé, je ne suis pas très bon pour expliquer cela mais d'autres personnes le sont, voir

4
Ian Ringrose

Moi aussi, j'ai souvent souhaité une syntaxe plus simple! Cela devient particulièrement laid lorsque vous avez des valeurs de retour de méthode qui peuvent être nulles, car vous avez alors besoin de 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 cela est assez similaire à la réponse de @ John ci-dessus (avec la méthode d'extension Coal) mais je trouve que c'est plus simple et moins de frappe. Voici à quoi ça 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 de valeur ou par défaut. Cela fonctionne vraiment bien pour moi!

3
Scott Rippey

Comme suggéré dans John Leidegrenréponse , une approche pour contourner ce problème consiste à utiliser des méthodes d'extension et des 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 de valeur, les types de référence et les types de valeur nullable. Vous pouvez trouver une implémentation complète dans Timwi 's answer to Quelle est la bonne façon de vérifier les valeurs nulles? .

1
Sam

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
heybeliman

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

Il y a projet codeplex peut-être qui implémente Maybe ou IfNotNull en utilisant des lambdas pour les expressions profondes en C #

Exemple d'utilisation:

int? CityId= employee.Maybe(e=>e.Person.Address.City);

Le lien a été suggéré dans une question similaire Comment vérifier les valeurs nulles dans une expression lambda profonde?

1

J'ai posté cela hier soir, puis un ami m'a signalé cette question. J'espère que cela 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 article de blog complet ici .

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

0
Tyler Jensen

Lorsque vous avez besoin pour 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);
        }
    }
}
0
kernowcode

J'ai légèrement modifié le code de 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 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
kaptan