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

Hom*_*mde 128 c# null

注意:这个问题是在引进之前要求.?在C#6/Visual Studio的2015年运营商.

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

if (cake != null && cake.frosting != null && cake.frosting.berries != null) ...
Run Code Online (Sandbox Code Playgroud)

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

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

Eri*_*ert 220

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

cake?.frosting?.berries?.loader
Run Code Online (Sandbox Code Playgroud)

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

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

更新(2014):?.运营商正在计划在未来罗斯林编译器版本.请注意,对运算符的确切语法和语义分析仍存在争议.

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

  • @Ian:这个问题非常普遍.这是我们得到的最常见的请求之一. (33认同)
  • @lazyberezovsky:我从来没有理解得墨忒耳的所谓"法律"; 首先,它似乎更准确地被称为"德米特的建议".第二,将"只有一个成员访问"的结果作为其"逻辑结论"的结果是"上帝对象",其中每个对象都需要为每个客户做一切事情,而不是能够分发知道如何做客户端的对象想.我更喜欢与demeter法则完全相反:每个对象都能很好地解决少数问题,其中一个解决方案可以是"这里是另一个可以更好地解决问题的对象" (28认同)
  • @John:我们几乎完全从*我们最有经验的程序员那里得到这个功能请求.MVP始终要求**.但我明白意见各不相同; 如果除了批评之外你还想提出建设性的语言设计建议,我很乐意考虑. (12认同)
  • 没有点,它在条件(A?B:C)运算符的语法上是模糊的.我们试图避免需要我们在令牌流中任意"向前看"的词汇结构.(不幸的是,在C#中已经存在这样的结构;我们宁愿不再添加.) (10认同)
  • @Ian:在可行的情况下,我也更喜欢使用null对象模式,但是大多数人并没有使用他们自己设计的对象模型.许多现有的对象模型使用空值,因此我们必须使用这个世界. (7认同)
  • 我从未被出售过"德米特法则".特别是,我认为"一点"经验法则是愚蠢的,正是由于Eric提到的原因.例如,我已经看到一些Demeter狂热者创建了可怕的类,只有四到五倍的成员才能通过子对象(以避免强迫客户使用第二个点),因为他们有成员提供类相关的服务.这是一种反模式.也许是对LoD的误解?但是,如果善意的开发者被这个原则引发废话,那么原则本身也需要改进. (6认同)
  • 虽然我会欢迎一个零安全的解除引用操作符,但是`.`在眼睛上有点难.我会提出类似`..`的东西.另外,出于好奇,C#中的词法结构有哪些例子需要任意长的向前看? (5认同)
  • @Eric - 我的原始命题是在编译时公开表达式树.我坚信将编译器转换为我可以控制的服务,它不会真正需要任何语法糖.例如,煤(x => x.cake.frosting.color)在编译时由编译时变换煤转换为适当的短路代码.煤看起来像这样,const表达式<Func <T,V >> Coal <T,V>(此T obj,表达式<Func <T,V >> expr).我已经重新使用const关键字作为静态修饰符和编译时标志.返回的表达式树以IL的形式发出. (5认同)
  • @LBushkin:当我试图弄清楚"(某事物)是一个演员及其操作数,或括号表达式后跟另一个人"时,我们也做了深刻的预测.例如,"(Foo.Bar)/ Blah"vs"(Foo.Bar)Blah".这里的规则非常复杂,详见7.6.6节.或者7.5.4.2节也有一些有趣的情况,其中解析需要相当大的前瞻和启发式. (3认同)
  • @Eric-如果你给一个人一条鱼,那么他将会被喂养,如果你教导这个人钓鱼,他就永远不会再饿了,或者我听说过.如果你给缺乏经验的程序员一个简短的符号以避免思考这些问题,我想我将更难以说服我们的开发团队思考他们编写代码的架构含义.你应该推广更多优秀的语言实践,而不是给予人们摆脱困境的廉价技巧. (2认同)

dri*_*iis 27

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 might be 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);
Run Code Online (Sandbox Code Playgroud)

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:

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;            
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Joh*_*ren 24

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

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

这是我从C#和T-SQL中的空合并运算符中获得的一个想法.好处是返回类型始终是内部属性的返回类型.

这样你可以这样做:

var berries = cake.Coal(x => x.frosting).Coal(x => x.berries);
Run Code Online (Sandbox Code Playgroud)

......或略有变化的上述内容:

var berries = cake.Coal(x => x.frosting, x => x.berries);
Run Code Online (Sandbox Code Playgroud)

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


Joh*_*lph 16

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

当您想要使用默认值替换空对象时,通常就是这种情况.在这种情况下,您应该考虑实现Null对象模式.它充当真实对象的替身,提供默认值和"非动作"方法.

  • 是啊.这才是重点.基本上,您将使用Null对象模式模拟ObjC行为. (2认同)

sta*_*ica 10

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

除了重新设计你的代码,比如 这个删除的答案建议,另一个(尽管很可怕)选项是使用一个try…catch块来查看是否NullReferenceException在深度属性查找期间发生了某个时间.

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

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

  • 它看起来不太好看.
  • 它使用异常处理,它应针对特殊情况而不是您希望在正常操作过程中经常发生的事情.
  • NullReferenceException应该永远不应该明确地抓住s.(见这个问题.)

所以可以使用一些扩展方法,或者它是一种语言功能,[...]

这几乎肯定必须是语言特性(在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" );
Run Code Online (Sandbox Code Playgroud)

(注意:代码已编辑.)

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

[...],或者这只是一个坏主意?

我会留下来:

if (cake != null && cake.frosting != null && ...) ...
Run Code Online (Sandbox Code Playgroud)

或者选择Mehrdad Afshari的上述答案.


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

  • 我不知道为什么这会被投票,我做了一个平衡的支持:答案是正确的并带来一个新的方面(并明确提到这个解决方案的缺点......) (2认同)

Dou*_*own 5

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

下面的NullCoalesce就是这样,它返回一个带有空检查的新lambda表达式,如果任何路径为null,则返回默认值(TResult).

例:

NullCoalesce((Process p) => p.StartInfo.FileName)
Run Code Online (Sandbox Code Playgroud)

将返回一个表达式

(Process p) => (p != null && p.StartInfo != null ? p.StartInfo.FileName : default(string));
Run Code Online (Sandbox Code Playgroud)

码:

    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;
    }
Run Code Online (Sandbox Code Playgroud)