[C#] 깊은 null 검사, 더 좋은 방법이 있습니까?

참고 : 이 질문은 도입하기 전에 질문을 받았다 스튜디오 2015 # 6 / 비주얼 C의 연산자 ..?

우리는 모두 거기에 있었고, 우리는 그것이 null인지 확인 해야하는 cake.frosting.berries.loader와 같은 깊은 속성을 가지고 있으므로 예외는 없습니다. 수행하는 방법은 단락 if 문을 사용하는 것입니다

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

이것은 정확하게 우아하지 않으며 아마도 전체 체인을 확인하고 null 변수 / 속성에 대해 나타나는지 더 쉬운 방법이 있어야합니다.

일부 확장 방법을 사용하는 것이 가능합니까, 아니면 언어 기능입니까, 아니면 나쁜 생각입니까?



답변

새로운 작업 “?”을 추가하는 것을 고려했습니다. 원하는 의미가있는 언어로 (그리고 지금 추가되었습니다. 아래를 참조하십시오.)

cake?.frosting?.berries?.loader

컴파일러는 모든 단락 검사를 생성합니다.

그것은 C # 4에 대한 기준을 만들지 못했습니다. 아마도 가상의 미래 버전의 언어에 대한 것일 것입니다.

업데이트 (2014) :?. 운영자가 지금 계획 은 다음 로슬린 컴파일러 출시. 연산자의 정확한 구문 및 의미 론적 분석에 대해서는 여전히 논쟁의 여지가 있습니다.

업데이트 (2015 년 7 월) : Visual Studio 2015가 출시되었으며 null 조건부 연산자 ?.?[] 을 지원하는 C # 컴파일러가 제공 됩니다.


답변

이 질문에서 영감을 얻어 식 트리를 사용하여 더 쉽고 더 예쁘게 구문을 사용하여 이러한 종류의 깊은 null 검사를 수행하는 방법을 알아 냈습니다. 계층 구조의 깊은 인스턴스에 액세스해야하는 경우 디자인이 잘못 수 있다는 답변에 동의하지만 데이터 표시와 같은 경우에는 매우 유용 할 수 있다고 생각합니다.

그래서 확장 메소드를 만들었습니다.

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

표현식의 일부가 null이 아닌 경우 Berries를 반환합니다. null이 있으면 null이 반환됩니다. 그러나 현재 버전에서는 간단한 멤버 액세스에서만 작동하며 v4의 새로운 MemberExpression.Update 메서드를 사용하기 때문에 .NET Framework 4에서만 작동합니다. 다음은 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;
        }
    }
}

그것은 당신의 표현을 나타내는 표현 트리를 검사하고 부품을 차례로 평가함으로써 작동합니다. 결과가 null이 아닌지 확인할 때마다

MemberExpression 이외의 다른 표현식이 지원되도록 이것을 확장 할 수 있다고 확신합니다. 이것을 개념 증명 코드로 생각하고 그것을 사용하면 성능이 저하 될 수 있음을 명심하십시오 (아마도 많은 경우 중요하지 않지만 단단한 루프에서는 사용하지 마십시오 :-))


답변

이 확장이 딥 네 스팅 시나리오에 매우 유용하다는 것을 알았습니다.

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의 null 병합 연산자에서 파생 된 아이디어입니다. 좋은 점은 반환 유형이 항상 내부 속성의 반환 유형이라는 것입니다.

그렇게하면 이렇게 할 수 있습니다 :

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

… 또는 위의 약간의 변형 :

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

내가 아는 가장 좋은 구문은 아니지만 작동합니다.


답변

Mehrdad Afshari가 이미 지적했듯이 데메테르 법칙을 위반하는 것 외에도 의사 결정 논리에 대해 “깊은 null 검사”가 필요한 것 같습니다.

빈 개체를 기본값으로 바꾸려는 경우가 가장 흔합니다. 이 경우 널 오브젝트 패턴 구현을 고려해야합니다 . 실제 객체의 독립형으로 작동하여 기본값 및 “비 동작”방법을 제공합니다.


답변

업데이트 : 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 #이 이미 좀 더 정교한 게으른 평가를 갖지 않았거나 리플렉션을 사용하지 않는 한 (언어가 아닌 다른 언어 기능이 C # 6에서 .?and ?[]연산자 형식으로 제공 될 수 있음) 거의 확실 합니다. 성능 및 형식 안전상의 이유로 좋은 아이디어).

단순히 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호출 함수에서이를 확인하거나 예외를 throw해야하며 시작한 곳으로 돌아갑니다. 셋째, 느려질 수 있습니다. 넷째, 처음 시작한 것보다 더 나빠 보입니다.

[…] 아니면 나쁜 생각입니까?

나는 함께 머물 것입니다 :

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

또는 Mehrdad Afshari의 위 답변으로 이동하십시오.


추신 : 이 답변을 썼을 때, 나는 분명히 람다 함수의 표현 트리를 고려하지 않았습니다. 이 방향의 솔루션에 대한 예는 @driis의 답변을 참조하십시오. 또한 일종의 리플렉션을 기반으로하므로 간단한 솔루션 ( if (… != null & … != null) …) 뿐만 아니라 성능도 좋지 않을 수도 있지만 구문 관점에서 더 잘 판단 될 수 있습니다.


답변

driis의 답변은 흥미롭지 만, 너무 비싸서 현명하다고 생각합니다. 많은 대리자를 컴파일하는 대신 속성 경로 당 하나의 람다를 컴파일하고 캐시 한 다음 여러 유형을 다시 호출하는 것을 선호합니다.

아래 NullCoalesce는 null 검사와 함께 경로가 null 인 경우 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;
    }


답변

하나의 옵션은 Null Object Patten을 사용하는 것이므로 케이크가 없을 때 null을 사용하는 대신 NullFosting 등을 반환하는 NullCake가 있습니다. 죄송합니다. 설명이 잘되지 않지만 다른 사람들은