使用Expression调用属性和对象,并确定对象是否为null

时间:2012-04-10 12:22:17

标签: c# lambda expression

我希望能够在可能为null的对象上调用属性,但在调用时不必显式检查它们是否为null。

像这样:

var something = someObjectThatMightBeNull.Property;

我的想法是创建一个采用Expression的方法,如下所示:

var something = GetValueSafe(() => someObjectThatMightBeNull.Property);

TResult? GetValueSafe<TResult>(Expression<Func<TResult>> expression) 
    where TResult : struct
{
    // what must I do?
}

我需要做的是检查表达式并确定someObjectThatMightBeNull是否为空。我该怎么做?

如果有任何更聪明的方式,我也会很感激。

谢谢!

2 个答案:

答案 0 :(得分:3)

您所谈论的内容称为无效安全解除引用 - 此SO专门提出了这样一个问题:C# if-null-then-null expression

表达式并不是真正的答案(请参阅下文,以澄清我对该陈述的理由)。但是,这种扩展方法可能是:

public static TResult? GetValueSafe<TInstance, TResult>(this TInstance instance,
  Func<TInstance, TResult> accessor)
  where TInstance : class
  where TResult : struct
{  
   return instance != null ? (TResult?)accessor(instance) : (TResult?)null;
}

现在你可以这样做:

MyObject o = null;
int? i = o.GetValueSafe(obj => obj.SomeIntProperty);

Assert.IsNull(i);    

显然,当属性是结构时,这是最有用的;您可以缩减为任何类型,只需使用default(TResult) - 但是您可以获得0的整数,双打等:

public static TResult GetValueSafe<TInstance, TResult>(this TInstance instance,
  Func<TInstance, TResult> accessor, TResult def = default(TResult))
  where TInstance : class
{  
   return instance != null ? accessor(instance) : def;
}

第二个版本更有用,因为它适用于任何TResult。我使用可选参数进行了扩展,以允许调用者提供默认值,例如(使用前一代码中的o):

int i = o.GetValueSafe(obj => obj.SomeIntProperty); //yields 0
i = o.GetValueSafe(obj => obj.SomeIntProperty, -1); //yields -1

//while this yields string.Empty instead of null
string s = o.GetValueSafe(obj => obj.SomeStringProperty, string.Empty);

编辑 - 回应David的评论

大卫建议我的答案是错误的,因为它没有提供基于表达式的解决方案,这就是所要求的。我的观点是,对SO的任何真正正确且负责任的答案应该总是试图为提出问题的人寻求一个更简单的解决方案。我相信人们普遍认为,在我们的日常职业生涯中应该避免过于复杂的解决方案,以避免出现其他简单的问题。而SO只是因为它的社区行为方式一样受欢迎。

大卫还对我的不合理的陈述提出异议,即他们不是解决方案&#39; - 所以我现在要扩展它,并说明为什么基于表达式的解决方案基本上没有意义,除了在一个罕见的边缘情况下OP没有实际要求(顺便提一下,David&# 39;答案也没有涵盖。)

具有讽刺意味的是,它使得这个答案本身可能不必要地复杂化了:) 如果你真的不关心为什么表达不是这样的话,你可以放心地从这里忽略。最好的路线

虽然说可以用表达式来解决这个问题是正确的,但对于问题中列出的例子,没有理由使用它们 - 它过于复杂了最终是一个非常简单的问题;并且在运行时编译表达式的开销(并随后将其丢弃,除非你将缓存放入其中,除非你发出类似呼叫站点之类的东西,比如DLR使用,这将变得非常棘手)与解决方案相比是巨大的我在这里。

最终,任何解决方案的动机都是尽量将调用者所需的工作保持在最低限度,但与此同时,您还需要将表达式分析器完成的工作保持在最低限度。否则,如果没有大量工作,解决方案几乎无法解决。为了说明我的观点 - 让我们看一下我们可以用一个带有表达式的静态方法实现的最简单的方法,给定我们的对象o

var i = GetValueSafe(obj => obj.SomeIntProperty);

哦,哦,那个表达实际上并没有做任何事情 - 因为它没有传递 o给它 - 表达本身对我们来说没用,因为我们需要o 的实际引用,可能是null。所以 - 第一个解决方案,当然是明确传递引用:

var i = GetValueSafe(o, obj => obj.SomeIntProperty);

(注意 - 也可以写为扩展方法)

因此,静态方法的工作是获取第一个参数,并在调用它时将其传递给已编译的表达式。这也有助于识别寻求其属性的表达式的类型。但是,首先完全取消使用表达式的原因;因为方法本身可以立即决定是否访问该属性 - 因为它具有对可能是null的对象的引用。因此,在这种情况下,它更容易,更简单,更快简单地传递引用和访问者委托(而不是表达式),因为我的扩展方法

正如我所提到的,有一种方法可以绕过实例,这就是执行以下操作之一:

var i = GetValueSafe(obj => o.SomeIntProperty);

或者

var i = GetValueSafe(() => o.SomeIntProperty);

我们正在对扩展方法版本进行折扣 - 因为我们得到了一个传递给方法的引用,一旦我们得到一个引用,我们就可以废除表达式,正如我的最后一点所证明的那样。

在这里,我们依赖于调用者来理解他们必须包含一个表达式,该表达式表示正文中的实际实例(无论是范围内的属性或字段或局部变量)表达式,在成员读取的左侧,以便我们实际上可以从中获取具体值以进行空检查。

首先,这不是表达式参数的自然使用,所以我相信你的调用者可能会感到困惑。还有另外一个问题,如果你想打算使用它,我认为这将是一个杀手 - 你无法缓存这些表达式,因为每次实例,其中都是&#39; null-ness&#39;你想要回避,被烘焙到传递的表达中。这意味着您总是必须为每个调用重新编译表达式;那将是真的慢。如果您在表达式中对参数进行参数化,则可以对其进行缓存 - 但最终我们会得到第一个需要传递实例的解决方案;而且我已经在那里展示过我们可以只使用代表了!

使用ExpressionVisitor类相对容易 - 编写可以将所有属性/字段读取(以及方法调用)转换为“安全”的内容。你喜欢的电话。但是,除非您打算对此类内容进行安全阅读,否则我无法看到这样做的任何好处:a.b.c.d。但是随后将值类型增加到可自由的版本本身会让你在表达式树重写中遇到很多麻烦,我可以告诉你;留下几乎没有人会理解的解决方案:)

答案 1 :(得分:3)

这很复杂,但它可以完成,而不会留下“表达土地”:

// Get the initial property expression from the left 
// side of the initial lambda. (someObjectThatMightBeNull.Property)
var propertyCall = (MemberExpression)expression.Body;

// Next, remove the property, by calling the Expression 
// property from the MemberExpression (someObjectThatMightBeNull)
var initialObjectExpression = propertyCall.Expression;

// Next, create a null constant expression, which will 
// be used to compare against the initialObjectExpression (null)
var nullExpression = Expression.Constant(null, initialObjectExpression.Type);

// Next, create an expression comparing the two: 
// (someObjectThatMightBeNull == null)
var equalityCheck = Expression.Equal(initialObjectExpression, nullExpression);

// Next, create a lambda expression, so the equalityCheck 
// can actually be called ( () => someObjectThatMightBeNull == null )
var newLambda = Expression.Lambda<Func<bool>>(equalityCheck, null);

// Compile the expression. 
var function = newLambda.Compile();

// Run the compiled delegate. 
var isNull = function();

话虽如此,正如安德拉斯·佐尔坦在评论中雄辩地说:“只是因为你的意思并不意味着你应该这样做。”确保你有充分的理由这样做。如果有更好的方法,那就去做吧。安德拉斯有一个很好的解决方法。