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

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

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

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



답변

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

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

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

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


답변

나는 합리적인 추측을 할 수있다 여기에 무슨 일이 일어나고 있는지에 관해서는,하지만 조금 복잡 모두이다 : 그것은 포함 널 상태와 널 (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-정적 분석 도구는 시간이 지남에 따라 진화 할 수있는 모든 종류의 휴리스틱을 갖는 것이 좋습니다. 언어 사양.


답변

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

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

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

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


답변

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

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

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


답변