Подтвердить что ты не робот

Как разбить цепочку выражений доступа к члену?

Короткая версия (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?

Почему я это делаю:

Я пытаюсь имитировать "подъем" оператора доступа к члену в С#, например оператор экзистенциального доступа CoffeeScript ?.. Эрик Липперт заявил, что аналогичный оператор был рассмотрен для С#,, но для его реализации не было бюджета.

Если бы такой оператор существовал в С#, вы могли бы сделать что-то вроде этого:

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

Если какая-либо часть цепочки target.foo.bar.baz оказалась нулевой, тогда вся эта вещь будет оцениваться как 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);
}

Это было слабо вдохновлено этим ответом.


Альтернативы методу подъема и почему я не могу их использовать:

Возможно, монада

value = x.ToMaybe()
         .Bind(y => y.foo)
         .Bind(f => f.bar)
         .Bind(b => b.baz)
         .Value;
Плюсы: Минусы:
  • Это слишком многословно. Я не хочу, чтобы целая цепочка вызовов функций выполнялась каждый раз, когда я хочу сверлить несколько членов. Даже если я реализую SelectMany и использую синтаксис запроса, IMHO, который будет выглядеть более грязным, не менее.
  • Мне нужно вручную переписать x.foo.bar.baz как свои отдельные компоненты, а это значит, что я должен знать, что они находятся во время компиляции. Я не могу просто использовать выражение из переменной типа result = Lift(expr, obj);.
  • Не предназначен для того, что я пытаюсь сделать, и не чувствую себя идеально подходящим.

ExpressionVisitor

Я изменил метод Ian Griffith LiftMemberAccessToNull в общий метод расширения, который можно использовать, как я описал. Код слишком длинный, чтобы включить сюда, но я опубликую Gist, если кому-то интересно.

Плюсы:
  • Выполняет синтаксис result = target.Lift(x => x.foo.bar.baz)
  • Отлично работает, если каждый шаг в цепочке возвращает ссылочный тип или тип с нулевым значением
Минусы:
  • Он задыхается, если какой-либо член в цепочке является нулевым типом значений, что действительно ограничивает его полезность для меня. Мне нужно, чтобы он работал для Nullable<DateTime> участников.

Try/улов

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

Это самый очевидный способ, и я буду использовать его, если не найду более элегантный способ.

Плюсы:
  • Это просто.
  • Очевидно, что для этого кода.
  • Мне не нужно беспокоиться о случаях краев.
Минусы:
  • Это уродливое и многословное
  • Блок try/catch представляет собой нетривиальный * показатель производительности
  • Это блок операторов, поэтому я не могу заставить его генерировать дерево выражений для LINQ
  • Похоже на признание поражения

Я не собираюсь лгать; "Не признавая поражения" - главная причина, по которой я так упрям. Мои инстинкты говорят, что должен быть элегантный способ сделать это, но найти это было непросто. Я не могу поверить, что так легко получить доступ к левой части выражения, но правая сторона почти недостижима.

У меня действительно есть две проблемы, поэтому я принимаю все, что решает одно:

  • Разложение выражений, которое сохраняет обе стороны, имеет разумную производительность и работает на любом типе
  • Доступ к неограниченному члену доступа

Обновление:

Доступ к неограниченному члену доступа запланирован для , включенного в С# 6.0. Тем не менее, мне все равно нравится решение для разложения выражений.

4b9b3361

Ответ 1

Если это просто простая цепочка выражений доступа к члену, есть простое решение:

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