Wie verwende ich Expression Tree, um sicher auf den Pfad von nullbaren Objekten zuzugreifen?

c# expression-trees lambda path tree

Frage

Wenn ich deserialisiertes XML-Ergebnis in den xsd-generierten Baum von Objekten bekomme und ein tiefes Objekt in diesem Baum abcdef verwenden möchte, gibt es eine Ausnahme, wenn ein Knoten in diesem Abfragepfad fehlt.

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

Ich möchte vermeiden, für jedes Level wie folgt nach null zu suchen:

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

Die erste Lösung besteht darin, die Get-Erweiterungsmethode zu implementieren, die dies ermöglicht:

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

Die zweite Lösung besteht darin, die Get (string) -Erweiterungsmethode zu implementieren und Reflektion zu verwenden, um ein Ergebnis zu erhalten, das folgendermaßen aussieht:

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

Die dritte Lösung könnte darin bestehen, ExpandoObject zu implementieren und den dynamischen Typ zu verwenden, um ein Ergebnis zu erhalten, das folgendermaßen aussieht:

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

Aber die letzten 2 Lösungen bieten keine Vorteile von starker Typisierung und IntelliSense.

Ich denke, die beste könnte vierte Lösung sein, die mit Expression Trees implementiert werden kann:

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

oder

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

Ich habe bereits erste und zweite Lösungen implementiert.

So sieht die erste Lösung aus:

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

Wie implementiert man die 4. Lösung wenn möglich?

Es wäre auch interessant zu sehen, wie eine dritte Lösung implementiert werden könnte.

Akzeptierte Antwort

Der Ausgangspunkt ist also ein Expression-Besucher. Auf diese Weise können wir alle Mitgliedzugriffe innerhalb eines bestimmten Ausdrucks finden. Dies hinterlässt uns die Frage, was wir für jeden Mitgliederzugriff tun müssen.

Das erste ist also, den Ausdruck, auf den auf das Mitglied zugegriffen wird, rekursiv zu besuchen. Von dort aus können wir Expression.Condition , um einen bedingten Block zu erstellen, der diesen verarbeiteten zugrunde liegenden Ausdruck mit null vergleicht und null zurückgibt, wenn der ursprüngliche Ausgangsausdruck wahr ist, falls dies nicht der Fall ist.

Beachten Sie, dass wir Implementierungen für Member- und Methodenaufrufe bereitstellen müssen, aber der Prozess für beide ist grundsätzlich identisch.

Wir fügen auch eine Überprüfung hinzu, so dass der zugrundeliegende Ausdruck null (dh es gibt keine Instanz und es ist ein statisches Element) oder wenn es sich um einen nicht nullbaren Typ handelt, verwenden wir stattdessen einfach das Basisverhalten.

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

Wir können dann eine Erweiterungsmethode erstellen, um den Aufruf zu vereinfachen:

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

Sowie eine, die ein Lambda anstelle eines Ausdrucks akzeptiert und einen kompilierten Delegaten zurückgeben kann:

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

Beachten Sie, dass wir zur Unterstützung von Fällen, in denen das Element, auf das zugegriffen wird, in einen nicht nullbaren Wert aufgelöst wird, den Typ dieser Ausdrücke so ändern, dass sie nullable sind, indem MakeNullable . Dies ist ein Problem mit diesem letzten Ausdruck, da es ein Func<T> , und es wird nicht übereinstimmen, wenn T nicht auch aufgehoben wird. Obwohl es sehr wenig ideal ist (im Idealfall würden Sie diese Methode niemals mit einem nicht nullbaren T aufrufen, aber es gibt keine gute Möglichkeit, dies in C # zu unterstützen), koaleszieren wir den endgültigen Wert, indem wir den Standardwert für diesen Typ verwenden notwendig.

(Sie können dies trivial ändern, um ein Lambda zu akzeptieren, das einen Parameter akzeptiert, und einen Wert übergeben, aber Sie können diesen Parameter auch einfach so schließen, sodass ich keinen wirklichen Grund sehe, dies zu tun.)


Es lohnt sich auch, darauf hinzuweisen, dass wir in C # 6.0, wenn es tatsächlich veröffentlicht wird, einen tatsächlichen Null-Propagierungsoperator ( ?. ) Haben, was all dies sehr unnötig macht. Sie können schreiben:

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

und genau die Semantik haben, nach der du suchst.



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