如何使用表达式树安全地访问可为空对象的路径?
How to use Expression Tree to safely access path of nullable objects?
当我反序列化 XML 结果为 xsd 生成的对象树并想在该树 a.b.c.d.e.f 中使用一些深层对象时,如果有任何节点,它会给我异常缺少该查询路径。
if(a.b.c.d.e.f != null)
Console.Write("ok");
我想避免像这样检查每个级别的空值:
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 扩展方法:
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) 扩展方法并使用反射来获得如下所示的结果:
if(a.Get("b.c.d.e.f") != null)
Console.Write("ok");
第三个解决方案,可以是实现 ExpandoObject 并使用动态类型来获得如下所示的结果:
dynamic da = new SafeExpando(a);
if(da.b.c.d.e.f != null)
Console.Write("ok");
但是最后两个解决方案没有提供强类型和 IntelliSense 的好处。
我认为最好的可能是第四种可以用表达式树实现的解决方案:
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");
我已经实施了第一个和第二个解决方案。
这是第一个解决方案的样子:
[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;
}
如果可能,如何实施第 4 种解决方案?
如果可能的话,看看如何实施第三个解决方案也很有趣。
因此,起点是创建一个表达式访问者。这让我们可以找到特定表达式中的所有成员访问。这给我们留下了一个问题,即如何为每个成员访问做些什么。
所以第一件事就是递归访问成员被访问的表达式。从那里,我们可以使用 Expression.Condition
创建一个条件块,将处理过的基础表达式与 null
进行比较,如果为真,则 returns 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 而不是任何表达式,并且可以 return 编译委托的人:
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
没有被提升,它也不会匹配。因此,尽管它非常不理想(理想情况下,您永远不会使用不可为 null 的 T
调用此方法,但在 C# 中没有支持此方法的好方法)我们使用默认值合并最终值那种类型,如果需要的话。
(您可以简单地修改它以接受接受参数的 lambda,并传入一个值,但您也可以轻松地关闭该参数,所以我认为没有真正的理由这样做。)
还值得指出的是,在 C# 6.0 中,当它实际发布时,我们将有一个实际的 null 传播运算符 (?.
),这使得所有这些都变得非常不必要。你将能够写:
if(a?.b?.c?.d?.e?.f != null)
Console.Write("ok");
并且完全符合您要查找的语义。
当我反序列化 XML 结果为 xsd 生成的对象树并想在该树 a.b.c.d.e.f 中使用一些深层对象时,如果有任何节点,它会给我异常缺少该查询路径。
if(a.b.c.d.e.f != null)
Console.Write("ok");
我想避免像这样检查每个级别的空值:
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 扩展方法:
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) 扩展方法并使用反射来获得如下所示的结果:
if(a.Get("b.c.d.e.f") != null)
Console.Write("ok");
第三个解决方案,可以是实现 ExpandoObject 并使用动态类型来获得如下所示的结果:
dynamic da = new SafeExpando(a);
if(da.b.c.d.e.f != null)
Console.Write("ok");
但是最后两个解决方案没有提供强类型和 IntelliSense 的好处。
我认为最好的可能是第四种可以用表达式树实现的解决方案:
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");
我已经实施了第一个和第二个解决方案。
这是第一个解决方案的样子:
[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;
}
如果可能,如何实施第 4 种解决方案?
如果可能的话,看看如何实施第三个解决方案也很有趣。
因此,起点是创建一个表达式访问者。这让我们可以找到特定表达式中的所有成员访问。这给我们留下了一个问题,即如何为每个成员访问做些什么。
所以第一件事就是递归访问成员被访问的表达式。从那里,我们可以使用 Expression.Condition
创建一个条件块,将处理过的基础表达式与 null
进行比较,如果为真,则 returns 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 而不是任何表达式,并且可以 return 编译委托的人:
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
没有被提升,它也不会匹配。因此,尽管它非常不理想(理想情况下,您永远不会使用不可为 null 的 T
调用此方法,但在 C# 中没有支持此方法的好方法)我们使用默认值合并最终值那种类型,如果需要的话。
(您可以简单地修改它以接受接受参数的 lambda,并传入一个值,但您也可以轻松地关闭该参数,所以我认为没有真正的理由这样做。)
还值得指出的是,在 C# 6.0 中,当它实际发布时,我们将有一个实际的 null 传播运算符 (?.
),这使得所有这些都变得非常不必要。你将能够写:
if(a?.b?.c?.d?.e?.f != null)
Console.Write("ok");
并且完全符合您要查找的语义。