代码之家  ›  专栏  ›  技术社区  ›  Homde

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

  •  126
  • Homde  · 技术社区  · 16 年前

    注: 这个问题是在 the .? operator in C# 6 / Visual Studio 2015 .

    我们都去过那里,我们有一些很深的财产,比如蛋糕,磨砂,浆果,装载机,我们需要检查它是否为空,所以没有例外。方法是使用短路if语句

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

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

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

    16 回复  |  直到 8 年前
        1
  •  217
  •   stakx - no longer contributing Saravana Kumar    11 年前

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

    cake?.frosting?.berries?.loader
    

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

    它没有成为C 4的酒吧。也许是对未来语言的假设版本。

    更新(2014): 这个 ?. 现在是操作符 planned 对于下一个Roslyn编译器版本。请注意,对于运算符的确切语法和语义分析,仍存在一些争论。

    更新(2015年7月): Visual Studio 2015已发布,并随附支持 null-conditional operators ?. and ?[] .

        2
  •  27
  •   driis    16 年前

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

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

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

    如果表达式的任何部分都不为空,则返回浆果。如果遇到空值,则返回空值。不过,还有一些警告,在当前版本中,它只能使用简单的成员访问,并且只能在.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;            
            }
        }
    }
    

    它的工作原理是检查表示表达式的表达式树,并逐个评估各个部分;每次检查结果是否为空。

    我确信这可以扩展,以便支持除memberExpression之外的其他表达式。将此代码视为概念代码的证明,请记住,使用它会导致性能损失(在许多情况下,这可能不重要,但不要在紧密循环中使用它:-)

        3
  •  23
  •   John Leidegren    16 年前

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

    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中的空合并操作符中派生出来的一个想法。好的是,返回类型始终是内部属性的返回类型。

    这样就可以做到:

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

    …或上述的细微变化:

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

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

        4
  •  16
  •   Johannes Rudolph    16 年前

    除了违反了德米特定律之外,正如梅尔达德阿夫沙里已经指出的,在我看来,决策逻辑需要“深度空检查”。

    当您希望用默认值替换空对象时,这种情况最常见。在这种情况下,您应该考虑实现 Null Object Pattern . 它充当一个真实对象的代理,提供默认值和“非操作”方法。

        5
  •  10
  •   Community Mohan Dere    9 年前

    更新: 从Visual Studio 2015开始,C编译器(语言版本6)现在可以识别 ?. 操作员,这使得“深度零位检查”变得轻而易举。见 this answer 详情。

    除了重新设计代码,比如 this deleted answer 建议, 另一个(尽管很糟糕)选择是使用 try…catch 阻止以查看 NullReferenceException 在深度属性查找期间的某个时间发生。

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

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

    • 看起来不太好。
    • 它使用异常处理,它应该针对异常情况,而不是您期望在正常操作过程中经常发生的事情。
    • 空引用异常 S可能永远不会被明确捕获。(见 this question 。)

    因此,是否可以使用某种扩展方法,或者它是一种语言特性,[…]

    这几乎肯定是一种语言特性(在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 && ...) ...
    

    或者按照上面的答案去做。


    附笔。: 回到我写这个答案的时候,我显然没有考虑lambda函数的表达式树;请参见@driis'answer以获得这个方向的解决方案。它还基于一种反射,因此可能不如一个简单的解决方案好用。( if (… != null & … != null) … 但是从语法的角度来看,它可能会更好地判断。

        6
  •  5
  •   Double Down    15 年前

    虽然德里斯的回答很有趣,但我认为从性能上看,这有点太贵了。我宁愿为每个属性路径编译一个lambda,缓存它,然后重新调用许多类型,而不是编译许多委托。

    下面的nullcoalesce就是这样做的,它返回一个带有空检查的新lambda表达式,并在任何路径为空时返回默认值(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;
        }
    
        7
  •  4
  •   Ian Ringrose    16 年前

    一种选择是使用空对象模式,因此,当您没有蛋糕时,您有一个空蛋糕,它返回一个nullfosting等。抱歉,我不太擅长解释这个问题,但其他人是,请参见

        8
  •  3
  •   Scott Rippey    14 年前

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

    然而,这里有一个我使用的相当不错的替代方法:创建一个空的安全链帮助器方法。我意识到这与上面的@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个参数),以及允许链以值类型或默认值结尾的重载。这对我来说真的很管用!

        9
  •  1
  •   Community Mohan Dere    9 年前

    Maybe codeplex project 那工具 使用lambdas表示c中的深层表达式的maybe或ifnotnull#

    使用实例:

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

    链接 was suggested 在类似的问题中 How to check for nulls in a deep lambda expression?

        10
  •  1
  •   Community Mohan Dere    9 年前

    如中建议的 John Leidegren answer 解决此问题的一种方法是使用扩展方法和委托。使用它们可能看起来像这样:

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

    实现很混乱,因为您需要让它适用于值类型、引用类型和可以为空的值类型。您可以在中找到完整的实现 Timwi answer What is the proper way to check for null values? .

        11
  •  1
  •   heybeliman    11 年前

    或者您可以使用反射:)

    反射函数:

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

    我的情况(在反射函数中返回dbnull.value而不是null):

    cmd.Parameters.AddWithValue("CustomerContactEmail", GetPropValue("AccountingCustomerParty.Party.Contact.ElectronicMail.Value", eInvoiceType));
    
        12
  •  1
  •   Peter Mortensen Pieter Jan Bonestroo    8 年前

    试试这个代码:

        /// <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;
        }
    
        13
  •  0
  •   Tyler Jensen    15 年前

    我昨晚发了这个帖子,然后一个朋友向我指出了这个问题。希望它有帮助。然后您可以这样做:

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

    阅读 full blog post here .

    同样的朋友也建议你 watch this .

        14
  •  0
  •   Community Mohan Dere    9 年前

    我稍微修改了代码 here 要使问题得到解决,请执行以下操作:

    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);
    
        15
  •  0
  •   Peter Mortensen Pieter Jan Bonestroo    8 年前

    如果需要实现此目标,请执行以下操作:

    用法

    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);
            }
        }
    }
    
        16
  •  -3
  •   Peter Mortensen Pieter Jan Bonestroo    8 年前

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

    “Objective-C语言采用了另一种解决此问题的方法,不在nil上调用方法,而是为所有此类调用返回nil。”

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