我如何打破成员访问表达式链? [英] How do I break down a chain of member access expressions?

查看:159
本文介绍了我如何打破成员访问表达式链?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

短版本(TL; DR):



假设我有这只是一个成员访问运营商链的表达式:

 表达式来; Func键< TX,Tbaz>> E = X => x.foo.bar.baz; 

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



<预类=郎-CS prettyprint-覆盖> 表达式来; Func键< TX,Tfoo>> E1 =器(Tx x)的= GT; x.foo; Func键< Tfoo,TBAR>>
表达式来; E2 =(Tfoo富)=> foo.bar; Func键< TBAR,Tbaz>>
表达式来; E3 =(TBAR巴)=> bar.baz;



我想要做的是打破电子下来到这些组件的子表达式,所以我可以单独与他们合作。



的更短的版本:



如果我有表达 X => x.foo.bar ,我已经知道如何断绝 X => x.foo 。我怎样才能拔出其他子表达式,富=> ?foo.bar



为什么我在做这些:



我米试图模拟揭的成员访问运算符在C#中,如的CoffeeScript的存在接入运营商 。埃里克利珀指出,类似的运营商被认为是C#,但没有预算来实现它。



如果这样的运营商在C#中存在,你可以做这样的事情:

 值=目标包含.foo的.bar .baz??; 

如果任何部分的 target.foo.bar.baz 链竟然是空,那么这整个事情将评估为空,从而避免一个NullReferenceException。



我要一个电梯扩展方法,可以模拟这样的事情:

 值= target.Lift(X => x.foo.bar.baz); //返回target.foo.bar.baz或空



我已经试过:



我已经得到的东西,编译,它的排序工作。然而,这是不完全的,因为我只知道如何保持成员访问表达式的左侧。我可以把 X => x.foo.bar.baz X => x.foo.bar ,但我不知道如何保持栏= GT; 。bar.baz



所以,它结束了做这样的事情(伪):

 收益率(X => X)(目标)== NULL?空
:(X => x.foo)(目标)== NULL?空
:(X => x.foo.bar)(目标)== NULL?空
:(X => x.foo.bar.baz)(目标);

这意味着,在表达式中最左边的步骤得到评估一遍又一遍。也许不是一个大问题,如果他们是在POCO的对象只是属性,但把它变成方法调用和效率低下(以及潜在的副作用)成为了很多比较明显的:

  //还是伪
则返回(x => X())(目标)== NULL?空
:(X => X()富()。)(目标)== NULL?空
:(X => X()富()条()。)(目标)== NULL?空
:(目标);(X => X()富()条()巴兹()。)



的代码:



<预类=郎-cs prettyprint-覆盖> 静态TResult电梯< T,TResult>(这件T目标,表达< Func键< T,TResult>> EXP)
其中TResult:类
{
//忽略:如果目标可以是零和放大器;&安培;目标== NULL,只是返回null

VAR memberExpression = exp.Body为MemberExpression;
如果(memberExpression!= NULL)
{
//如果memberExpression是{} x.foo.bar,然后innerExpression为{} x.foo
VAR innerExpression = memberExpression。表达;
VAR innerLambda = Expression.Lambda<&Func键LT; T,对象>>(
innerExpression,
exp.Parameters
);

如果(target.Lift(innerLambda)== NULL)
{
返回NULL;
}
,否则
{
////这是我卡上的部分。可能的伪代码:
// VAR成员= memberExpression.Member;
//返回GetValueOfMember(target.Lift(innerLambda)成员);
}
}

//现在,我坚持这一点:
返回exp.Compile()(目标); //计算器:
}

这是由松散的这个答案






替代一个瘦脸方法,和为什么我不能使用它们:



的单子也许



 值= x.ToMaybe()
.Bind(Y => y.foo)
.Bind(F => f.bar)
.Bind(b = GT; b.baz)
.value的;



优点:


  1. 使用现有格局这是在函数式编程流行

  2. 有其他的用途之外解除成员访问



缺点:$ b​​
$ b

  1. 这太冗长。我不想要的功能,一个巨大的调用链我要钻几个会员下来每一次。即使我实施的SelectMany 并使用查询语法,恕我直言,这将显得更加凌乱,而不是更少。

  2. 我必须手动改写 x.foo.bar.baz 的各个组成部分,这意味着我必须知道他们是在编译时什么。我不能只用一种表情从一个变量如结果=电梯(表达式,则obj);

  3. 没有真正专为我想要做的,不觉得自己是一个完美的结合。



ExpressionVisitor



我修改伊恩·格里菲斯的LiftMemberAccessToNull方法的进入,因为我已经描述了可以使用的通用扩展方法。该代码太长,包括在这里,但如果任何人的兴趣,我会发布一个要点



优点:


  1. 沿用了结果= target.Lift(X => x.foo.bar.baz)语法

  2. 如果伟大工程链中的每一步返回引用类型或一个非空的值类型



缺点:$ b​​
$ b

  1. 这扼流圈如果链中的任何成员是一个空值类型,这确实限制了它的用处我。我需要它来为可空<工作; DateTime的> 成员。



的try / catch



 尝试
{
值= x.foo.bar.baz;
}
赶上(前的NullReferenceException)
{
值= NULL;
}

这是最明显的方式,这就是我会,如果我使用。无法找到一个更优雅的方式。



优点:$ b​​
$ b

  1. 这很简单
  2. $ b。 $ b
  3. 很明显的代码是什么。

  4. 我不担心边界情况。



缺点:$ b​​
$ b

  1. 这是丑陋和详细

  2. 的try / catch块是的平凡* 性能命中

  3. 这是一个语句块,这样我就可以' ŧ使它散发出的表达式树LINQ

  4. 这感觉就像承认失败



我不打算说谎; 不认输是我这么固执的主要原因。我的直觉说,必须有一个优雅的方式来做到这一点,但发现它是一个挑战。 我简直不敢相信它是如此容易地访问表达式的左侧,但是右侧近了不可达。



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




  • 表达分解,保留两侧,有合理的性能,并适用于任何类型

  • 空蔓延成员访问






更新:



空蔓延成员访问的是<击>计划 包含在 C#6.0 。我还是想一个解决方案,以表达分解,虽然。


解决方案

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

 公共静态TResult电梯< T,TResult>(这件T目标,表达< Func键< T,TResult> ;> EXP)
其中TResult:类
{
回报(TResult)GetValueOfExpression(目标,exp.Body);
}

私有静态对象GetValueOfExpression< T>(T目标,表达式exp)
{
如果(exp.NodeType == ExpressionType.Parameter)
{
回报率的目标;
}
,否则如果(exp.NodeType == ExpressionType.MemberAccess)
{
VAR memberExpression =(MemberExpression)EXP;
VAR parentValue = GetValueOfExpression(目标,memberExpression.Expression);

如果(parentValue == NULL)
{
返回NULL;
}
,否则
{
如果(memberExpression.Member是的PropertyInfo)
回报率((的PropertyInfo)memberExpression.Member).GetValue(parentValue,NULL);
,否则
回报率((字段信息)memberExpression.Member).GetValue(parentValue);
}
}
,否则
{
抛出新的ArgumentException(表达式只能包含成员访问的电话。,EXP);
}
}



修改



如果您要添加的支持方法调用,使用这种更新方式:

 私有静态对象GetValueOfExpression< T>(T目标,表达式exp)
{
如果(EXP == NULL)
{
返回NULL;
}
,否则如果(exp.NodeType == ExpressionType.Parameter)
{
回报率的目标;
}
,否则如果(exp.NodeType == ExpressionType.Constant)
{
回报率((常量表达式)EXP).value的;
}
,否则如果(exp.NodeType == ExpressionType.Lambda)
{
返回EXP;
}
,否则如果(exp.NodeType == ExpressionType.MemberAccess)
{
VAR memberExpression =(MemberExpression)EXP;
VAR parentValue = GetValueOfExpression(目标,memberExpression.Expression);

如果(parentValue == NULL)
{
返回NULL;
}
,否则
{
如果(memberExpression.Member是的PropertyInfo)
回报率((的PropertyInfo)memberExpression.Member).GetValue(parentValue,NULL);
,否则
回报率((字段信息)memberExpression.Member).GetValue(parentValue);
}
}
,否则如果(exp.NodeType == ExpressionType.Call)
{
VAR methodCallExpression =(MethodCallExpression)EXP;
VAR parentValue = GetValueOfExpression(目标,methodCallExpression.Object);

如果(parentValue == NULL和放大器;&安培;!methodCallExpression.Method.IsStatic)
{
返回NULL;
}
,否则
{
变种参数= methodCallExpression.Arguments.Select(一个= GT; GetValueOfExpression(靶,))ToArray的();

//所需的comverting表达参数,以委托调用
VAR参数= methodCallExpression.Method.GetParameters();
的for(int i = 0; I< parameters.Length;我++)
{
如果(typeof运算(代表).IsAssignableFrom(参数[I] .ParameterType))
{
参数由[i] =((LambdaExpression)参数[I])编译();
}
}

如果(的arguments.length大于0&放大器;&放大器;参数[0] == NULL&放大器;&放大器; methodCallExpression.Method.IsStatic&放大器;&放大器;
methodCallExpression.Method.IsDefined(typeof运算(ExtensionAttribute),FALSE))//扩展方法
{
返回NULL;
}
,否则
{
返回methodCallExpression.Method.Invoke(parentValue,参数);
}
}
}
,否则
{
抛出新的ArgumentException(
的String.Format(表达式类型{0}无效的成员调用,exp.NodeType))。
}
}


The Short Version (TL;DR):

Suppose I have an expression that's just a chain of member access operators:

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

You can think of this expression as a composition of sub-expressions, each comprising one member-access operation:

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;

What I want to do is break e down into these component sub-expressions so I can work with them individually.

The Even Shorter Version:

If I have the expression x => x.foo.bar, I already know how to break off x => x.foo. How can I pull out the other sub-expression, foo => foo.bar?

Why I'm Doing This:

I'm trying to simulate "lifting" the member access operator in C#, like CoffeeScript's existential access operator ?.. Eric Lippert has stated that a similar operator was considered for C#, but there was no budget to implement it.

If such an operator existed in C#, you could do something like this:

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

If any part of the target.foo.bar.baz chain turned out to be null, then this whole thing would evaluate to null, thus avoiding a NullReferenceException.

I want a Lift extension method that can simulate this sort of thing:

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

What I've Tried:

I've got something that compiles, and it sort of works. However, it's incomplete because I only know how to keep the left side of a member access expression. I can turn x => x.foo.bar.baz into x => x.foo.bar, but I don't know how to keep bar => bar.baz.

So it ends up doing something like this (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);

This means that the leftmost steps in the expression get evaluated over and over again. Maybe not a big deal if they're just properties on POCO objects, but turn them into method calls and the inefficiency (and potential side effects) become a lot more obvious:

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

The Code:

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 was loosely inspired by this answer.


Alternatives to a Lift Method, and Why I Can't Use Them:

The Maybe monad

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

Pros:

  1. Uses an existing pattern that's popular in functional programming
  2. Has other uses besides lifted member access

Cons:

  1. It's too verbose. I don't want a massive chain of function calls every time I want to drill a few members down. Even if I implement SelectMany and use the query syntax, IMHO that will look more messy, not less.
  2. I have to manually rewrite x.foo.bar.baz as its individual components, which means I have to know what they are at compile time. I can't just use an expression from a variable like result = Lift(expr, obj);.
  3. Not really designed for what I'm trying to do, and doesn't feel like a perfect fit.

ExpressionVisitor

I modified Ian Griffith's LiftMemberAccessToNull method into a generic extension method that can be used as I've described. The code is too long to include here, but I'll post a Gist if anyone's interested.

Pros:

  1. Follows the result = target.Lift(x => x.foo.bar.baz) syntax
  2. Works great if every step in the chain returns a reference type or a non-nullable value type

Cons:

  1. It chokes if any member in the chain is a nullable value type, which really limits its usefulness to me. I need it to work for Nullable<DateTime> members.

Try/catch

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

This is the most obvious way, and it's what I'll use if I can't find a more elegant way.

Pros:

  1. It's simple.
  2. It's obvious what the code is for.
  3. I don't have to worry about edge cases.

Cons:

  1. It's ugly and verbose
  2. The try/catch block is a nontrivial* performance hit
  3. It's a statement block, so I can't make it emit an expression tree for LINQ
  4. It feels like admitting defeat

I'm not going to lie; "not admitting defeat" is the main reason I'm being so stubborn. My instincts say there must be an elegant way to do this, but finding it has been a challenge. I can't believe it's so easy to access the left side of an expression, yet the right side is nigh-unreachable.

I really have two problems here, so I'll accept anything that solves either one:

  • Expression decomposition that preserves both sides, has reasonable performance, and works on any type
  • Null-propagating member access

Update:

Null-propagating member access is planned for included in C# 6.0. I'd still like a solution to expression decomposition, though.

解决方案

If it's just a simple chain of member access expressions, there is an easy solution:

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

EDIT

If you want to add support for method calls, use this updated method:

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

这篇关于我如何打破成员访问表达式链?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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