2015-05-27 19 views
6

Quando ottengo il risultato XML deserializzato in un albero di oggetti generato da xsd e voglio utilizzare un oggetto profondo all'interno di tale albero a.b.c.d.e.f, esso mi darà un'eccezione se manca qualsiasi nodo su quel percorso di query.Come utilizzare Expression Tree per accedere in modo sicuro al percorso degli oggetti nullable?

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

voglio evitare il controllo per null per ogni livello in questo modo:

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

prima soluzione è quella di implementare Get metodo di estensione che permette 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"); 

seconda soluzione è quella implementare Get (stringa) metodo di estensione e utilizzare la riflessione per ottenere risultati come questo:

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

terza soluzione, potrebbe essere quella di implementare e utilizzare ExpandoObject tipo dinamico per ottenere il risultato simile a questo:

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

Ma lo scorso 2 soluzioni non danno vantaggi della tipizzazione forte e IntelliSense.

penso che il 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 1 ° e 2 ° già implementato soluzioni.

Ecco come prima soluzione si presenta come:

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

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

    return ret; 
} 

come implementare quarta soluzione, se possibile?

Inoltre sarebbe interessante vedere come implementare la terza soluzione, se possibile.

+0

[Questa domanda] (http://stackoverflow.com/questions/3897249/how-to-avoid-multiple-if-null-checks) ha due collegamenti alle domande con risposte. Uno dipende dal compilatore di Roslyn; l'altro è un semplice snippet di codice che farà il trucco. –

+0

Ho letto la domanda vera e le risposte sopra non indirizzano direttamente la tua domanda ma sono rilevanti e interessanti –

+0

Recentemente ho sperimentato questo e forse la mia soluzione sarebbe interessante per te. L'ho pubblicato [qui] (http://codereview.stackexchange.com/questions/116798/improved-nullguard-v3-that-supports-property-chahod-methods-and-ignores-value-t) sulla revisione del codice. Ho scritto la mia implementazione perché volevo qualcosa di più che semplice controllo su null ;-) La tua domanda e la risposta di Servy mi hanno ispirato a provare qualcosa di diverso. – t3chb0t

risposta

12

Quindi il luogo 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 in modo ricorsivo sull'espressione a cui accede il membro. Da lì, è possibile utilizzare Expression.Condition per creare un blocco condizionale che confronta l'espressione sottostante elaborata su 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 di metodo, 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 è un tipo non annullabile, che usiamo solo il comportamento di base invece.

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 definendola facile:

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

così come uno che accetta un lambda, piuttosto che qualsiasi espressione, e in grado di restituire un delegato compilato:

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 anche lo T non viene sollevato. Quindi, mentre è molto non ideale (idealmente non chiameresti mai questo metodo con un valore non nullable T, ma non c'è un buon modo per supportare questo in C#) e unire il valore finale usando il valore predefinito per quel tipo, se necessario.

(È banalmente possibile modificare questo per accettare un lambda accettare un parametro, e passare un valore, ma si può altrettanto facilmente vicino su quel parametro, invece, in modo da non vedo alcuna ragione reale 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. Potrai scrivere:

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

e avere esattamente la semantica che stai cercando.

+0

Expression.Constant (null, node.Type) per tipo di valore genera eccezione, sostituisce null con il valore predefinito di tipo (http://stackoverflow.com/a/2490274/440030) risolto problema. –

+0

@RezaArabQaeni Questa non è proprio la semantica corretta per un operatore di propagazione nullo. Dovrebbe sollevare il risultato in un tipo annullabile, ma i limiti del linguaggio non consentono realmente che ciò accada in tutti i casi, solo alcuni potrebbero essere supportati. Specificamente, se l'espressione finale viene revocata, non corrisponderà più alla firma del delegato, e il cambiamento è davvero al di là di ciò che C# può supportare efficacemente. L'utilizzo del valore predefinito in quel caso è almeno migliore del solito, credo, quindi l'ho fatto per ora. – Servy

+0

Ok, hai ragione, allora qual è la tua idea su quando il tipo di nodo è il tipo di valore allora invece di Expression.Condition (per verificare se la proprietà è null) restituisce un'espressione semplice senza verifica condizione nulla, anche se non so quale sia l'implementazione di questa semplice espressione –