이 코드가 "가능한 널 참조 리턴"컴파일러 경고를 발생시키는 이유는 무엇입니까?


70

다음 코드를 고려하십시오.

using System;

#nullable enable

namespace Demo
{
    public sealed class TestClass
    {
        public string Test()
        {
            bool isNull = _test == null;

            if (isNull)
                return "";
            else
                return _test; // !!!
        }

        readonly string _test = "";
    }
}

이것을 빌드하면로 표시된 줄 !!!에 컴파일러 경고가 표시됩니다 warning CS8603: Possible null reference return..

_test읽기 전용이며 null이 아닌 것으로 초기화 되면 다소 혼란 스럽습니다 .

코드를 다음과 같이 변경하면 경고가 사라집니다.

        public string Test()
        {
            // bool isNull = _test == null;

            if (_test == null)
                return "";
            else
                return _test;
        }

누구든지이 행동을 설명 할 수 있습니까?


1
Debug.Assert는 런타임 검사이므로 관련이 없지만 컴파일러 경고는 컴파일 시간 검사입니다. 컴파일러는 런타임 동작에 액세스 할 수 없습니다.
Polyfun

5
The Debug.Assert is irrelevant because that is a runtime check- 그것은 이다 당신이 라인 밖으로 코멘트 경우, 경고가 사라집니다 때문에 관련.
Matthew Watson

1
@Polyfun : 컴파일러는 Debug.Assert테스트가 실패하면 예외를 발생시키는 속성을 통해 알 수 있습니다 .
Jon Skeet

2
여기에 다양한 사례를 추가했으며 실제로 흥미로운 결과가 있습니다. 나중에 답을 쓸 것입니다-지금해야 할 일.
Jon Skeet

2
@EricLippert : Debug.Assert이제 조건 매개 변수 에 대한 주석 ( src )이 DoesNotReturnIf(false)있습니다.
Jon Skeet

답변:


38

nullable 흐름 분석 은 변수 의 null 상태 를 추적하지만 변수 값과 같은 다른 상태는 추적하지 않으며 bool( isNull위와 같이) 개별 변수 상태 간의 관계 (예 : isNull_test) 는 추적하지 않습니다 .

실제 정적 분석 엔진은 아마도 이러한 작업을 수행 할 수도 있지만 어느 정도는 "휴리스틱"또는 "임의"일 수도 있습니다. 규칙을 따를 필요는 없으며, 시간이 지남에 따라 규칙이 변경 될 수도 있습니다.

C # 컴파일러에서 직접 할 수있는 것은 아닙니다. nullable 경고에 대한 규칙은 Jon의 분석에서 볼 수 있듯이 매우 정교하지만 규칙이므로 추론 할 수 있습니다.

이 기능을 출시함에 따라 대부분 적절한 균형을 잡은 것처럼 느껴지지만 어색한 곳이 몇 군데 있으며 C # 9.0의 위치를 ​​다시 방문하게됩니다.


3
당신은 당신이 명세서에 격자 이론을 넣기를 원한다는 것을 안다; 격자 이론은 대단하고 전혀 혼란스럽지 않습니다! 해! :)
에릭 리퍼 트

7
C #의 프로그램 관리자가 응답하면 질문이 합법적이라는 것을 알고 있습니다!
샘 루비

1
@TanveerBadar : 격자 이론은 부분 순서가있는 값 집합의 분석에 관한 것입니다. 유형이 좋은 예입니다. X 유형의 값을 Y 유형의 변수에 할당 할 수 있으면 Y가 X를 보유하기에 "충분히 큰"것을 의미하고 격자를 형성하기에 충분하다는 것을 의미합니다. 그러면 컴파일러에서 할당 가능성을 검사 할 수 있습니다. 격자 이론의 관점에서 본 명세서에서. 유형 할당 이외의 분석기에 대한 많은 관심 주제가 격자로 표현 될 수 있기 때문에 이것은 정적 분석과 관련이 있습니다.
Eric Lippert

1
@TanveerBadar : lara.epfl.ch/w/_media/sav08:schwartzbach.pdf 에는 정적 분석 엔진이 격자 이론을 사용하는 방법에 대한 좋은 소개 예제가 있습니다.
Eric Lippert

1
@EricLippert Awesome는 당신을 묘사하기 시작하지 않습니다. 그 링크는 바로 필독 목록에 들어갑니다.
Tanveer Badar

56

나는 합리적인 추측을 할 수있다 여기에 무슨 일이 일어나고 있는지에 관해서는,하지만 조금 복잡 모두이다 : 그것은 포함 널 상태와 널 (null)이 초안 사양에 설명 된 내용 추적 . 기본적으로 반환하려는 지점에서 컴파일러는 식의 상태가 "not null"이 아닌 "null 일 수 있음"인지 경고합니다.

이 답변은 "여기에 결론이 있습니다"라기보다는 다소 이야기 형식으로되어 있습니다. 그 방법이 더 유용하기를 바랍니다.

필드를 제거하여 예제를 약간 단순화하고 다음 두 서명 중 하나를 사용하는 방법을 고려합니다.

public static string M(string? text)
public static string M(string text)

아래 구현에서 각 메소드에 다른 번호를 지정하여 특정 예제를 명확하게 참조 할 수 있습니다. 또한 모든 구현이 동일한 프로그램에 존재할 수 있습니다.

각 경우 아래 설명에서 우리는 다양한 일을하지만 복귀를 시도하게 될 겁니다 text- 그것의 널 상태 그래서text .

무조건 반환

먼저 직접 반환 해 봅시다.

public static string M1(string? text) => text; // Warning
public static string M2(string text) => text;  // No warning

지금까지는 간단합니다. 메소드 시작시 매개 변수의 널 입력 가능 상태는 유형 인 경우 "널 (NULL) 일 수 string?있으며" 유형 인 경우 "널 (null)"이 아닙니다 string.

간단한 조건부 반환

이제 if명령문 조건 자체 에서 null을 확인하십시오 . (나는 조건부 연산자를 사용할 것입니다.이 연산자는 동일한 효과가 있다고 생각하지만 질문에 더 충실하고 싶었습니다.)

public static string M3(string? text)
{
    if (text is null)
    {
        return "";
    }
    else
    {
        return text; // No warning
    }
}

public static string M4(string text)
{
    if (text is null)
    {
        return "";
    }
    else
    {
        return text; // No warning
    }
}

if조건 자체가 널 (NULL) 여부를 검사 하는 명령문 내 에서처럼 보이므로 명령문의 각 분기 내 변수 상태는 if다를 수 있습니다. else블록 내 에서 두 코드에서 상태는 "널 (null)이 아닙니다"입니다. 특히, M3에서 상태는 "아마도 null"에서 "null이 아님"으로 변경됩니다.

지역 변수를 사용한 조건부 반환

이제 그 조건을 지역 변수에 끌어 올리십시오.

public static string M5(string? text)
{
    bool isNull = text is null;
    if (isNull)
    {
        return "";
    }
    else
    {
        return text; // Warning
    }
}

public static string M6(string text)
{
    bool isNull = text is null;
    if (isNull)
    {
        return "";
    }
    else
    {
        return text; // Warning
    }
}

M5와 M6 모두 경고를 발행합니다. 따라서 M5에서 상태 변경이 "아마도 null"에서 "null이 아님"으로 긍정적 인 효과를 얻지 못할뿐만 아니라 (M3에서 와 같이) 상태가 "에서 진행 M6에 영향을 not null "에서"null "일 수 있습니다. 정말 놀랐습니다.

그래서 우리는 그것을 배운 것 같습니다 :

  • "로컬 변수 계산 방법"에 대한 논리는 상태 정보를 전파하는 데 사용되지 않습니다. 나중에 더 자세히.
  • 널 (null) 비교를 도입하면 컴파일러는 이전에 널이 아닌 것으로 생각 된 것이 결국 널일 수 있음을 경고 할 수 있습니다.

무시 된 비교 후 무조건 리턴

무조건 반환 전에 비교를 도입하여 해당 글 머리 기호의 두 번째 요점을 살펴 보겠습니다. (따라서 비교 결과를 완전히 무시하고 있습니다.) :

public static string M7(string? text)
{
    bool ignored = text is null;
    return text; // Warning
}

public static string M8(string text)
{
    bool ignored = text is null;
    return text; // Warning
}

M8이 M2와 동등해야한다고 느낀다는 점에 주목하십시오. 둘 다 무조건 반환하는 null이 아닌 매개 변수가 있지만 null과 비교하면 상태가 "null이 아님"에서 "null이 될 수 있음"으로 변경됩니다. text조건 전에 역 참조 를 시도하여 이에 대한 추가 증거를 얻을 수 있습니다 .

public static string M9(string text)
{
    int length1 = text.Length;   // No warning
    bool ignored = text is null;
    int length2 = text.Length;   // Warning
    return text;                 // No warning
}

return명령문에 지금 경고가 표시되지 않는 방법에 주목하십시오 . 실행 상태 text.Length는 "널이 아님"입니다 (해당 표현식을 성공적으로 실행하면 널이 될 수 없기 때문에). 따라서 text매개 변수는 유형으로 인해 "널이 아님"으로 시작하고, 널 비교로 인해 "아마도 널"이 된 다음에 "널이 아님"이됩니다 text2.Length.

어떤 비교가 상태에 영향을 줍니까?

그래야의 비교입니다 text is null... 유사한 비교가 무슨 효과는? 널이 아닌 문자열 매개 변수로 시작하는 네 가지 방법이 더 있습니다.

public static string M10(string text)
{
    bool ignored = text == null;
    return text; // Warning
}

public static string M11(string text)
{
    bool ignored = text is object;
    return text; // No warning
}

public static string M12(string text)
{
    bool ignored = text is { };
    return text; // No warning
}

public static string M13(string text)
{
    bool ignored = text != null;
    return text; // Warning
}

그래서 비록 x is object지금에 권장 대안 x != null들이 같은 효과가 없습니다 : 만 비교 널 (null)와 (어떤과를 is, ==또는 !=) "어쩌면 널 (null)"에 "NOT NULL"에서 상태를 변경합니다.

상태를 올리는 것이 왜 효과가 있습니까?

첫 번째 글 머리 기호로 돌아가서 왜 M5와 M6이 지역 변수로 이어지는 조건을 고려하지 않습니까? 다른 사람들을 놀라게하는 것만 큼 놀랍지는 않습니다. 이러한 종류의 논리를 컴파일러와 사양에 구축하는 것은 많은 작업이며 상대적으로 이익이 적습니다. 다음은 무언가 인라인이 영향을 미치는 nullable과 관련이없는 또 다른 예입니다.

public static int X1()
{
    if (true)
    {
        return 1;
    }
}

public static int X2()
{
    bool alwaysTrue = true;
    if (alwaysTrue)
    {
        return 1;
    }
    // Error: not all code paths return a value
}

우리alwaysTrue 는 항상 사실이라는 것을 알고 있지만 명세서 뒤의 코드를 if도달 할 수 없게 만드는 사양의 요구 사항을 충족시키지 못합니다 .

명확한 할당에 관한 또 다른 예는 다음과 같습니다.

public static void X3()
{
    string x;
    bool condition = DateTime.UtcNow.Year == 2020;
    if (condition)
    {
        x = "It's 2020.";
    }
    if (!condition)
    {
        x = "It's not 2020.";
    }
    // Error: x is not definitely assigned
    Console.WriteLine(x);
}

우리 는 코드가 그 if문장 중 하나에 정확하게 들어갈 것이라는 것을 알고 있지만 , 그 사양을 해결하는 데는 아무것도 없습니다. 정적 분석 도구는 그렇게 할 수는 있지만 언어 사양에 넣는 것은 나쁜 생각이 될 것입니다. IMO-정적 분석 도구는 시간이 지남에 따라 진화 할 수있는 모든 종류의 휴리스틱을 갖는 것이 좋습니다. 언어 사양.


7
훌륭한 분석 Jon. Coverity checker를 공부할 때 내가 배운 핵심은 코드가 저자의 신념에 대한 증거라는 것입니다 . 우리가 코드의 저자가 그 검사가 필요하다고 생각했음을 알려주는 null 검사를 볼 때. 체커는 실제로 저자의 신념이 일관성이 없다는 증거를 찾고 있는데, 왜냐하면 그것이 버그가 발생했다는 것에 대한 일관성없는 신념을 보는 장소이기 때문입니다.
Eric Lippert

6
예를 들어 if (x != null) x.foo(); x.bar();두 가지 증거가 있습니다. 이 if진술은 "제작자가 x가 foo를 호출하기 전에 널이 될 수 있다고 믿는다"는 제안의 증거이며, 다음 진술은 "저자가 x를 호출하기 전에 x가 널이 아니라고 믿는다"는 증거이며, 이러한 모순은 버그가 있다는 결론. 버그는 불필요한 널 검사의 비교적 양성적인 버그이거나 잠재적으로 충돌하는 버그입니다. 어떤 버그가 실제 버그인지는 확실하지 않지만 버그가있는 것은 분명합니다.
Eric Lippert

1
현지인의 의미를 추적하지 않고 "가상 경로"를 제거하지 않는 비교적 정교하지 않은 체커 (인류가 불가능하다고 말할 수있는 흐름 경로 제어)는 정확하게 모델링하지 않았기 때문에 정확하게 오 탐지를 생성하는 경향이 있습니다. 저자의 신념. 그 까다로운 비트입니다!
Eric Lippert

3
"is object", "is {}"및 "! = null"간의 불일치는 지난 몇 주 동안 내부적으로 논의한 항목입니다. 가까운 시일 내에 LDM에서이를 가져 와서 순수한 널 검사로 간주해야하는지 아닌지를 결정합니다 (동작이 일관되게 함).
JaredPar

1
@ArnonAxelrod 즉이 아니에요 말한다 의미 null이 될 수 있습니다. 널 입력 가능 참조 유형은 컴파일러 힌트 일 뿐이므로 여전히 널일 수 있습니다. (예 : M8 (null!); 또는 C # 7 코드에서 호출하거나 경고를 무시합니다.) 이는 나머지 플랫폼의 유형 안전과 다릅니다.
Jon Skeet

29

로컬 변수로 인코딩 된 의미를 추적 할 때이 경고를 생성하는 프로그램 흐름 알고리즘이 비교적 정교하지 않다는 증거를 발견했습니다.

흐름 검사기의 구현에 대한 구체적인 지식은 없지만 과거에 비슷한 코드의 구현에 대해 연구 한 결과 교육받은 추측을 할 수 있습니다. 플로우 체커는 오탐 (false positive) 경우 두 가지를 추론 할 가능성_test 이 있습니다 . (1) null 일 수 있습니다. 그렇지 않은 경우 처음에는 비교할 isNull수 없기 때문에 (2) true 또는 false 일 수 있습니다. 그것을 할 수 없다면, 당신은 그것을 가질 수 없습니다 if. 그러나 null이 아닌 return _test;경우에만 실행 _test되는 연결은 연결되지 않습니다.

이것은 놀랍게도 까다로운 문제이며, 전문가가 여러 해 동안 작업해온 도구를 정교하게 만드는 데 컴파일러가 시간이 걸릴 것으로 예상해야합니다. 예를 들어 Coverity flow checker는 두 변형 중 어느 것도 null을 반환하지 않는다고 추론하는 데 전혀 문제가 없지만 Coverity flow checker는 회사 고객에게 심각한 비용이 듭니다.

또한 Coverity checker는 밤새 큰 코드베이스에서 실행되도록 설계되었습니다 . C # 컴파일러의 분석은 편집기의 키 입력 사이에서 실행해야하므로 합리적으로 수행 할 수있는 심층 분석의 종류가 크게 변경됩니다.


"단순한는"맞다 - 그것은 우리 모두가 알고있는 정지 문제는 문제에 힘든 너트의 비트가 있지만, 사이의 모든에서 차이가 있다는 사실, 조건문 같은 것들에 실수를 한단다 경우, 나는 그것이 용서 고려 bool b = x != null대가 bool b = x is { }(에 할당이 실제로 사용되지 않았다!)는 널 검사에 대해 인식 된 패턴조차도 의심 스럽다는 것을 보여줍니다. 의심의 여지없이 팀의 노력을 비난하지 않고 실제 사용중인 코드베이스에 대해이 작업을 수행하도록하십시오. 분석은 자본 P 실용적 인 것처럼 보입니다.
Jeroen Mostert

@JeroenMostert : Jared Par는 Microsoft 가이 문제를 내부적으로 논의하고 있다는 Jon Skeet의 답변 에 대한 언급에서 언급했습니다 .
Brian

8

다른 모든 대답은 거의 정확합니다.

궁금한 사람이 있으면 https://github.com/dotnet/roslyn/issues/36927#issuecomment-508595947 에서 가능한 한 명시 적으로 컴파일러의 논리를 철자하려고했습니다.

언급되지 않은 한 가지는 널 검사가 "순수한"것으로 간주되어야하는지 여부를 결정하는 방법입니다.이를 수행하는 경우 널이 가능한지 심각하게 고려해야합니다. C #에는 많은 "부수적 인"null 검사가 있는데, 여기서 다른 것을 수행하는 과정에서 null을 테스트하므로 검사 세트를 사람들이 의도적으로 수행하고있는 것으로 확인하기로 결정했습니다. 우리가 함께했다 경험적의 이유 그래서 "단어 널 (null)이 들어"했다 x != nullx is object다른 결과를 생성합니다.

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