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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2080647/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 23:20:41  来源:igfitidea点击:

Deep null checking, is there a better way?

c#null

提问by Homde

Note:This question was asked before the introduction of the .?operator in C# 6 / Visual Studio 2015.

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

We've all been there, we have some deep property like cake.frosting.berries.loader that we need to check if it's null so there's no exception. The way to do is is to use a short-circuiting if statement

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

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

This is not exactly elegant, and there should perhaps be an easier way to check the entire chain and see if it comes up against a null variable/property.

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

Is it possible using some extension method or would it be a language feature, or is it just a bad idea?

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

采纳答案by Eric Lippert

We have considered adding a new operation "?." to the language that has the semantics you want. (And it has been added now; see below.) That is, you'd say

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

cake?.frosting?.berries?.loader

and the compiler would generate all the short-circuiting checks for you.

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

It didn't make the bar for C# 4. Perhaps for a hypothetical future version of the language.

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

Update (2014):The ?.operator is now plannedfor the next Roslyn compiler release. Note that there is still some debate over the exact syntactic and semantic analysis of the operator.

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

Update (July 2015):Visual Studio 2015 has been released and ships with a C# compiler that supports the null-conditional operators ?.and ?[].

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

回答by Johannes Rudolph

Besides violating the Law of Demeter, as Mehrdad Afshari has already pointed out, it seems to me you need "deep null checking" for decision logic.

除了违反 Demeter 法则,正如 Mehrdad Afshari 已经指出的那样,在我看来,您需要对决策逻辑进行“深度空检查”。

This is most often the case when you want to replace empty objects with default values. In this case you should consider implementing the Null Object Pattern. It acts as a stand-in for a real object, providing default values and "non-action" methods.

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

回答by stakx - no longer contributing

Update:Starting with Visual Studio 2015, the C# compiler (language version 6) now recognizes the ?.operator, which makes "deep null checking" a breeze. See this answerfor details.

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

Apart from re-designing your code, like this deleted answersuggested, another (albeit terrible) option would be to use a try…catchblock to see if a NullReferenceExceptionoccurs sometime during that deep property lookup.

除了重新设计你的代码,就像 这个删除的答案所建议的那样,另一个(尽管很糟糕)的选择是使用一个try…catch块来查看NullReferenceException在深度属性查找期间是否发生了。

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

I personally wouldn't do this for the following reasons:

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

  • It doesn't look nice.
  • It uses exception handling, which should target exceptional situations and not something that you expect to happen often during the normal course of operation.
  • NullReferenceExceptions should probably never be caught explicitly. (See this question.)
  • 看起来不好看。
  • 它使用异常处理,它应该针对异常情况,而不是您期望在正常操作过程中经常发生的事情。
  • NullReferenceExceptions 可能永远不会被明确捕获。(见这个问题。)

So is it possible using some extension method or would it be a language feature, [...]

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

This would almost certainly have to be a language feature (which is available in C# 6 in the form of the .?and ?[]operators), unless C# already had more sophisticated lazy evaluation, or unless you want to use reflection (which probably also isn't a good idea for reasons of performance and type-safety).

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

Since there's no way to simply pass cake.frosting.berries.loaderto a function (it would be evaluated and throw a null reference exception), you would have to implement a general look-up method in the following way: It takes in an objects and the names of properties to look up:

由于无法简单地传递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" );

(Note: code edited.)

(注:代码已编辑。)

You quickly see several problems with such an approach. First, you don't get any type safety and possible boxing of property values of a simple type. Second, you can either return nullif something goes wrong, and you will have to check for this in your calling function, or you throw an exception, and you're back to where you started. Third, it might be slow. Fourth, it looks uglier than what you started with.

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

[...], or is it just a bad idea?

[...],还是只是个坏主意?

I'd either stay with:

我要么留在:

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

or go with the above answer by Mehrdad Afshari.

或使用 Mehrdad Afshari 的上述答案。



P.S.:Back when I wrote this answer, I obviously didn't consider expression trees for lambda functions; see e.g. @driis' answer for a solution in this direction. It's also based on a kind of reflection and thus might not perform quite as well as a simpler solution (if (… != null & … != null) …), but it may be judged nicer from a syntax point-of-view.

PS:当我写这个答案时,我显然没有考虑 lambda 函数的表达式树;请参阅例如@driis 的回答以了解此方向的解决方案。它也基于一种反射,因此可能不如更简单的解决方案 ( if (… != null & … != null) …)表现得那么好,但从语法的角度来看,它可能会被判断为更好。

回答by John Leidegren

I've found this extension to be quite useful for deep nesting scenarios.

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

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

It's an idea I derrived from the null coalescing operator in C# and T-SQL. The nice thing is that the return type is always the return type of the inner property.

这是我从 C# 和 T-SQL 中的空合并运算符派生出来的一个想法。好消息是返回类型始终是内部属性的返回类型。

That way you can do this:

这样你就可以这样做:

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

...or a slight variation of the above:

...或上述内容的轻微变化:

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

It's not the best syntax I know, but it does work.

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

回答by driis

I got inspired by this question to try and find out how this kind of deep null checking can be done with an easier / prettier syntax using expression trees. While I do agree with the answers stating that it mightbe a bad design if you often need to access instances deep in the hierarchy, I also do think that in some cases, such as data presentation, it can be very useful.

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

So I created an extension method, that will allow you to write:

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

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

This will return the Berries if no part of the expression is null. If null is encountered, null is returned. There are some caveats though, in the current version it will only work with simple member access, and it only works on .NET Framework 4, because it uses the MemberExpression.Update method, which is new in v4. This is the code for the IfNotNull extension method:

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

It works by examining the expression tree representing your expression, and evaluating the parts one after the other; each time checking that the result is not null.

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

I am sure this could be extended so that other expressions than MemberExpression is supported. Consider this as proof-of-concept code, and please keep in mind that there will be a performance penalty by using it (which will probably not matter in many cases, but don't use it in a tight loop :-) )

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

回答by Ian Ringrose

One option is to use the Null Object Patten, so instead of having null when you don't have a cake, you have a NullCake that returns a NullFosting etc. Sorry I am not very good at explaining this but other people are, see

一种选择是使用 Null Object Patten,因此当您没有蛋糕时,您可以使用 NullCake 返回 NullFosting 等,而不是 null。对不起,我不太擅长解释这一点,但其他人是,请参阅

回答by Tyler Jensen

I posted this last night and then a friend pointed me to this question. Hope it helps. You can then do something like this:

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

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

Read the full blog post here.

此处阅读完整的博客文章

The same friend also suggested that you watch this.

同样的朋友也建议你看这个

回答by Double Down

While driis' answer is interesting, I think it's a bit too expensive performance wise. Rather than compiling many delegates, I'd prefer to compile one lambda per property path, cache it and then reinvoke it many types.

虽然 driis 的回答很有趣,但我认为它在性能方面有点过于昂贵。与其编译许多委托,我更愿意为每个属性路径编译一个 lambda,缓存它,然后重新调用它多种类型。

NullCoalesce below does just that, it returns a new lambda expression with null checks and a return of default(TResult) in case any path is null.

下面的 NullCoalesce 就是这样做的,它返回一个带有空检查的新 lambda 表达式,并在任何路径为空的情况下返回 default(TResult)。

Example:

例子:

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

Will return an expression

将返回一个表达式

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

Code:

代码:

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

回答by Scott Rippey

I too have often wished for a simpler syntax! It gets especially ugly when you have method-return-values that might be null, because then you need extra variables (for example: cake.frosting.flavors.FirstOrDefault().loader)

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

However, here's a pretty decent alternative that I use: create an Null-Safe-Chain helper method. I realize that this is pretty similar to @John's answer above (with the Coalextension method) but I find it's more straightforward and less typing. Here's what it looks like:

但是,这是我使用的一个相当不错的替代方法:创建一个 Null-Safe-Chain 辅助方法。我意识到这与上面@John 的答案非常相似(使用Coal扩展方法),但我发现它更直接,输入更少。这是它的样子:

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

Here's the implementation:

这是实现:

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

I also created several overloads (with 2 to 6 parameters), as well as overloads that allow the chain to end with a value-type or default. This works really well for me!

我还创建了几个重载(带有 2 到 6 个参数),以及允许链以值类型或默认值结尾的重载。这对我来说真的很好用!

回答by JKSUN

Try this code:

试试这个代码:

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