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


129

注意:这个问题是在引进之前问.?用C#操作6 /的Visual Studio 2015年

我们都去过那里,我们有一些很深的属性,例如cake.frosting.berries.loader,我们需要检查它是否为null,所以也不例外。要做的方法是使用短路if语句

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

这并不十分优雅,也许应该有一种更简单的方法来检查整个链,看看它是否与空变量/属性相抵触。

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


3
我经常希望这样做-但我提出的所有想法都比实际问题要糟糕。
peterchen

感谢您提供所有答案,并很高兴看到其他人也有相同的想法。我想着要如何解决这个问题,尽管Eric的解决方案很好,但我认为我只想编写以下内容:if(IsNull(abc))或if(IsNotNull(abc)),但也许这只是我的口味:)
霍姆德(Homde)2010年

当实例化结霜时,它具有浆果的属性,因此在构造函数中,您能否仅告诉结霜,只要它被无效化即可创建空(非空)浆果?并且每当浆果经过改良糖霜时,都会检查其值吗?
道格·张伯伦

在某种程度上松散相关,我发现这里的一些技术对于我试图解决的“深空值”问题更可取。 stackoverflow.com/questions/818642/...
AaronLS

Answers:


223

我们考虑过添加一个新的操作“?”。具有所需语义的语言。(现在已添加;请参见下文。)也就是说,您会说

cake?.frosting?.berries?.loader

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

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

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

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


10
如果没有该点,则它与条件(A?B:C)运算符在句法上变得不明确。我们试图避免词汇构造要求我们在令牌流中任意“遥遥领先”。(不过,不幸的是,C#中已经存在这样的构造;我们宁愿不再添加。)
Eric Lippert 2010年

33
@Ian:这个问题非常普遍。这是我们收到的最常见的请求之一。
埃里克·利珀特

7
@Ian:我也更喜欢在可能的情况下使用空对象模式,但是大多数人没有使用他们自己设计的对象模型的奢望。许多现有的对象模型都使用null,因此这就是我们必须忍受的世界。
埃里克·利珀特

12
@John:我们几乎完全是我们经验最丰富的程序员那里获得此功能请求的。MVP 一直都在要求。但是我知道,观点各不相同。如果您除了批评之外还想提出建设性的语言设计建议,我很乐意考虑。
埃里克·利珀特

28
@lazyberezovsky:我从不了解Demeter的所谓“法律”;首先,它似乎更准确地称为“ Demeter的建议”。其次,“仅一个成员访问”得出其逻辑结论的结果是“上帝对象”,其中每个对象都需要为每个客户端执行所有操作,而不是能够分发知道如何执行客户端操作的对象想要。我更喜欢与demeter定律完全相反的问题:每个对象都能很好地解决少量问题,其中一个解决方案可以是“这是另一个更好地解决您的问题的对象”
Eric Lippert 2012年

27

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

因此,我创建了一个扩展方法,该方法允许您编写:

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

如果表达式的任何部分都不为null,则将返回Berries。如果遇到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;            
        }
    }
}

它通过检查代表您的表情的表情树,然后一个接一个地评估各个部分来工作。每次检查结果都不为空。

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


你的lambda技巧给我留下了深刻的印象:)然而,语法似乎确实比人们想的要复杂一些,至少是if语句的场景
Homde 2010年

很酷,但是它运行的代码比if .. &&多100倍。仅当它仍编译为if .. &&时才值得。
2013年

1
啊,然后我看到了DynamicInvoke。我虔诚地避免了:)
nawfal

24

我发现此扩展对于深度嵌套方案非常有用。

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

这不是我所知道的最好的语法,但是确实可以。


为什么选择“煤”,那看起来非常令人毛骨悚然。;)但是,如果结霜为空,则样本将失败。应该看起来像这样:varberries = cake.NullSafe(c => c.Frosting.NullSafe(f => f.Berries));
罗伯特·吉斯凯

哦,但是您的意思是第二个参数不是对Coal的调用,它当然必须如此。这只是一个方便的更改。选择器(x => x.berries)被传递到Coal方法中的Coal调用,该方法带有两个参数。
约翰·莱德格伦

合并或合并的名称来自T-SQL,这是我首先想到的地方。IfNotNull表示如果不为null,则发生某事,但是IfNotNull方法调用未解释该事件。煤炭确实是个奇怪的名字,但实际上这是一个值得注意的奇怪方法。
约翰·莱德格伦

最好的字面意思是“ ReturnIfNotNull”或“ ReturnOrDefault”
John Leidegren

@flq +1 ...在我们的项目中称为IfNotNull :)
Marc Sigrist

16

正如Mehrdad Afshari指出的那样,除了违反Demeter法则之外,在我看来,您还需要“深度null检查”来确定决策逻辑。

当您想用默认值替换空对象时,通常是这种情况。在这种情况下,您应该考虑实现Null对象模式。它充当真实对象的替代者,提供默认值和“非操作”方法。


否,objective-c允许将消息发送到空对象,并在必要时返回适当的默认值。那里没有问题。
约翰内斯·鲁道夫

2
是的 这才是重点。基本上,您将使用Null Object Pattern模拟ObjC行为。
Mehrdad Afshari 2010年

10

更新:从Visual Studio 2015开始,C#编译器(语言版本6)现在可以识别?.运算符,从而使“深度null检查”变得轻而易举。有关详细信息,请参见此答案

除了重新设计代码(如 建议的已删除答案)外,另一个(虽然很糟糕)的选择是使用一个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中以.?and ?[]运算符的形式提供),除非C#已经具有更复杂的惰性求值,或者除非您要使用反射(这可能也不是反射功能)。基于性能和类型安全性的好主意)。

由于没有方法可以简单地传递cake.frosting.berries.loader给函数(该函数将被评估并引发null引用异常),因此您将必须通过以下方式实现常规查找方法:它接受一个对象和属性名称以抬头:

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) …)表现出色,但从语法角度来看可能会更好。


2
我不知道为什么要投票反对,我为平衡而投票:答案是正确的,并带来了新的方面(并明确提到了该解决方案的缺点...)
MartinStettner,2010年

“ Mehrdad Afshari的上述答案”在哪里?
Marson Mao

1
@MarsonMao:该答案已被删除。(如果您的SO等级足够高,您仍然可以阅读它。)感谢您指出我的错误:我应该使用超链接引用其他答案,而不要使用“ see see” /“ see below”之类的词(因为答案不会以固定的顺序出现)。我已经更新了答案。
stakx-不再贡献2015年

5

尽管driis的答案很有趣,但我认为这在性能上有点太昂贵了。我不想编译许多委托,而是更愿意为每个属性路径编译一个lambda,将其缓存,然后重新调用它许多类型。

下面的NullCoalesce就是这样做的,它返回带有空检查的新lambda表达式,并在任何路径为空的情况下返回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;
    }


3

我也经常希望使用更简单的语法!当你有方法返回值可能是零它变得特别难看,因为那么你需要额外的变量(例如: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个参数),以及允许链以value-type或default结尾的重载。这对我来说真的很好!



1

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

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

该实现比较麻烦,因为您需要使它适用于值类型,引用类型和可为空的值类型。你可以找到一个完整的实现Timwi回答什么是检查空值的正确方法?


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

我的案例(在反射函数中返回DBNull.Value而不是null):

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

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

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

此处阅读完整的博客文章

同一位朋友还建议您观看此影片。


3
Expression如果您只是要编译和捕获,为什么还要打扰呢?只需使用一个即可Func<T>
Scott Rippey

0

我从这里稍微修改了代码,以使其适用于所提出的问题:

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

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

-3

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

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

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

1
另一种语言的功能(以及您对此的看法)与使其在C#中工作几乎完全无关。它没有帮助任何人解决他们的C#问题
ADyson
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.