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

时间:2012-06-19 19:45:38

标签: c# lambda expression-trees nullreferenceexception lifting

短版本(TL; DR):

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

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

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

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

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

更短的版本:

如果我有x => x.foo.bar这个词,我已经知道如何中断x => x.foo。如何提取其他子表达式foo => foo.bar

为什么我这样做:

我正在尝试模拟“提升”C#中的成员访问运算符,例如CoffeeScript's existential access operator ?.。 Eric Lippert表示a similar operator was considered for 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<T, TResult>(this T target, Expression<Func<T, TResult>> 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<T, object>>(
                              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);
}

这是this answer的灵感。


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

The Maybe monad

value = x.ToMaybe()
         .Bind(y => y.foo)
         .Bind(f => f.bar)
         .Bind(b => b.baz)
         .Value;
优点:
  1. 使用在函数式编程中流行的现有模式
  2. Has other uses除了提升会员访问权限
  3. 缺点:
    1. 太冗长了。每次我想要钻几个成员时,我都不想要大量的函数调用。即使我实现SelectMany并使用查询语法,恕我直言也会更加混乱,而不是更少。
    2. 我必须手动重写x.foo.bar.baz作为其各个组件,这意味着我必须知道它们在编译时是什么。我不能只使用像result = Lift(expr, obj);这样的变量的表达式。
    3. 并非真正为我想做的事而设计,并且感觉不是一个完美的契合。
    4. ExpressionVisitor

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

      优点:
      1. 遵循result = target.Lift(x => x.foo.bar.baz)语法
      2. 如果链中的每个步骤都返回引用类型或不可为空的值类型
      3. ,则效果很好 缺点:
        1. 如果链中的任何成员是可以为空的值类型,它会窒息,这实际上限制了它对我的用处。我需要它为Nullable<DateTime>成员工作。
        2. 尝试/捕获

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

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

          优点:
          1. 很简单。
          2. 代码的用途很明显。
          3. 我不必担心边缘情况。
          4. 缺点:
            1. 这是丑陋而冗长的
            2. try / catch块的效果为nontrivial*
            3. 这是一个语句块,所以我不能让它为LINQ
            4. 发出一个表达式树
            5. 感觉就像承认失败
            6. 我不会撒谎; “不承认失败”是我如此顽固的主要原因。我的直觉说必须有一种优雅的方式来做到这一点,但找到它一直是一个挑战。 我无法相信它可以轻松访问表达式的左侧,但右侧几乎无法访问。

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

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

              更新

              空传播成员访问is planned for included in C# 6.0。不过,我仍然喜欢表达式分解的解决方案。

1 个答案:

答案 0 :(得分:8)

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

public static TResult Lift<T, TResult>(this T target, Expression<Func<T, TResult>> exp)
    where TResult : class
{
    return (TResult) GetValueOfExpression(target, exp.Body);
}

private static object GetValueOfExpression<T>(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>(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));
    }
}