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

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

if(abcdef != null) Console.Write("ok"); 

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

 if(a != null) if(ab != null) if(abc != null) if(abcd != null) if(abcde != null) if(abcdef != null) Console.Write("ok"); 

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

 if(a.Get(o=>ob).Get(o=>oc).Get(o=>od).Get(o=>oe).Get(o=>of) != null) Console.Write("ok"); 

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

 if(a.Get("bcdef") != null) Console.Write("ok"); 

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

 dynamic da = new SafeExpando(a); if(da.bcdef != null) Console.Write("ok"); 

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

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

 if(Get(abcdef) != null) Console.Write("ok"); 

要么

 if(a.Get(a=>abcdef) != null) Console.Write("ok"); 

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

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

 [DebuggerStepThrough] public static To Get(this From @this, Func 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; } 

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

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

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

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

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

我们还将添加一个检查,以便底层表达式为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<>); } } 

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

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

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

 public static Func PropogateNull(this Expression> 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>(body, expression.Parameters) .Compile(); } 

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

(你可以简单地修改它来接受lambda接受一个参数,然后传入一个值,但你可以很容易地关闭那个参数,所以我认为没有真正的理由。)


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

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

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