深度检查,是否有更好的方法?

时间:2010-01-17 10:17:40

标签: c# null

注意:在介绍the .? operator in C# 6 / Visual Studio 2015之前询问了这个问题。

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

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

这不是很优雅,也许应该有一种更简单的方法来检查整个链,看看它是否出现了null变量/属性。

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

16 个答案:

答案 0 :(得分:218)

我们考虑过添加新操作“?”。到具有您想要的语义的语言。 (现在已添加;见下文。)也就是说,你会说

cake?.frosting?.berries?.loader

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

它没有成为C#4的标准。也许是对于该语言的假设未来版本。

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

更新(2015年7月): Visual Studio 2015已发布,附带支持null-conditional operators ?. and ?[]的C#编译器。

答案 1 :(得分:27)

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

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

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

如果表达式的任何部分都不为null,则返回Berries。如果遇到null,则返回null。但是有一些注意事项,在当前版本中它只能用于简单的成员访问,它只适用于.NET Framework 4,因为它使用了MemberExpression.Update方法,这是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;            
        }
    }
}

它的工作原理是检查表达你的表达式的表达式树,并一个接一个地评估这些部分;每次检查结果都不为空。

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

答案 2 :(得分:23)

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

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

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

答案 3 :(得分:16)

除了违反德米特定律之外,正如Mehrdad Afshari已经指出的那样,在我看来,你需要对决策逻辑进行“深度空检查”。

当您想要使用默认值替换空对象时,通常会出现这种情况。在这种情况下,您应该考虑实施Null Object Pattern。它充当真实对象的替身,提供默认值和“非动作”方法。

答案 4 :(得分:10)

更新:从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
    ...
}

由于以下原因,我个人不会这样做:

  • 看起来不太好。
  • 它使用异常处理,它应针对特殊情况,而不是您在正常操作过程中经常发生的事情。
  • NullReferenceException应该永远不会被明确捕获。 (见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 && ...) ...

或者使用Mehrdad Afshari的上述答案。


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

答案 5 :(得分:5)

虽然driis的答案很有趣,但我认为这样做的性能太贵了。我不是编译许多委托,而是更喜欢为每个属性路径编译一个lambda,缓存它然后重新调用它多种类型。

下面的NullCoalesce就是这样,它返回一个带有空检查的新lambda表达式,如果任何路径为null,则返回默认值(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;
    }

答案 6 :(得分:4)

一个选项是使用Null Object Patten,所以当你没有蛋糕时,不是没有null,你有一个NullCake返回NullFosting等。抱歉,我不是很擅长解释这个,但其他人是,见

答案 7 :(得分:3)

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

然而,这是我使用的一个相当不错的选择:创建一个Null-Safe-Chain辅助方法。我意识到这与@ 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个参数),以及允许链以值类型或默认值结束的重载。这对我来说真的很好用!

答案 8 :(得分:1)

试试这段代码:

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

答案 9 :(得分:1)

Maybe codeplex project实施 也许或者IfNotNull在C#

中使用lambdas进行深层表达式

使用示例:

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

类似问题was suggested

中的链接How to check for nulls in a deep lambda expression?

答案 10 :(得分:1)

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

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

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

答案 11 :(得分:1)

或者您可以使用反射:)

反射功能:

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

答案 12 :(得分:0)

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

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

答案 13 :(得分:0)

我稍微修改了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); }
    }
}

是的,由于尝试/捕获性能的影响,这可能不是最佳解决方案,但它可行:&gt;

用法:

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

答案 14 :(得分:0)

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

用法

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

答案 15 :(得分:-3)

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

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

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