如何分解成员访问表达式链?

短版(TL; DR):

假设我有一个表达式,它只是一个成员访问运算符链:

Expression<Func> e = x => x.foo.bar.baz; 

您可以将此表达式视为子表达式的组合,每个子表达式包含一个成员访问操作:

 Expression<Func> e1 = (Tx x) => x.foo; Expression<Func> e2 = (Tfoo foo) => foo.bar; Expression<Func> e3 = (Tbar bar) => bar.baz; 

我想要做的是将这些组件子表达式分解,以便我可以单独使用它们。

更短的版本:

如果我有表达式x => x.foo.bar ,我已经知道如何中断x => x.foo 。 如何拉出另一个子表达式foo => foo.bar

为什么我这样做:

我试图在C#中模拟“提升”成员访问操作符,就像CoffeeScript的存在访问操作符一样?. 。 Eric Lippert表示, 类似的运营商被考虑用于C#,但没有预算来实施它。

如果这样的运算符存在于C#中,您可以执行以下操作:

 value = target?.foo?.bar?.baz; 

如果target.foo.bar.baz链的任何部分结果为null,那么整个事情将评估为null,从而避免NullReferenceException。

我想要一个可以模拟这种事情的Lift扩展方法:

 value = target.Lift(x => x.foo.bar.baz); //returns target.foo.bar.baz or null 

我试过的:

我有一些编译的东西,它有点工作。 但是,它不完整,因为我只知道如何保留成员访问表达式的左侧。 我可以将x => x.foo.bar.baz变成x => x.foo.bar ,但我不知道如何保持bar => bar.baz

所以它最终会做这样的事情(伪代码):

 return (x => x)(target) == null ? null : (x => x.foo)(target) == null ? null : (x => x.foo.bar)(target) == null ? null : (x => x.foo.bar.baz)(target); 

这意味着表达式中最左边的步骤会一遍又一遍地进行评估。 如果他们只是POCO对象的属性,可能不是什么大不了的事,但是把它们变成方法调用,效率低下(和潜在的副作用)变得更加明显:

 //still pseudocode return (x => x())(target) == null ? null : (x => x().foo())(target) == null ? null : (x => x().foo().bar())(target) == null ? null : (x => x().foo().bar().baz())(target); 

代码:

 static TResult Lift(this T target, Expression<Func> exp) where TResult : class { //omitted: if target can be null && target == null, just return null var memberExpression = exp.Body as MemberExpression; if (memberExpression != null) { //if memberExpression is {x.foo.bar}, then innerExpression is {x.foo} var innerExpression = memberExpression.Expression; var innerLambda = Expression.Lambda<Func>( innerExpression, exp.Parameters ); if (target.Lift(innerLambda) == null) { return null; } else { ////This is the part I'm stuck on. Possible pseudocode: //var member = memberExpression.Member; //return GetValueOfMember(target.Lift(innerLambda), member); } } //For now, I'm stuck with this: return exp.Compile()(target); } 

这个答案松散地启发了这一点 。


提升方法的替代方案,以及为什么我不能使用它们:

可能的monad

 value = x.ToMaybe() .Bind(y => y.foo) .Bind(f => f.bar) .Bind(b => b.baz) .Value; 

优点:

  1. 使用在函数式编程中流行的现有模式
  2. 除了解除成员访问之外还有其他用途

缺点:

  1. 它太冗长了。 每次我想钻几个成员时,我都不想要大量的函数调用。 即使我实现SelectMany并使用查询语法,恕我直言,看起来会更混乱,而不是更少。
  2. 我必须手动重写x.foo.bar.baz作为其各自的组件,这意味着我必须知道它们在编译时是什么。 我不能只使用像result = Lift(expr, obj);这样的变量的result = Lift(expr, obj);
  3. 不是真正的设计,我想要做的,并不觉得完美契合。

ExpressionVisitor

我将Ian Griffith的LiftMemberAccessToNull方法修改为一个可以像我所描述的那样使用的通用扩展方法。 代码太长了,不能包括在这里,但如果有人感兴趣,我会发布一个Gist。

优点:

  1. 遵循result = target.Lift(x => x.foo.bar.baz)语法
  2. 如果链中的每个步骤都返回引用类型或不可为空的值类型,则效果很好

缺点:

  1. 如果链中的任何成员是可以为空的值类型,它会窒息,这实际上限制了它对我的用处。 我需要它为Nullable成员工作。

试着抓

 try { value = x.foo.bar.baz; } catch (NullReferenceException ex) { value = null; } 

这是最明显的方式,如果我找不到更优雅的方式,我就会使用它。

优点:

  1. 这很简单。
  2. 很明显代码是什么。
  3. 我不必担心边缘情况。

缺点:

  1. 这是丑陋和冗长的
  2. try / catch块是一个非常重要的*性能命中
  3. 这是一个语句块,所以我不能让它为LINQ发出一个表达式树
  4. 感觉就像承认失败一样

我不会撒谎; “不承认失败”是我如此顽固的主要原因。 我的直觉说必须有一种优雅的方式来做到这一点,但找到它一直是一个挑战。 我无法相信它可以轻松访问表达式的左侧,但右侧几乎无法访问。

我真的有两个问题,所以我会接受任何解决其中任何一个的问题:

  • 保留双方的表达式分解具有合理的性能,适用于任何类型
  • 空传播成员访问

更新:

计划 包含在 C#6.0中的空传播成员访问。 不过,我仍然喜欢表达式分解的解决方案。

如果它只是一个简单的成员访问表达式链,那么有一个简单的解决方案:

 public static TResult Lift(this T target, Expression> exp) where TResult : class { return (TResult) GetValueOfExpression(target, exp.Body); } private static object GetValueOfExpression(T target, Expression exp) { if (exp.NodeType == ExpressionType.Parameter) { return target; } else if (exp.NodeType == ExpressionType.MemberAccess) { var memberExpression = (MemberExpression) exp; var parentValue = GetValueOfExpression(target, memberExpression.Expression); if (parentValue == null) { return null; } else { if (memberExpression.Member is PropertyInfo) return ((PropertyInfo) memberExpression.Member).GetValue(parentValue, null); else return ((FieldInfo) memberExpression.Member).GetValue(parentValue); } } else { throw new ArgumentException("The expression must contain only member access calls.", "exp"); } } 

编辑

如果要添加对方法调用的支持,请使用此更新方法:

 private static object GetValueOfExpression(T target, Expression exp) { if (exp == null) { return null; } else if (exp.NodeType == ExpressionType.Parameter) { return target; } else if (exp.NodeType == ExpressionType.Constant) { return ((ConstantExpression) exp).Value; } else if (exp.NodeType == ExpressionType.Lambda) { return exp; } else if (exp.NodeType == ExpressionType.MemberAccess) { var memberExpression = (MemberExpression) exp; var parentValue = GetValueOfExpression(target, memberExpression.Expression); if (parentValue == null) { return null; } else { if (memberExpression.Member is PropertyInfo) return ((PropertyInfo) memberExpression.Member).GetValue(parentValue, null); else return ((FieldInfo) memberExpression.Member).GetValue(parentValue); } } else if (exp.NodeType == ExpressionType.Call) { var methodCallExpression = (MethodCallExpression) exp; var parentValue = GetValueOfExpression(target, methodCallExpression.Object); if (parentValue == null && !methodCallExpression.Method.IsStatic) { return null; } else { var arguments = methodCallExpression.Arguments.Select(a => GetValueOfExpression(target, a)).ToArray(); // Required for comverting expression parameters to delegate calls var parameters = methodCallExpression.Method.GetParameters(); for (int i = 0; i < parameters.Length; i++) { if (typeof(Delegate).IsAssignableFrom(parameters[i].ParameterType)) { arguments[i] = ((LambdaExpression) arguments[i]).Compile(); } } if (arguments.Length > 0 && arguments[0] == null && methodCallExpression.Method.IsStatic && methodCallExpression.Method.IsDefined(typeof(ExtensionAttribute), false)) // extension method { return null; } else { return methodCallExpression.Method.Invoke(parentValue, arguments); } } } else { throw new ArgumentException( string.Format("Expression type '{0}' is invalid for member invoking.", exp.NodeType)); } }