如何使用表达式树安全地访问可空对象的路径?

alp*_*pav 6 c# tree lambda path expression-trees

当我将反序列化的XML结果导入到xsd生成的对象树中并希望在该树abcdef中使用一些深层对象时,如果缺少该查询路径上的任何节点,它将给出异常.

if(a.b.c.d.e.f != null)
    Console.Write("ok");
Run Code Online (Sandbox Code Playgroud)

我想避免为每个级别检查null,如下所示:

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");
Run Code Online (Sandbox Code Playgroud)

第一个解决方案是实现Get扩展方法,该方法允许:

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");
Run Code Online (Sandbox Code Playgroud)

第二个解决方案是实现Get(string)扩展方法并使用反射来获得如下所示的结果:

if(a.Get("b.c.d.e.f") != null)
    Console.Write("ok");
Run Code Online (Sandbox Code Playgroud)

第三种解决方案,可以是实现ExpandoObject并使用动态类型来获得如下所示的结果:

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

但是最后2个解决方案并没有带来强类型和智能感知的好处.

我认为最好的可能是可以用Expression Trees实现的第四个解决方案:

if(Get(a.b.c.d.e.f) != null)
    Console.Write("ok");
Run Code Online (Sandbox Code Playgroud)

要么

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

我已经实施了第一和第二个解决方案

以下是第一个解决方案的样子:

[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;
}
Run Code Online (Sandbox Code Playgroud)

如果可能,如何实施第四解决方案?

如果可能的话,看看如何实施第三种解决方案也很有趣.

Ser*_*rvy 13

所以起点是创建一个表达式访问者.这使我们可以找到特定表达式中的所有成员访问.这给我们留下了为每个成员访问做些什么的问题.

所以第一件事就是以递归方式访问正在访问该成员的表达式.从那里,我们可以使用Expression.Condition创建一个条件块来比较已处理的底层表达式null,null如果不是则返回true(原始起始表达式).

请注意,我们需要为成员和方法调用提供实现,但每个的过程基本相同.

我们还将添加一个检查,以便底层表达式null(也就是说,没有实例,它是一个静态成员)或者如果它是一个非可空类型,我们只使用基本行为.

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<>);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,我们可以创建一个扩展方法,使其更容易调用:

public static Expression PropogateNull(this Expression expression)
{
    return new MemberNullPropogationVisitor().Visit(expression);
}
Run Code Online (Sandbox Code Playgroud)

除了接受lambda而不是任何表达式之外,还可以返回已编译的委托:

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();
}
Run Code Online (Sandbox Code Playgroud)

请注意,为了支持被访问成员解析为不可为空的值的情况,我们正在改变这些表达式的类型,以便将它们提升为可为空MakeNullable.这是最后一个表达式的一个问题,因为它需要是a Func<T>,如果T不被解除则不匹配.因此,虽然它非常不理想(理想情况下你永远不会用非可空的方法调用这个方法T,但是在C#中没有好的方法来支持它),如果需要,我们使用该类型的默认值来合并最终值. .

(你可以通过简单地修改它来接受lambda接受一个参数,并传入一个值,但你可以轻松地关闭那个参数,所以我认为没有真正的理由.)


值得指出的是,在C#6.0中,当它实际发布时,我们将有一个实际的null传播运算符(?.),这使得所有这些都非常不必要.你可以写:

if(a?.b?.c?.d?.e?.f != null)
    Console.Write("ok");
Run Code Online (Sandbox Code Playgroud)

并且具有您正在寻找的语义.