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


130

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

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

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

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

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


3
나는 그것을 충분히 자주 바랐지만, 내가 생각 해낸 모든 아이디어는 실제 문제보다 나빴다.
peterchen

다른 사람들이 같은 생각을 가지고 있다는 것을 알게 된 모든 대답에 감사드립니다. 나는 이것이 스스로 해결되는 방법을 생각하고 에릭의 솔루션은 훌륭하지만 나는 단순히 if (IsNull (abc)) 또는 if (IsNotNull (abc)) 같은 것을 작성하고 싶다고 생각하지만 어쩌면 그건 내 취향이다 :)
Homde

프로스팅을 인스턴스화하면 베리의 속성이 있으므로 생성자의 해당 시점에서 프로스팅에 빈 (널이 아닌) 베리를 만들기 위해 멈출 때마다 말할 수 있습니까? 그리고 열매가 수정 될 때마다 설탕 프로스팅이 값을 확인합니까 ????
더그 체임벌린

다소 느슨하게 관련되어 있으며, 여기에서 다루는 일부 기술은 내가 겪고 자하는 "deep nulls"문제보다 선호되는 것으로 나타났습니다. stackoverflow.com/questions/818642/…
AaronLS

답변:


223

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

cake?.frosting?.berries?.loader

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

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

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

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


10
점이 없으면 조건부 (A? B : C) 연산자와 구문 상 모호하게됩니다. 우리는 토큰 스트림에서 임의적으로 "미리보기"를 요구하는 어휘 구성을 피하려고 노력합니다. (불행히도 C #에는 이미 그러한 구성이 있습니다. 더 이상 추가하지는 않겠습니다.)
Eric Lippert

33
@Ian :이 문제는 매우 일반적입니다. 이것은 가장 빈번한 요청 중 하나입니다.
Eric Lippert

7
@Ian : 또한 가능할 때 null 객체 패턴을 사용하는 것을 선호하지만 대부분의 사람들은 스스로 디자인 한 객체 모델로 작업 할 수 없습니다. 기존의 많은 객체 모델은 널 (null)을 사용하므로 우리가 살아야 할 세상입니다.
Eric Lippert

12
@ 존 : 우리는이 기능 요청을 거의 전적으로 가장 숙련 된 프로그래머 로부터받습니다 . MVP들은이를 항상 요구합니다 . 그러나 나는 의견이 다양하다는 것을 이해한다. 당신의 비판과 더불어 건설적인 언어 디자인 제안을하고 싶으 시다면 기꺼이 고려해 보시기 바랍니다.
Eric Lippert

28
@ lazyberezovsky : 나는 Demeter의 소위 "법칙"을 이해하지 못했습니다. 우선, 더 정확하게 "계량기 제안"이라고합니다. 둘째, 논리적 결론에 "단일 구성원 액세스"를 수행 한 결과는 클라이언트가 무엇을 수행 하는지를 아는 개체를 전달할 수있는 것이 아니라 모든 개체가 모든 클라이언트에 대해 모든 작업을 수행해야하는 "신 개체"입니다. 원한다. 저는 데메테르 법칙의 정반대를 선호합니다. 모든 물체는 적은 수의 문제를 잘 해결하며, 그 해결책 중 하나는 "문제를 더 잘 해결하는 또 다른 물체가 될 수 있습니다"
Eric Lippert

27

이 질문에서 영감을 얻어 식 트리를 사용하여 더 쉽고 더 예쁘게 구문을 사용하여 이러한 종류의 깊은 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 이외의 다른 표현식이 지원되도록 이것을 확장 할 수 있다고 확신합니다. 이것을 개념 증명 코드로 생각하고 그것을 사용하면 성능이 저하 될 수 있음을 명심하십시오 (아마도 많은 경우 중요하지 않지만 단단한 루프에서는 사용하지 마십시오 :-))


나는 당신의 람다 능력에 감동하고 있습니다 : 구문 그러나이어야은 if 문 시나리오, 조금 한 원하는 것보다 더 복잡한 비트 것으로 보인다
Homde

멋지지만 if .. &&보다 100x 더 많은 코드처럼 실행됩니다. 여전히 if .. &&로 컴파일되는 경우에만 가치가 있습니다.
Monstieur

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

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

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

... 또는 위의 약간의 변형 :

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

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


왜 "석탄", 그것은 매우 소름 끼치는 것 같습니다. ;) 그러나 프로스팅이 null 인 경우 샘플이 실패합니다. 다음과 같이 보일 것입니다 : var berries = cake.NullSafe (c => c.Frosting.NullSafe (f => f.Berries));
Robert Giesecke

아, 그러나 두 번째 주장은 물론 석탄에 대한 호출이 아니라는 것을 암시하고 있습니다. 편리한 변경입니다. 선택기 (x => x.berries)는 Coal 메서드 내에서 두 개의 인수를 사용하는 Coal 호출로 전달됩니다.
John Leidegren

통합 또는 통합이라는 이름은 T-SQL에서 가져 왔으며, 여기서 처음으로 아이디어를 얻었습니다. IfNotNull은 null이 아닌 경우 어떤 일이 발생한다는 것을 의미하지만, IfNotNull 메서드 호출로 설명되지 않습니다. 석탄은 실제로 이상한 이름이지만 주목할만한 이상한 방법입니다.
John Leidegren

문자 그대로 가장 좋은 이름은 "ReturnIfNotNull"또는 "ReturnOrDefault"와 같습니다.
John Leidegren

@flq 일 ... 우리의 프로젝트는 것 또한 : IfNotNull라고
마크 Sigrist

16

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

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


아니요, objective-c를 사용하면 메시지를 null 객체로 보낼 수 있으며 필요한 경우 적절한 기본값을 반환합니다. 문제 없습니다.
Johannes Rudolph

2
네. 그게 요점입니다. 기본적으로 Null Object Pattern을 사용하여 ObjC 동작을 에뮬레이션합니다.
Mehrdad Afshari

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 #이 이미 좀 더 정교한 게으른 평가를 갖지 않았거나 리플렉션을 사용하지 않는 한 (언어가 아닌 다른 언어 기능이 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) …) 뿐만 아니라 성능도 좋지 않을 수도 있지만 구문 관점에서 더 잘 판단 될 수 있습니다.


2
나는 이것이 다운 보트 된 이유를 모른다, 나는 균형을위한 공감대를했다 : 대답은 정확하고 새로운 측면을 가져온다 (그리고이 솔루션의 단점을 분명히 언급한다 ...)
MartinStettner

"Mehrdad Afshari의 위 답변"은 어디에 있습니까?
Marson Mao

1
@MarsonMao : 그 답변은 그 동안 삭제되었습니다. (SO 등급이 충분히 높으면 여전히 읽을 수 있습니다.) 내 실수를 지적 해 주셔서 감사합니다. "위 참조"/ "아래 참조"와 같은 단어를 사용하지 않고 하이퍼 링크를 사용하여 다른 답변을 참조해야합니다. 답변은 고정 된 순서로 표시되지 않습니다). 내 답변을 업데이트했습니다.
stakx-더 이상

5

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


3

나도 종종 더 간단한 구문을 원했다! null 일 수있는 method-return-values가있는 경우 특히 추악해진다. 추가 변수가 필요하기 때문이다 (예 :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 매개 변수 포함)를 만들었습니다. 이것은 정말 잘 작동합니다!



1

John Leidegren답변 에서 제안한 것처럼 이 문제를 해결하는 한 가지 방법은 확장 방법과 대리자를 사용하는 것입니다. 그것들을 사용하면 다음과 같이 보일 수 있습니다 :

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

값 유형, 참조 유형 및 널 입력 가능 값 유형에서 작동하도록 구현해야하기 때문에 구현이 복잡합니다. 당신의 완전한 구현을 찾을 수 있습니다 Timwi대답널 (null) 값을 확인하는 적절한 방법은 무엇입니까? .


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

내 경우 (리플렉션 함수에서 null 대신 DBNull.Value를 반환) :

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

그리고 그렇습니다. 이것은 시도 / 캐치 성능 영향으로 인해 최적의 솔루션 은 아니지만 작동합니다 :>

용법:

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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.