Come usare Expression Tree per accedere in sicurezza al percorso degli oggetti nullable?

c# expression-trees lambda path tree

Domanda

Quando ottengo risultati XML deserializzati in un albero di oggetti generato da xsd e voglio usare qualche oggetto profondo all'interno di tale albero abcdef, mi darà un'eccezione se manca qualsiasi nodo su quel percorso di ricerca.

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

Voglio evitare di verificare null per ogni livello come questo:

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 prima soluzione è implementare il metodo di estensione Get che consente questo:

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 seconda soluzione consiste nell'implementare il metodo di estensione Get (stringa) e utilizzare il reflection per ottenere un risultato simile a questo:

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

Terza soluzione, potrebbe essere quella di implementare ExpandoObject e utilizzare il tipo dinamico per ottenere risultati come questo:

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

Ma le ultime 2 soluzioni non danno benefici di tipizzazione forte e IntelliSense.

Penso che la soluzione migliore potrebbe essere la quarta soluzione che può essere implementata con Expression Trees:

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

o

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

Ho già implementato le soluzioni 1a e 2a.

Ecco come appare la prima soluzione:

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

Come implementare la quarta soluzione, se possibile?

Sarebbe anche interessante vedere come implementare la terza soluzione, se possibile.

Risposta accettata

Quindi il punto di partenza sta creando un visitatore di espressioni. Questo ci permette di trovare tutti gli accessi dei membri all'interno di una particolare espressione. Questo ci lascia con la domanda su cosa fare per ogni accesso dei membri.

Quindi, la prima cosa è visitare ricorsivamente sull'espressione a cui si accede il membro. Da lì, possiamo usare Expression.Condition per creare un blocco condizionale che paragona l'espressione sottostante elaborata a null e restituisce null se true e l'espressione iniziale originale se non lo è.

Si noti che è necessario fornire implementazioni per entrambi i membri e le chiamate ai metodi, ma il processo per ciascuno è sostanzialmente identico.

Aggiungiamo anche un controllo in modo che l'espressione sottostante sia null (ovvero, non ci sia un'istanza ed è un membro statico) o se si tratta di un tipo non nullable, che invece usiamo semplicemente il comportamento di 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<>);
    }
}

Possiamo quindi creare un metodo di estensione per rendere più semplice la chiamata:

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

Così come uno che accetta un lambda, piuttosto che qualsiasi espressione, e può restituire un delegato compilato:

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

Si noti che, per supportare i casi in cui il membro a cui si accede risolve un valore non annullabile, stiamo modificando il tipo di quelle espressioni per sollevarle in modo da rendere nullable, utilizzando MakeNullable . Questo è un problema con questa espressione finale, in quanto deve essere un Func<T> , e non corrisponderà se T non viene sollevato. Quindi, mentre è molto non ideale (idealmente non chiameresti mai questo metodo con una T non nullable, ma non c'è un buon modo per supportarlo in C #) e unire il valore finale usando il valore predefinito per quel tipo, se necessario.

(Puoi banalmente modificarlo per accettare un lambda che accetta un parametro e passare un valore, ma puoi semplicemente chiudere questo parametro, quindi non vedo alcun motivo per).


Vale anche la pena sottolineare che in C # 6.0, quando viene effettivamente rilasciato, avremo un vero operatore di propagazione nullo ( ?. ), Rendendo tutto ciò molto superfluo. Sarai in grado di scrivere:

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

e hai esattamente la semantica che stai cercando.



Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché
Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché