Wie untergliedere ich eine Kette von Mitgliedszugriffsausdrücken?

c# expression-trees lambda lifting nullreferenceexception

Frage

Die Kurzfassung (TL; DR):

Angenommen, ich habe einen Ausdruck, der nur eine Kette von Mitgliedszugriffsoperatoren darstellt:

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

Sie können sich diesen Ausdruck als eine Zusammensetzung von Unterausdrücken vorstellen, die jeweils eine Mitgliedszugriffsoperation umfassen:

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;

Was ich will, ist , tun brechen e nach unten in diese Komponente Unterausdrücke , damit ich mit ihnen individuell arbeiten können.

Die noch kürzere Version:

Wenn ich den Ausdruck x => x.foo.bar , weiß ich schon, wie man x => x.foo . Wie kann ich den anderen Unterausdruck foo => foo.bar ?

Warum mache ich das?

Ich versuche zu simulieren "Heben" des Mitgliedszugriffsoperators in C #, wie CoffeeScript Existential Access Operator ?. . Eric Lippert hat erklärt, dass ein ähnlicher Betreiber für C # in Betracht gezogen wurde, aber es gab kein Budget, um es umzusetzen.

Wenn ein solcher Operator in C # existiert, könnten Sie Folgendes tun:

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

Wenn sich ein Teil der target.foo.bar.baz Kette als null herausstellen würde, würde diese ganze Sache zu null ausgewertet werden, wodurch eine NullReferenceException vermieden wird.

Ich möchte eine Lift Erweiterungsmethode, die solche Dinge simulieren kann:

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

Was ich versucht habe:

Ich habe etwas, das kompiliert, und es funktioniert irgendwie. Es ist jedoch unvollständig, weil ich nur weiß, wie man die linke Seite eines Mitgliedszugriffsausdrucks behält. Ich kann x => x.foo.bar.baz in x => x.foo.bar , aber ich weiß nicht, wie man bar => bar.baz .

Am Ende macht es so etwas (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);

Dies bedeutet, dass die am weitesten links liegenden Schritte im Ausdruck immer wieder ausgewertet werden. Vielleicht keine große Sache, wenn es sich nur um Eigenschaften von POCO-Objekten handelt, sondern um Methodenaufrufe, und die Ineffizienz (und mögliche Nebenwirkungen) werden viel offensichtlicher:

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

Der Code:

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

Dies wurde lose von dieser Antwort inspiriert.


Alternativen zu einer Lift-Methode und warum ich sie nicht benutzen kann:

Die vielleicht Monade

value = x.ToMaybe()
         .Bind(y => y.foo)
         .Bind(f => f.bar)
         .Bind(b => b.baz)
         .Value;
Vorteile:
  1. Verwendet ein vorhandenes Muster, das in der funktionalen Programmierung beliebt ist
  2. Hat andere Anwendungen neben dem Zugriff auf Mitglieder
Nachteile:
  1. Es ist zu ausführlich. Ich möchte nicht jedes Mal eine riesige Kette von Funktionsaufrufen haben, wenn ich ein paar Mitglieder nach unten bohren möchte. Selbst wenn ich SelectMany implementiere und die Abfragesyntax verwende, wird IMHO chaotischer aussehen, nicht weniger.
  2. Ich muss x.foo.bar.baz als einzelne Komponenten manuell neu x.foo.bar.baz , was bedeutet, dass ich wissen muss, was sie zur Kompilierzeit sind. Ich kann nicht einfach einen Ausdruck aus einer Variablen wie result = Lift(expr, obj); .
  3. Nicht wirklich für das, was ich versuche zu tun, und fühlt sich nicht wie eine perfekte Passform.

ExpressionVisitor

Ich habe Ian Griffiths LiftMemberAccessToNull-Methode in eine generische Erweiterungsmethode umgewandelt, die wie beschrieben verwendet werden kann. Der Code ist zu lang, um hier aufgenommen zu werden, aber ich werde einen Gist posten, wenn jemand interessiert ist.

Vorteile:
  1. Folgt der Syntax result = target.Lift(x => x.foo.bar.baz)
  2. Funktioniert hervorragend, wenn jeder Schritt in der Kette einen Referenztyp oder einen nicht nullbaren Werttyp zurückgibt
Nachteile:
  1. Es drosselt, wenn ein Mitglied in der Kette ein Nullwerttyp ist, was seine Nützlichkeit für mich wirklich einschränkt. Ich brauche es für Nullable<DateTime> -Mitglieder.

Versuchen / fangen

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

Dies ist der naheliegendste Weg, und ich werde es verwenden, wenn ich keinen eleganteren Weg finde.

Vorteile:
  1. Es ist einfach.
  2. Es ist offensichtlich, wofür der Code ist.
  3. Ich muss mir keine Sorgen um Randfälle machen.
Nachteile:
  1. Es ist hässlich und ausführlich
  2. Der try / catch-Block ist ein nichttrivialer * Leistungstreffer
  3. Da es sich um einen Anweisungsblock handelt, kann ich keinen Ausdrucksbaum für LINQ ausgeben
  4. Es fühlt sich an wie eine Niederlage zuzugeben

Ich werde nicht lügen; "Ich gebe keine Niederlage zu" ist der Hauptgrund, warum ich so stur bin. Mein Instinkt sagt, dass es einen eleganten Weg dafür geben muss, aber es war eine Herausforderung, sie zu finden. Ich kann nicht glauben, dass es so einfach ist, auf die linke Seite eines Ausdrucks zuzugreifen, aber die rechte Seite ist nahezu unerreichbar.

Ich habe hier wirklich zwei Probleme, also nehme ich alles an, was eines löst:

  • Die Ausdruckszerlegung, die beide Seiten erhält, hat eine angemessene Leistung und funktioniert auf jedem beliebigen Typ
  • Zugriff auf Null-propagierenden Members

Aktualisieren:

Null-Ausbreitungsteil Zugang ist für enthalten in geplanten C # 6.0 . Ich würde trotzdem eine Lösung für die Dekomposition von Ausdrücken finden.

Akzeptierte Antwort

Wenn es sich nur um eine einfache Kette von Mitgliedszugriffsausdrücken handelt, gibt es eine einfache Lösung:

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

BEARBEITEN

Verwenden Sie diese aktualisierte Methode, wenn Sie Unterstützung für Methodenaufrufe hinzufügen möchten:

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


Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow