30 votes

Façon élégante de lire une propriété enfant d'un objet

Disons que vous essayez de lire cette propriété

var town = Staff.HomeAddress.Postcode.Town;

Quelque part le long de la chaîne, il pourrait y avoir une nullité. Quelle serait la meilleure façon de lire la ville ?

J'ai expérimenté quelques méthodes d'extension...

public static T2 IfNotNull<T1, T2>(this T1 t, Func<T1, T2> fn) where T1 : class
{
    return t != null ? fn(t) : default(T2);
}

var town = staff.HomeAddress.IfNotNull(x => x.Postcode.IfNotNull(y=> y.Town));

o

public static T2 TryGet<T1, T2>(this T1 t, Func<T1, T2> fn) where T1 : class
{
if (t != null)
{
    try
    {
        return fn(t);
    }
    catch{ }
}
return default(T2);
}

var town = staff.TryGet(x=> x.HomeAddress.Postcode.Town);

Évidemment, il ne s'agit que d'abstraire la logique et de rendre le code (un peu) plus lisible.

Mais existe-t-il un moyen meilleur/plus efficace ?

EDITAR:

Dans mon cas particulier, les objets sont renvoyés par un service WCF et je n'ai aucun contrôle sur l'architecture de ces objets.

EDIT 2 :

Il y a aussi cette méthode :

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

    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 as for the pseudocode
    ///     var r = Pharmacy?.GuildMembership?.State?.Name
    /// can be written as
    ///     var r = Nullify( Pharmacy, p => p.GuildMembership, g => g.State, s => s.Name );
    /// </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);
    }
}

de cet article http://qualityofdata.com/2011/01/27/nullsafe-dereference-operator-in-c/

19voto

Oded Points 271275

Le meilleur moyen serait d'éviter de violer le loi de Déméter .

var town = Staff.GetTown();

Et dans Staff :

string GetTown()
{
    HomeAddress.GetTown();
}

Et dans HomeAddress :

string GetTown()
{
    PostCode.GetTown();
}

Et dans PostCode :

string GetTown()
{
    Town.GetTownName();
}

Mise à jour :

Puisque vous n'avez pas de contrôle sur cela, vous pouvez utiliser évaluation des courts-circuits :

if(Staff != null 
   && Staff.HomeAddress != null
   && Staff.HomeAddress.PostCode != null
   && Staff.HomeAddress.PostCode.Town != null)
{
    var town = Staff.HomeAddress.Postcode.Town;
}

10voto

Ani Points 59747

Je suis d'accord avec Oded que cela viole la loi de Déméter.

J'ai été intrigué par votre question, alors j'ai écrit une méthode d'extension "Null-Safe Evaluate" du pauvre avec des arbres d'expression, juste pour le plaisir. Cela devrait vous donner une syntaxe compacte pour exprimer la sémantique désirée.

S'il vous plaît, n'utilisez pas ceci dans le code de production.

Utilisation :

var town = Staff.NullSafeEvaluate(s => s.HomeAddress.Postcode.Town);

Cette évaluation se fera successivement :

Staff
Staff.HomeAddress
Staff.HomeAddress.Postcode
Staff.HomeAddress.Postcode.Town

(Mise en cache et réutilisation des valeurs des expressions intermédiaires pour produire la suivante)

S'il rencontre un null il renvoie la valeur par défaut du type de la référence Town . Sinon, il renvoie la valeur de l'expression complète.

(Non testé en profondeur, peut être amélioré en termes de performance et ne supporte pas les méthodes d'instance. POC seulement).

public static TOutput NullSafeEvaluate<TInput, TOutput>
        (this TInput input, Expression<Func<TInput, TOutput>> selector)
{
    if (selector == null)
        throw new ArgumentNullException("selector");

    if (input == null)
        return default(TOutput);

    return EvaluateIterativelyOrDefault<TOutput>
            (input, GetSubExpressions(selector));
}

private static T EvaluateIterativelyOrDefault<T>
        (object rootObject, IEnumerable<MemberExpression> expressions)
{
    object currentObject = rootObject;

    foreach (var sourceMemEx in expressions)
    {
        // Produce next "nested" member-expression. 
        // Reuse the value of the last expression rather than 
        // re-evaluating from scratch.
        var currentEx = Expression.MakeMemberAccess
                      (Expression.Constant(currentObject), sourceMemEx.Member);

        // Evaluate expression.
        var method = Expression.Lambda(currentEx).Compile();
        currentObject = method.DynamicInvoke();

        // Expression evaluates to null, return default.
        if (currentObject == null)
            return default(T);
    }

    // All ok.
    return (T)currentObject;
}

private static IEnumerable<MemberExpression> GetSubExpressions<TInput, TOutput>
        (Expression<Func<TInput, TOutput>> selector)
{
    var stack = new Stack<MemberExpression>();

    var parameter = selector.Parameters.Single();
    var currentSubEx = selector.Body;

    // Iterate through the nested expressions, "reversing" their order.
    // Stop when we reach the "root", which must be the sole parameter.
    while (currentSubEx != parameter)
    {
        var memEx = currentSubEx as MemberExpression;

        if (memEx != null)
        {
            // Valid member-expression, push. 
            stack.Push(memEx);
            currentSubEx = memEx.Expression;
        }

        // It isn't a member-expression, it must be the parameter.
        else if (currentSubEx != parameter)
        {

            // No, it isn't. Throw, don't support arbitrary expressions.
            throw new ArgumentException
                        ("Expression not of the expected form.", "selector");
        }
    }

    return stack;
}

9voto

Evgeny Gavrin Points 3180
    var town = "DefaultCity";
    if (Staff != null &&
        Staff.HomeAddress != null &&
        Staff.HomeAddress.Postcode != null &&
        Staff.HomeAddress.Postcode.Town != null)
    {
        town = Staff.HomeAddress.Postcode.Town;
    }

1voto

Teoman Soygul Points 17544

Conformément à l'encapsulation, il est toujours du devoir d'une classe d'effectuer une validation appropriée (c'est-à-dire des contrôles de nullité) pour ses champs (et ses propriétés) avant de les retourner. Ainsi, chaque objet est responsable de ses champs, vous pouvez choisir de renvoyer null, une chaîne vide, ou lever une exception et la gérer un niveau plus haut dans la chaîne. Essayer de contourner cela revient à essayer de contourner l'encapsulation.

1voto

David Murdoch Points 28521

Voici une solution utilisant des opérateurs de coalescence nuls que j'ai mise au point pour le plaisir (les autres réponses sont meilleures). Si vous considérez que c'est la réponse, je vais devoir vous traquer et vous enlever votre clavier ! :-)

En gros, si un objet de Staff est null sa valeur par défaut sera utilisée à la place.

// define a defaultModel
var defaultModel = new { HomeAddress = new { PostCode = new { Town = "Default Town" } } };
// null coalesce through the chain setting defaults along the way.
var town = (((Staff ?? defaultModel)
                .HomeAddress  ?? defaultModel.HomeAddress)
                    .PostCode ?? defaultModel.HomeAddress.PostCode)
                        .Town ?? defaultModel.HomeAddress.PostCode.Town;

Disclaimer, je suis un gars de javascript et nous, les javascripters, savons que l'accès aux propriétés d'un objet peut être coûteux - nous avons donc tendance à mettre en cache un peu tout, ce qui est ce que le code ci-dessus accomplit (chaque propriété est seulement recherchée une fois). Avec les compilateurs et les optimiseurs de C#, il n'est probablement pas nécessaire de le faire (une confirmation à ce sujet serait la bienvenue).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X