Come posso abbattere una catena di espressioni di accesso dei membri?

c# expression-trees lambda lifting nullreferenceexception

Domanda

La versione abbreviata (TL; DR):

Supponiamo di avere un'espressione che è solo una catena di operatori di accesso membri:

Expression<Func<Tx, Tbaz>> e = x => x.foo.bar.baz;

Puoi pensare a questa espressione come a una composizione di sottoespressioni, ognuna delle quali comprende un'operazione di accesso membro:

Expression<Func<Tx, Tfoo>>   e1 = (Tx x) => x.foo;
Expression<Func<Tfoo, Tbar>> e2 = (Tfoo foo) => foo.bar;
Expression<Func<Tbar, Tbaz>> e3 = (Tbar bar) => bar.baz;

Quello che voglio fare è rompere e giù in questi componenti sub-espressioni in modo da poter lavorare con loro individualmente.

La versione ancora più corta:

Se ho l'espressione x => x.foo.bar , so già come rompere x => x.foo . Come posso estrarre l'altra sottoespressione, foo => foo.bar ?

Perché sto facendo questo:

Sto cercando di simulare il "sollevamento" dell'operatore di accesso membro in C #, come l'operatore di accesso esistenziale di CoffeeScript ?. . Eric Lippert ha dichiarato che un operatore simile era considerato per C #, ma non c'era un budget per implementarlo.

Se un tale operatore esistesse in C #, potresti fare qualcosa del genere:

value = target?.foo?.bar?.baz;

Se qualsiasi parte della catena target.foo.bar.baz risulta essere nulla, allora l'intera cosa valuterà il valore null, evitando così una NullReferenceException.

Voglio un metodo di estensione Lift che possa simulare questo tipo di cose:

value = target.Lift(x => x.foo.bar.baz); //returns target.foo.bar.baz or null

Quello che ho provato:

Ho qualcosa che compila, e funziona in un certo senso. Tuttavia, è incompleto perché so solo come mantenere il lato sinistro di un'espressione di accesso membro. Posso trasformare x => x.foo.bar.baz in x => x.foo.bar , ma non so come mantenere la bar => bar.baz .

Quindi finisce per fare qualcosa del genere (pseudocodice):

return (x => x)(target) == null ? null
       : (x => x.foo)(target) == null ? null
       : (x => x.foo.bar)(target) == null ? null
       : (x => x.foo.bar.baz)(target);

Ciò significa che i passaggi più a sinistra nell'espressione vengono valutati più e più volte. Forse non è un grosso problema se sono solo proprietà sugli oggetti POCO, ma trasformali in chiamate di metodo e l'inefficienza (ei potenziali effetti collaterali) diventano molto più evidenti:

//still pseudocode
return (x => x())(target) == null ? null
       : (x => x().foo())(target) == null ? null
       : (x => x().foo().bar())(target) == null ? null
       : (x => x().foo().bar().baz())(target);

Il codice:

static TResult Lift<T, TResult>(this T target, Expression<Func<T, TResult>> exp)
    where TResult : class
{
    //omitted: if target can be null && target == null, just return null

    var memberExpression = exp.Body as MemberExpression;
    if (memberExpression != null)
    {
        //if memberExpression is {x.foo.bar}, then innerExpression is {x.foo}
        var innerExpression = memberExpression.Expression;
        var innerLambda = Expression.Lambda<Func<T, object>>(
                              innerExpression, 
                              exp.Parameters
                          );  

        if (target.Lift(innerLambda) == null)
        {
            return null;
        }
        else
        {
            ////This is the part I'm stuck on. Possible pseudocode:
            //var member = memberExpression.Member;              
            //return GetValueOfMember(target.Lift(innerLambda), member);
        }
    }

    //For now, I'm stuck with this:
    return exp.Compile()(target);
}

Questo è stato liberamente ispirato da questa risposta .


Alternative a un metodo di sollevamento e perché non posso usarle:

La forse monade

value = x.ToMaybe()
         .Bind(y => y.foo)
         .Bind(f => f.bar)
         .Bind(b => b.baz)
         .Value;
Professionisti:
  1. Utilizza uno schema esistente che è popolare nella programmazione funzionale
  2. Ha altri usi oltre all'accesso dei membri sollevato
Contro:
  1. È troppo prolisso. Non voglio una catena massiccia di chiamate di funzioni ogni volta che voglio scavare alcuni membri. Anche se implemento SelectMany e utilizzo la sintassi della query, IMHO sembrerà più disordinato, non meno.
  2. Devo riscrivere manualmente x.foo.bar.baz come i suoi singoli componenti, il che significa che devo sapere cosa sono in fase di compilazione. Non posso semplicemente usare un'espressione da una variabile come result = Lift(expr, obj); .
  3. Non proprio progettato per quello che sto cercando di fare, e non mi sembra una perfetta aderenza.

ExpressionVisitor

Ho modificato il metodo LiftMemberAccessToNull di Ian Griffith in un metodo di estensione generico che può essere utilizzato come ho descritto. Il codice è troppo lungo da includere qui, ma pubblicherò un Gist se qualcuno fosse interessato.

Professionisti:
  1. Segue la result = target.Lift(x => x.foo.bar.baz)
  2. Funziona alla grande se ogni passo della catena restituisce un tipo di riferimento o un tipo di valore non annullabile
Contro:
  1. Soffoca se un membro della catena ha un valore nullo, il che limita davvero la sua utilità a me. Ho bisogno che funzioni per i membri Nullable<DateTime> .

Prova a prendere

try 
{ 
    value = x.foo.bar.baz; 
}
catch (NullReferenceException ex) 
{ 
    value = null; 
}

Questo è il modo più ovvio, ed è quello che userò se non riesco a trovare un modo più elegante.

Professionisti:
  1. È semplice.
  2. È ovvio a cosa serve il codice.
  3. Non devo preoccuparmi dei casi limite.
Contro:
  1. È brutto e prolisso
  2. Il blocco try / catch è un hit dalle prestazioni non banali *
  3. È un blocco di istruzioni, quindi non posso farlo emettere un albero di espressioni per LINQ
  4. Sembra ammettere la sconfitta

Non ho intenzione di mentire; "non ammettere la sconfitta" è la ragione principale per cui sono così testardo. Il mio istinto dice che ci deve essere un modo elegante per farlo, ma trovarlo è stata una sfida. Non riesco a credere che sia così facile accedere al lato sinistro di un'espressione, eppure il lato destro è quasi irraggiungibile.

Ho davvero due problemi qui, quindi accetterò tutto ciò che risolve uno dei due:

  • La decompressione di espressioni che preserva entrambi i lati, ha prestazioni ragionevoli e funziona su qualsiasi tipo
  • Accesso ai membri di propagazione null

Aggiornare:

L'accesso ai membri di propagazione dei nulli è pianificato per l' inclusione in C # 6.0 . Mi piacerebbe comunque una soluzione per la decomposizione delle espressioni.

Risposta accettata

Se si tratta solo di una semplice catena di espressioni di accesso dei membri, esiste una soluzione semplice:

public static TResult Lift<T, TResult>(this T target, Expression<Func<T, TResult>> exp)
    where TResult : class
{
    return (TResult) GetValueOfExpression(target, exp.Body);
}

private static object GetValueOfExpression<T>(T target, Expression exp)
{
    if (exp.NodeType == ExpressionType.Parameter)
    {
        return target;
    }
    else if (exp.NodeType == ExpressionType.MemberAccess)
    {
        var memberExpression = (MemberExpression) exp;
        var parentValue = GetValueOfExpression(target, memberExpression.Expression);

        if (parentValue == null)
        {
            return null;
        }
        else
        {
            if (memberExpression.Member is PropertyInfo)
                return ((PropertyInfo) memberExpression.Member).GetValue(parentValue, null);
            else
                return ((FieldInfo) memberExpression.Member).GetValue(parentValue);
        }
    }
    else
    {
        throw new ArgumentException("The expression must contain only member access calls.", "exp");
    }
}

MODIFICARE

Se si desidera aggiungere il supporto per le chiamate ai metodi, utilizzare questo metodo aggiornato:

private static object GetValueOfExpression<T>(T target, Expression exp)
{
    if (exp == null)
    {
        return null;
    }
    else if (exp.NodeType == ExpressionType.Parameter)
    {
        return target;
    }
    else if (exp.NodeType == ExpressionType.Constant)
    {
        return ((ConstantExpression) exp).Value;
    }
    else if (exp.NodeType == ExpressionType.Lambda)
    {
        return exp;
    }
    else if (exp.NodeType == ExpressionType.MemberAccess)
    {
        var memberExpression = (MemberExpression) exp;
        var parentValue = GetValueOfExpression(target, memberExpression.Expression);

        if (parentValue == null)
        {
            return null;
        }
        else
        {
            if (memberExpression.Member is PropertyInfo)
                return ((PropertyInfo) memberExpression.Member).GetValue(parentValue, null);
            else
                return ((FieldInfo) memberExpression.Member).GetValue(parentValue);
        }
    }
    else if (exp.NodeType == ExpressionType.Call)
    {
        var methodCallExpression = (MethodCallExpression) exp;
        var parentValue = GetValueOfExpression(target, methodCallExpression.Object);

        if (parentValue == null && !methodCallExpression.Method.IsStatic)
        {
            return null;
        }
        else
        {
            var arguments = methodCallExpression.Arguments.Select(a => GetValueOfExpression(target, a)).ToArray();

            // Required for comverting expression parameters to delegate calls
            var parameters = methodCallExpression.Method.GetParameters();
            for (int i = 0; i < parameters.Length; i++)
            {
                if (typeof(Delegate).IsAssignableFrom(parameters[i].ParameterType))
                {
                    arguments[i] = ((LambdaExpression) arguments[i]).Compile();
                }
            }

            if (arguments.Length > 0 && arguments[0] == null && methodCallExpression.Method.IsStatic &&
                methodCallExpression.Method.IsDefined(typeof(ExtensionAttribute), false)) // extension method
            {
                return null;
            }
            else
            {
                return methodCallExpression.Method.Invoke(parentValue, arguments);
            }
        }
    }
    else
    {
        throw new ArgumentException(
            string.Format("Expression type '{0}' is invalid for member invoking.", exp.NodeType));
    }
}


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é