Comment utiliser Expression Tree pour accéder en toute sécurité au chemin des objets nullables?

c# expression-trees lambda path tree

Question

Lorsque j'obtiens un résultat XML désérialisé dans une arborescence d'objets générée par xsd et que je souhaite utiliser un objet profond à l'intérieur de cette arborescence abcdef, cela me donnera une exception si un nœud de ce chemin de requête manque.

if(a.b.c.d.e.f != null)
    Console.Write("ok");

Je veux éviter de vérifier null pour chaque niveau comme ceci:

if(a != null)
if(a.b != null)
if(a.b.c != null)
if(a.b.c.d != null)
if(a.b.c.d.e != null)
if(a.b.c.d.e.f != null)
    Console.Write("ok");

La première solution consiste à implémenter la méthode d'extension Get qui permet ceci:

if(a.Get(o=>o.b).Get(o=>o.c).Get(o=>o.d).Get(o=>o.e).Get(o=>o.f) != null)
    Console.Write("ok");

La deuxième solution consiste à implémenter la méthode d'extension Get (string) et à utiliser la réflexion pour obtenir un résultat ressemblant à ceci:

if(a.Get("b.c.d.e.f") != null)
    Console.Write("ok");

La troisième solution pourrait consister à implémenter ExpandoObject et à utiliser un type dynamique pour obtenir un résultat ressemblant à ceci:

dynamic da = new SafeExpando(a);
if(da.b.c.d.e.f != null)
    Console.Write("ok");

Mais les deux dernières solutions n'offrent pas les avantages d'un typage puissant et d'IntelliSense.

Je pense que la meilleure solution pourrait être la quatrième solution pouvant être implémentée avec Expression Trees:

if(Get(a.b.c.d.e.f) != null)
    Console.Write("ok");

ou

if(a.Get(a=>a.b.c.d.e.f) != null)
    Console.Write("ok");

J'ai déjà implémenté les 1ère et 2ème solutions.

Voici à quoi ressemble la 1ère solution:

[DebuggerStepThrough]
public static To Get<From,To>(this From @this, Func<From,To> get)
{
    var ret = default(To);
    if(@this != null && !@this.Equals(default(From)))
        ret = get(@this);

    if(ret == null && typeof(To).IsArray)
        ret = (To)Activator.CreateInstance(typeof(To), 0);

    return ret;
}

Comment mettre en œuvre la 4ème solution si possible?

Aussi, il serait intéressant de voir comment implémenter la 3ème solution si possible.

Réponse acceptée

Donc, le point de départ est la création d'un visiteur d'expression. Cela nous permet de trouver tous les accès membres dans une expression particulière. Cela nous laisse avec la question de savoir quoi faire pour chaque accès membre.

La première chose à faire est donc de consulter récursivement l'expression sur laquelle le membre est consulté. À partir de là, nous pouvons utiliser Expression.Condition pour créer un bloc conditionnel qui compare l’expression sous-jacente traitée à null et renvoie null si true et l’expression de départ originale si ce n’est pas le cas.

Notez que nous devons fournir des implémentations pour les membres et les appels de méthodes, mais le processus pour chacun est fondamentalement identique.

Nous ajouterons également une vérification pour que l'expression sous-jacente soit null (c'est-à-dire qu'il n'y ait pas d'instance et qu'il s'agisse d'un membre statique) ou, s'il s'agit d'un type non nullable, nous utilisons simplement le comportement de base.

public class MemberNullPropogationVisitor : ExpressionVisitor
{
    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Expression == null || !IsNullable(node.Expression.Type))
            return base.VisitMember(node);

        var expression = base.Visit(node.Expression);
        var nullBaseExpression = Expression.Constant(null, expression.Type);
        var test = Expression.Equal(expression, nullBaseExpression);
        var memberAccess = Expression.MakeMemberAccess(expression, node.Member);
        var nullMemberExpression = Expression.Constant(null, node.Type);
        return Expression.Condition(test, nullMemberExpression, node);
    }

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Object == null || !IsNullable(node.Object.Type))
            return base.VisitMethodCall(node);

        var expression = base.Visit(node.Object);
        var nullBaseExpression = Expression.Constant(null, expression.Type);
        var test = Expression.Equal(expression, nullBaseExpression);
        var memberAccess = Expression.Call(expression, node.Method);
        var nullMemberExpression = Expression.Constant(null, MakeNullable(node.Type));
        return Expression.Condition(test, nullMemberExpression, node);
    }

    private static Type MakeNullable(Type type)
    {
        if (IsNullable(type))
            return type;

        return typeof(Nullable<>).MakeGenericType(type);
    }

    private static bool IsNullable(Type type)
    {
        if (type.IsClass)
            return true;
        return type.IsGenericType &&
            type.GetGenericTypeDefinition() == typeof(Nullable<>);
    }
}

Nous pouvons ensuite créer une méthode d’extension pour faciliter l’appel:

public static Expression PropogateNull(this Expression expression)
{
    return new MemberNullPropogationVisitor().Visit(expression);
}

Ainsi que celui qui accepte un lambda, plutôt que toute expression, et peut renvoyer un délégué compilé:

public static Func<T> PropogateNull<T>(this Expression<Func<T>> expression)
{
    var defaultValue = Expression.Constant(default(T));
    var body = expression.Body.PropogateNull();
    if (body.Type != typeof(T))
        body = Expression.Coalesce(body, defaultValue);
    return Expression.Lambda<Func<T>>(body, expression.Parameters)
        .Compile();
}

Notez que, pour prendre en charge les cas où le membre auquel on accède est résolu en une valeur non nullable, nous modifions le type de ces expressions pour les rendre nulles, à l’aide de MakeNullable . C'est un problème avec cette expression finale, car il doit s'agir d'un Func<T> , et ne correspondra pas si T n'est pas également levé. Ainsi, bien que ce soit vraiment non idéal (idéalement, vous n'appelleriez jamais cette méthode avec un T non nullable, mais il n'y a aucun moyen de le supporter en C #), nous fusionnons la valeur finale en utilisant la valeur par défaut pour ce type, si nécessaire.

(Vous pouvez modifier trivialement ceci pour accepter un lambda acceptant un paramètre et lui transmettre une valeur, mais vous pouvez tout aussi facilement fermer ce paramètre, alors je ne vois aucune raison réelle de le faire.)


Il convient également de souligner qu'en C # 6.0, une fois publié, un opérateur de propogation nulle ( ?. ) Est créé, ce qui rend tout cela très inutile. Vous pourrez écrire:

if(a?.b?.c?.d?.e?.f != null)
    Console.Write("ok");

et avez exactement la sémantique que vous recherchez.



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow