Как разбить цепочку выражений доступа к члену?
Короткая версия (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. Тем не менее, мне все равно нравится решение для разложения выражений.
Ответы
Ответ 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));
}
}