短版(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
?
为什么我要这样做:
我正在尝试在 C# 中模拟“提升”成员访问运算符,例如CoffeeScript 的存在访问运算符?.
。Eric Lippert 曾表示曾考虑为 C# 使用类似的运算符,但没有预算来实施它。
如果 C# 中存在这样的运算符,您可以执行以下操作:
value = target?.foo?.bar?.baz;
如果链的任何部分target.foo.bar.baz
结果为 null,那么整个事情将评估为 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
并使用查询语法,恕我直言,这看起来会更混乱,而不是更少。 - 我必须手动将
x.foo.bar.baz
其重写为各个组件,这意味着我必须在编译时知道它们是什么。我不能只使用来自变量的表达式,例如result = Lift(expr, obj);
. - 不是为我想要做的事情而设计的,而且感觉不是完美的契合。
表达访客
我将 Ian Griffith 的 LiftMemberAccessToNull 方法修改为通用扩展方法,可以按照我的描述使用。代码太长,无法在此处包含,但如果有人感兴趣,我会发布 Gist。
优点:- 遵循
result = target.Lift(x => x.foo.bar.baz)
语法 - 如果链中的每个步骤都返回引用类型或不可为空的值类型,则效果很好
- 如果链中的任何成员是可为空的值类型,它就会窒息,这确实限制了它对我的用处。我需要它为
Nullable<DateTime>
会员工作。
试着抓
try
{
value = x.foo.bar.baz;
}
catch (NullReferenceException ex)
{
value = null;
}
这是最明显的方式,如果找不到更优雅的方式,我会使用它。
优点:- 这很简单。
- 代码的用途很明显。
- 我不必担心边缘情况。
- 它丑陋而冗长
- try/catch 块是一个不平凡的*性能影响
- 这是一个语句块,所以我不能让它为 LINQ 发出表达式树
- 感觉像是认输
我不会说谎;“不认输”是我这么固执的主要原因。我的直觉认为必须有一种优雅的方式来做到这一点,但找到它一直是一个挑战。我不敢相信访问表达式的左侧如此容易,但右侧却几乎无法访问。
我在这里真的有两个问题,所以我会接受任何可以解决任何一个问题的方法:
- 保留双方的表达式分解,具有合理的性能,适用于任何类型
- 空传播成员访问
更新:
空传播成员访问计划包含 在C# 6.0中。不过,我仍然想要表达式分解的解决方案。