深空检查,还有更好的方法吗?

注: 这个问题是在介绍 C # 6/Visual Studio 2015中的 .?操作符之前提出的。

我们都经历过这种情况,我们有一些深度属性,比如 cake.rosting.berry es.loader,我们需要检查它是否为 null,因此没有异常。这样做的方法是使用 short-Circuit iting if 语句

if (cake != null && cake.frosting != null && cake.frosting.berries != null) ...

这并不完全优雅,也许应该有一种更简单的方法来检查整个链,并查看它是否遇到空变量/属性。

是否有可能使用某种扩展方法,或者它是一种语言特性,或者它只是一个坏主意?

25143 次浏览

除了违反得墨忒耳定律之外,正如 Mehrdad Afshari 已经指出的那样,在我看来,决策逻辑需要“深空检查”。

当您希望用默认值替换空对象时,这种情况最为常见。在这种情况下,您应该考虑实现 空对象模式。它充当真实对象的替身,提供默认值和“非操作”方法。

更新: 从 Visual Studio 2015开始,C # 编译器(语言版本6)现在可以识别 ?.操作符,这使得“深空检查”变得轻而易举。详情请参阅 这个答案

除了重新设计代码,比如 这个被删除的答案暗示, 另一个(尽管很糟糕)选项是使用 try…catch块来查看在深度属性查找期间是否发生了 NullReferenceException

try
{
var x = cake.frosting.berries.loader;
...
}
catch (NullReferenceException ex)
{
// either one of cake, frosting, or berries was null
...
}

我个人不会这么做,原因如下:

  • 看起来不太好。
  • 它使用异常处理,这应该针对异常情况,而不是您期望在正常操作过程中经常发生的情况。
  • 可能永远不应该显式地捕获 NullReferenceException

那么,是否有可能使用某种扩展方法,或者它是一种语言特性,[ ... ]

这几乎可以肯定是一个语言特性(在 C # 6中以 .??[]操作符的形式提供) ,除非 C # 已经有了更复杂的惰性计算,或者除非你想使用反射(出于性能和类型安全的原因,这可能也不是一个好主意)。

因为没有办法简单地将 cake.frosting.berries.loader传递给函数(它将被计算并抛出一个空引用异常) ,所以你必须用以下方式实现一个通用的查找方法: 它接受一个对象和属性名来查找:

static object LookupProperty( object startingPoint, params string[] lookupChain )
{
// 1. if 'startingPoint' is null, return null, or throw an exception.
// 2. recursively look up one property/field after the other from 'lookupChain',
//    using reflection.
// 3. if one lookup is not possible, return null, or throw an exception.
// 3. return the last property/field's value.
}


...


var x = LookupProperty( cake, "frosting", "berries", "loader" );

(注: 代码已编辑)

您很快就会发现这种方法存在一些问题。首先,您不会得到任何类型安全以及简单类型的属性值可能的装箱。其次,如果出现错误,您可以返回 null,并且必须在调用函数中检查这一点,或者抛出异常,然后返回到开始的位置。第三,它可能会很慢。第四,它看起来比开始时更丑陋。

还是只是个坏主意?

我要么选择:

if (cake != null && cake.frosting != null && ...) ...

或者用 Mehrdad Afshari 的上述答案。


PS: 回到我写这个答案的时候,我显然没有考虑 lambda 函数的表达式树; 参见@driis 的这个方向的解答。它也基于一种反射,因此可能不会像更简单的解决方案(if (… != null & … != null) …)那样执行得那么好,但是从语法的角度来看,它可能更好。

我发现这个扩展对于深度嵌套场景非常有用。

public static R Coal<T, R>(this T obj, Func<T, R> f)
where T : class
{
return obj != null ? f(obj) : default(R);
}

这是我从 C # 和 T-SQL 中的 null 合并运算符得到的一个想法。好处是返回类型总是内部属性的返回类型。

这样你就可以做到这一点:

var berries = cake.Coal(x => x.frosting).Coal(x => x.berries);

... 或者是上面的一个小小的变化:

var berries = cake.Coal(x => x.frosting, x => x.berries);

这不是我知道的最好的语法,但它确实有用。

我们考虑过增加一个新的行动”?."到具有您想要的语义的语言。(现在已经添加了; 见下文。)你会说

cake?.frosting?.berries?.loader

编译器会为你生成所有短路检查。

它没有为 C # 4制定标准,也许是为了该语言未来的一个假设版本。

更新(2014年) : ?.操作符现在是下一个 Roslyn 编译器版本的 计划好的。请注意,对于操作符的确切句法和语义分析仍然存在一些争议。

更新(2015年7月) : Visual Studio 2015已经发布,并附带了一个支持 空条件运算符 ABC0和 ?[]的 C # 编译器。

我受到这个问题的启发,试图找出如何使用表达式树的更简单/更漂亮的语法来完成这种深空检查。虽然我同意这样的回答,即如果您经常需要访问层次结构深处的实例,那么 也许吧是一个糟糕的设计,但是我也认为在某些情况下,例如数据表示,它可能非常有用。

所以我创建了一个扩展方法,它允许你写:

var berries = cake.IfNotNull(c => c.Frosting.Berries);

如果表达式的任何部分都不为空,则返回浆果。如果遇到 null,则返回 null。但是有一些警告,在当前版本中,它只能在简单的成员访问中工作,而且只能在。NETFramework4,因为它使用 MemberExpression。更新方法,这在 v4中是新的。这是 IfNotNull 扩展方法的代码:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;


namespace dr.IfNotNullOperator.PoC
{
public static class ObjectExtensions
{
public static TResult IfNotNull<TArg,TResult>(this TArg arg, Expression<Func<TArg,TResult>> expression)
{
if (expression == null)
throw new ArgumentNullException("expression");


if (ReferenceEquals(arg, null))
return default(TResult);


var stack = new Stack<MemberExpression>();
var expr = expression.Body as MemberExpression;
while(expr != null)
{
stack.Push(expr);
expr = expr.Expression as MemberExpression;
}


if (stack.Count == 0 || !(stack.Peek().Expression is ParameterExpression))
throw new ApplicationException(String.Format("The expression '{0}' contains unsupported constructs.",
expression));


object a = arg;
while(stack.Count > 0)
{
expr = stack.Pop();
var p = expr.Expression as ParameterExpression;
if (p == null)
{
p = Expression.Parameter(a.GetType(), "x");
expr = expr.Update(p);
}
var lambda = Expression.Lambda(expr, p);
Delegate t = lambda.Compile();
a = t.DynamicInvoke(a);
if (ReferenceEquals(a, null))
return default(TResult);
}


return (TResult)a;
}
}
}

它通过检查表示表达式的表达式树,并一个接一个地计算各个部分来工作; 每次检查结果是否为空。

我确信这可以扩展,以便支持 MemberExpression 以外的其他表达式。请将此视为概念验证代码,并且请记住使用它会带来性能损失(这在许多情况下可能无关紧要,但是不要在紧凑循环中使用它: ——)

一种选择是使用 Null 对象模式,这样当你没有蛋糕的时候就不会使用 Null,而是使用一个返回 NullFosted 的 NullCake。对不起,我不太擅长解释这个,但其他人可以

我昨晚发布了这个,然后一个朋友给我指出了这个问题。希望能有帮助。然后你可以这样做:

var color = Dis.OrDat<string>(() => cake.frosting.berries.color, "blue");




using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;


namespace DeepNullCoalescence
{
public static class Dis
{
public static T OrDat<T>(Expression<Func><T>> expr, T dat)
{
try
{
var func = expr.Compile();
var result = func.Invoke();
return result ?? dat; //now we can coalesce
}
catch (NullReferenceException)
{
return dat;
}
}
}
}

读读 完整的博客文章请点击这里

同一个朋友也建议你 看这个

虽然德里斯的答案很有趣,但我认为它的性能有点太昂贵了。与编译许多委托相比,我更愿意为每个属性路径编译一个 lambda,缓存它,然后重新调用许多类型。

下面的 NullCoalesce 就是这样做的,它返回一个新的 lambda 表达式,检查为 null,如果任何路径为 null,则返回 default (TResult)。

例如:

NullCoalesce((Process p) => p.StartInfo.FileName)

将返回一个表达式

(Process p) => (p != null && p.StartInfo != null ? p.StartInfo.FileName : default(string));

密码:

    static void Main(string[] args)
{
var converted = NullCoalesce((MethodInfo p) => p.DeclaringType.Assembly.Evidence.Locked);
var converted2 = NullCoalesce((string[] s) => s.Length);
}


private static Expression<Func<TSource, TResult>> NullCoalesce<TSource, TResult>(Expression<Func<TSource, TResult>> lambdaExpression)
{
var test = GetTest(lambdaExpression.Body);
if (test != null)
{
return Expression.Lambda<Func<TSource, TResult>>(
Expression.Condition(
test,
lambdaExpression.Body,
Expression.Default(
typeof(TResult)
)
),
lambdaExpression.Parameters
);
}
return lambdaExpression;
}


private static Expression GetTest(Expression expression)
{
Expression container;
switch (expression.NodeType)
{
case ExpressionType.ArrayLength:
container = ((UnaryExpression)expression).Operand;
break;
case ExpressionType.MemberAccess:
if ((container = ((MemberExpression)expression).Expression) == null)
{
return null;
}
break;
default:
return null;
}
var baseTest = GetTest(container);
if (!container.Type.IsValueType)
{
var containerNotNull = Expression.NotEqual(
container,
Expression.Default(
container.Type
)
);
return (baseTest == null ?
containerNotNull :
Expression.AndAlso(
baseTest,
containerNotNull
)
);
}
return baseTest;
}

我也经常希望语法更简单!当您有可能为 null 的 method-return-value 时,情况会变得特别糟糕,因为这样就需要额外的变量(例如: cake.frosting.flavors.FirstOrDefault().loader)

然而,我使用了一个相当不错的替代方法: 创建一个 Null-Safe-Chain helper 方法。我意识到这与上面@John 的回答非常相似(使用 Coal扩展方法) ,但是我发现它更简单,输入更少。看起来是这样的:

var loader = NullSafe.Chain(cake, c=>c.frosting, f=>f.berries, b=>b.loader);

下面是实施方案:

public static TResult Chain<TA,TB,TC,TResult>(TA a, Func<TA,TB> b, Func<TB,TC> c, Func<TC,TResult> r)
where TA:class where TB:class where TC:class {
if (a == null) return default(TResult);
var B = b(a);
if (B == null) return default(TResult);
var C = c(B);
if (C == null) return default(TResult);
return r(C);
}

我还创建了几个重载(包含2到6个参数) ,以及允许链以值类型或默认值结束的重载。这对我很管用!

试试这个代码:

    /// <summary>
/// check deep property
/// </summary>
/// <param name="obj">instance</param>
/// <param name="property">deep property not include instance name example "A.B.C.D.E"</param>
/// <returns>if null return true else return false</returns>
public static bool IsNull(this object obj, string property)
{
if (string.IsNullOrEmpty(property) || string.IsNullOrEmpty(property.Trim())) throw new Exception("Parameter : property is empty");
if (obj != null)
{
string[] deep = property.Split('.');
object instance = obj;
Type objType = instance.GetType();
PropertyInfo propertyInfo;
foreach (string p in deep)
{
propertyInfo = objType.GetProperty(p);
if (propertyInfo == null) throw new Exception("No property : " + p);
instance = propertyInfo.GetValue(instance, null);
if (instance != null)
objType = instance.GetType();
else
return true;
}
return false;
}
else
return true;
}

我稍微修改了 给你中的代码,使其适用于提出的问题:

public static class GetValueOrDefaultExtension
{
public static TResult GetValueOrDefault<TSource, TResult>(this TSource source, Func<TSource, TResult> selector)
{
try { return selector(source); }
catch { return default(TResult); }
}
}

是的,这可能是 不是最佳解决方案,因为 try/catch 的性能影响,但它是有效的: >

用法:

var val = cake.GetValueOrDefault(x => x.frosting.berries.loader);

实现了 也许是 CODENX 项目 也许或者 IfNotNull 在 C # 中使用 lambdas 作为深表达式

使用示例:

int? CityId= employee.Maybe(e=>e.Person.Address.City);

链接 有人建议中的类似问题 如何检查深 lambda 表达式中的 null?

我喜欢 Objective-C 采取的方法:

Objective-C 语言采用了另一种方法来解决这个问题,它不对 nil 调用方法,而是对所有这样的调用返回 nil

if (cake.frosting.berries != null)
{
var str = cake.frosting.berries...;
}

正如 约翰 · 莱德格伦回答所建议的,解决这个问题的一种方法是使用扩展方法和委托。使用它们可以看起来像这样:

int? numberOfBerries = cake
.NullOr(c => c.Frosting)
.NullOr(f => f.Berries)
.NullOr(b => b.Count());

实现是混乱的,因为您需要让它对值类型、引用类型和可空值类型起作用。您可以在 Timwi回答检查空值的正确方法是什么?中找到完整的实现。

在你需要做到这一点的地方,这样做:

用法

Color color = someOrder.ComplexGet(x => x.Customer.LastOrder.Product.Color);

或者

Color color = Complex.Get(() => someOrder.Customer.LastOrder.Product.Color);

辅助类实现

public static class Complex
{
public static T1 ComplexGet<T1, T2>(this T2 root, Func<T2, T1> func)
{
return Get(() => func(root));
}


public static T Get<T>(Func<T> func)
{
try
{
return func();
}
catch (Exception)
{
return default(T);
}
}
}

或者你可以使用反射:)

反射功能:

public Object GetPropValue(String name, Object obj)
{
foreach (String part in name.Split('.'))
{
if (obj == null) { return null; }


Type type = obj.GetType();
PropertyInfo info = type.GetProperty(part);
if (info == null) { return null; }


obj = info.GetValue(obj, null);
}
return obj;
}

用法:

object test1 = GetPropValue("PropertyA.PropertyB.PropertyC",obj);

My Case (在反射函数中返回 DBNull. Value 而不是 null) :

cmd.Parameters.AddWithValue("CustomerContactEmail", GetPropValue("AccountingCustomerParty.Party.Contact.ElectronicMail.Value", eInvoiceType));