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

Краткая версия (TL;DR):

Предположим, у меня есть выражение, представляющее собой просто цепочку операторов доступа к членам:

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

Вы можете думать об этом выражении как о композиции подвыражения, каждое из которых включает одну операцию доступа к членам:

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

Что я хочу сделать, так это разбить eна эти составные подвыражения, чтобы я мог работать с ними по отдельности.

Еще более короткая версия:

Если у меня есть выражение x => x.foo.bar, я уже знаю, как прервать x => x.foo. Как я могу получить другое подвыражение, foo => foo.bar?

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

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

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

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 => бар.баз.

Таким образом, получается что-то вроде этого (псевдокод):

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

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


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

Монада Maybe

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);.
  3. На самом деле не предназначен для того, что я пытаюсь сделать, и не кажется идеальным.

ExpressionVisitor

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

Плюсы:

  1. Соответствует синтаксису result = target.Lift(x => x.foo.bar.baz)
  2. Отлично работает, если каждый шаг в цепочке возвращает ссылочный тип или ненулевое значение. тип

Минусы:

  1. Он задыхается, если какой-либо элемент в цепочке является типом значения, допускающим значение NULL, что действительно ограничивает его полезность для меня. Мне нужно, чтобы он работал для членов Nullable.

Try/catch

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

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

Плюсы:

  1. Все просто.
  2. Понятно, для чего этот код.
  3. Мне не нужно беспокоиться о пограничных случаях.

Минусы:

  1. Это уродливо и многословно
  2. Блок try/catch является нетривиальным*ударом по производительности
  3. Это блок операторов, поэтому я не могу заставить его генерировать дерево выражений для LINQ
  4. Это похоже на признание поражения

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

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

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

Обновление:

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

22
задан Community 23 May 2017 в 12:16
поделиться