Короткая версия (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. Тем не менее, мне все равно нравится решение для разложения выражений.