深空检查,有没有更好的办法?

注意:这个问题在引入之前被问过.? 运算符在C#6 / Visual Studio 2015中 。

我们都在那里,我们有一些像cake.frosting.berries.loader这样的深层属性,我们需要检查它是否为null,所以没有例外。 要做的方法是使用短路if语句

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

这不完全是优雅的,也许应该有一个更简单的方法来检查整个链,看看它是否出现在空variables/属性。

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

我们考虑过增加一个新的操作“?”。 到具有你想要的语义的语言。 (现在已经添加了,见下面)。那就是,你会说

 cake?.frosting?.berries?.loader 

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

它并没有为C#4做好准备。也许对于这个语言的假设性的未来版本。

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

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

我从这个问题得到启发,试图找出如何使用expression式树更容易/更漂亮的语法来完成这种深度空检查。 虽然我同意答案,说如果您经常需要访问层次结构中的深层实例,那么这可能是一个糟糕的devise,但我也认为在某些情况下(如数据表示),这可能非常有用。

所以我创build了一个扩展方法,这将允许你写:

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

如果没有expression式的任何部分为空,这将返回浆果。 如果遇到null,则返回null。 但是有一些注意事项,在当前版本中,它只能用于简单的成员访问,它只能在.NET Framework 4上运行,因为它使用了v4中新增的MemberExpression.Update方法。 这是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; } } } 

它通过检查代表你的expression的expression式树,并逐一评估这些部分来工作。 每次检查结果不为空。

我相信这可以被扩展,以便除了MemberExpression以外的其他expression式被支持。 考虑这个概念validation码,请记住使用它会有性能损失(这在很多情况下可能并不重要,但是不要在严格的循环中使用它):)

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

 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中的空合并运算符得到的想法。 好的是返回types始终是内部属性的返回types。

这样你可以做到这一点:

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

…或以上的细微变化:

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

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

除了违反德米特法之外,阿夫沙里(Mehrdad Afshari)已经指出,在我看来,对于决策逻辑来说,需要“深度空检查”。

这是最常见的情况,当你想用默认值replace空对象。 在这种情况下,你应该考虑实现空对象模式 。 它作为一个真实对象的替身,提供默认值和“非操作”方法。

更新:从Visual Studio 2015开始,C#编译器(语言版本6)现在可以识别?. 运算符,这使得“深空检查”变得轻而易举。 看到这个答案的细节。

除了重新devise你的代码,就像这个被删除的答案build议的那样,另一个(尽pipe可怕的)选项是使用try…catch块来查看在深度属性查找过程中是否发生NullReferenceException

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

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

  • 这看起来不错。
  • 它使用exception处理,它应该针对exception情况,而不是在正常的操作过程中经常发生的事情。
  • NullReferenceException s应该不会被明确地捕获。 (看这个问题 )

那么有可能使用某种扩展方法,或者它会成为一种语言function,

这几乎肯定必须是一个语言特性(在C#6中以.??[]运算符的forms提供),除非C#已经有了更复杂的懒惰评估,或者除非你想使用reflection由于性能和types安全的原因也不是好主意)。

由于没有办法简单地将cake.frosting.berries.loader传递给一个函数(它将被评估并抛出一个空的引用exception),你将不得不以下面的方式实现一个通用的查找方法:对象和要查找的属性的名称:

 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" ); 

(注:代码编辑。)

你很快就会看到这种方法的几个问题。 首先,你没有得到任何types的安全和可能拳击属性值的简单types。 其次,如果出现问题,你可以返回null ,你将不得不在你的调用函数中检查这个,或者抛出一个exception,然后你回到你开始的地方。 第三,可能会很慢。 第四,它看起来比你开始的还要丑陋。

[…],还是只是一个坏主意?

我要么留在:

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

或者按照Mehrdad Afshari的上述回答。


PS:当我写这个答案的时候,我显然没有考虑lambda函数的expression式树; 请参阅@driis'回答这个方向的解决scheme。 它也是基于一种reflection,因此可能不会像简单的解决scheme( if (… != null & … != null) … )performance的更好,但是可以从语法的angular度来判断它是否更好。

虽然driis的答案很有意思,但我认为这是一个有点太昂贵的性能。 而不是编译许多委托,我宁愿编译一个lambda每个属性path,caching它,然后重新调用它许多types。

下面的NullCoalesce就是这样做的,它返回一个新的lambdaexpression式,其中包含null检查,并且在任何path为null的情况下返回缺省值(TResult)。

例:

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

将返回一个expression式

 (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; } 

一个select是使用Null对象Patten,所以当你没有蛋糕的时候不用null,你有一个返回NullFosting的NullCake等等。对不起,我不是很善于解释这个,但是其他人

  • 空对象Patten用法的一个例子
  • 维基百科写下了空对象彭定康

安全导航操作员进入下一个版本的C#:

[ MSDN博客 ] 最后,C#正在获得“?。”,有时称为安全导航运营商

我也经常希望更简单的语法! 当方法返回值可能为null时,它会变得特别难看,因为那么你需要额外的variables(例如: cake.frosting.flavors.FirstOrDefault().loader

但是,这里有一个相当不错的select:创build一个Null-Safe-Chain辅助方法。 我意识到这与@ John的答案非常相似(使用Coal扩展方法),但是我发现它更直接,更less打字。 以下是它的样子:

 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); } 

我还创build了一些重载(有2到6个参数),以及重载允许链以值types或默认值结束。 这对我来说真的很好!

试试这个代码:

  /// <summary> /// check deep property /// </summary> /// <param name="obj">instance</param> /// <param name="property">deep property not include instance name example "ABCDE"</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; } 

有可能codeplex项目 ,实现可能或IfNotNull使用lambdaexpression式深入C#中的expression式

使用示例:

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

链接是在类似的问题中提出的如何检查深度lambdaexpression式中的空值?

正如John Leidegren的回答所build议的,解决这个问题的一种方法是使用扩展方法和委托。 使用它们可能看起来像这样:

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

实现是混乱的,因为你需要让它适用于值types,引用types和可为空的值types。 您可以在Timwi的答案中find一个完整的实现: 什么是检查空值的正确方法? 。

我真的很喜欢DmitriNеstеruk的这个版本: http : //www.codeproject.com/Articles/109026/Chained-null-checks-and-the-Maybe-monad

或者你可以使用reflection:)

reflectionfunction:

 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); 

我的大小写(在reflection函数中返回DBNull.Value而不是null):

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

我昨天晚上发布了这个消息,然后一个朋友向我指出了这个问题。 希望它有帮助。 然后你可以做这样的事情:

 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; } } } } 

在这里阅读完整的博客文章 。

同样的朋友也build议你看这个 。

我稍微修改了这里的代码,使它适用于所问的问题:

 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); } } } 

是的,这可能不是最佳的解决scheme,由于try / catch性能的影响,但它的工作原理:>

用法:

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

我喜欢Objective C采取的方法

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

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

你需要做到这一点。

用法

 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); } } }