3

我希望能够调用可能为空但未明确地在调用时检查它们是否为空的对象的属性。

像这样:

var something = someObjectThatMightBeNull.Property;

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

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

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

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

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

谢谢!

4

2 回答 2

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 之所以如此受欢迎,是因为它的社区行为方式相同。

David 还对我的“它们不是解决方案”的不合理陈述提出了异议——所以我现在将对此进行扩展,并说明为什么基于表达式的解决方案在很大程度上是毫无意义的,除非在 OP 的罕见边缘情况下实际上并没有要求(顺便说一句,大卫的回答也没有涵盖)。

具有讽刺意味的是,它本身可能使这个答案变得不必要地复杂:) 如果您实际上并不关心为什么表达式不是最佳途径,那么您可以从这里开始安全地忽略

虽然说你可以用表达式解决这个问题是正确的,但对于问题中列出的例子来说,根本没有理由使用它们——它使最终非常简单的问题过于复杂;并且在运行时编译表达式的开销(然后将其丢弃,除非您放入缓存,除非您发出诸如调用站点之类的东西,如 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);

我们正在打折扩展方法版本——因为这样我们得到了一个传递给方法的引用,并且一旦我们得到一个引用,我们就可以取消表达式,正如我最后一点所证明的那样。

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

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

使用ExpressionVisitor类编写一些可以将所有属性/字段读取(以及与此相关的方法调用)转换为您想要的“安全”调用的东西相对容易。但是,除非您打算对以下内容进行安全阅读,否则我看不出这样做有什么好处:a.b.c.d. 但是随后将值类型扩充为自身的可空版本将在表达式树重写中给您带来一些麻烦,我可以告诉您;留下一个几乎没有人会理解的解决方案:)

于 2012-04-10T12:28:58.643 回答
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();

That being said, as Andras Zoltan has so eloquently put in the comments: "Just because you can doesn't mean you should." Make sure you have a good reason to do this. If there's a better way to, then do that instead. Andras has a great workaround.

于 2012-04-10T12:38:12.423 回答