如何使用表达式树安全地访问可为空对象的路径? [英] How to use Expression Tree to safely access path of nullable objects?

查看:23
本文介绍了如何使用表达式树安全地访问可为空对象的路径?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当我将反序列化的 XML 结果放入 xsd 生成的对象树中并想在该树 a.b.c.d.e.f 中使用某个深层对象时,如果该查询路径上的任何节点丢失,它将给我异常.

When I get deserialized XML result into xsd-generated tree of objects and want to use some deep object inside that tree a.b.c.d.e.f, it will give me exception if any node on that query path is missing.

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

我想避免像这样为每个级别检查 null:

I want to avoid checking for null for each level like this:

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

第一个解决方案是实现允许这样做的 Get 扩展方法:

First solution is to implement Get extension method that allows this:

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

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

Second solution is to implement Get(string) extension method and use reflection to get result looking like this:

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

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

Third solution, could be to implement ExpandoObject and use dynamic type to get result looking like this:

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

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

But last 2 solutions do not give benefits of strong typing and IntelliSense.

我认为最好的可能是可以使用表达式树实现的第四个解决方案:

I think the best could be fourth solution that can be implemented with Expression Trees:

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

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

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

I already implemented 1st and 2nd solutions.

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

Here is how 1st solution looks like:

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

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

How to implement 4th solution if possible ?

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

Also it would be interesting to see how to implement 3rd solution if possible.

推荐答案

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

So the starting place is creating an expression visitor. This lets us find all of the member accesses within a particular expression. This leaves us with the question of what to do for each member access.

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

So the first thing is to recursively visit on the expression that the member is being accessed on. From there, we can use Expression.Condition to create a conditional block that compares that processed underlying expression to null, and returns null if true an the original starting expression if it's not.

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

Note that we need to provide implementations for both Members and method calls, but the process for each is basically identical.

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

We'll also add in a check so that of the underlying expression is null (which is to say, there is no instance and it's a static member) or if it's a non-nullable type, that we just use the base behavior instead.

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

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

We can then create an extension method to make calling it easier:

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

以及接受一个 lambda 而不是任何表达式,并且可以返回一个已编译的委托:

As well as one that accepts a lambda, rather than any expression, and can return a compiled delegate:

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

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

Note that, to support cases where the accessed member resolves to a non-nullable value, we're changing the type of those expressions to lift them to be nullable, using MakeNullable. This is a problem with this final expression, as it needs to be a Func<T>, and it won't match if T isn't also lifted. Thus, while it's very much non-ideal (ideally you'd never call this method with a non-nullable T, but there's no good way to support this in C#) we coalesce the final value using the default value for that type, if necessary.

(您可以简单地修改它以接受一个接受参数的 lambda,并传入一个值,但您也可以很容易地关闭该参数,所以我认为没有真正的理由.)

(You can trivially modify this to accept a lambda accepting a parameter, and pass in a value, but you can just as easily close over that parameter instead, so I see no real reason to.)

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

It's also worth pointing out that in C# 6.0, when it's actually released, we'll have an actual null propogation operator (?.), making all of this very unnecessary. You'll be able to write:

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

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

and have exactly the semantics you're looking for.

这篇关于如何使用表达式树安全地访问可为空对象的路径?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆