다음을 통해 공유


향상된 명확한 할당 분석

메모

이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 ECMA 사양에 통합될 때까지 게시됩니다.

기능 사양과 완료된 구현 간에 약간의 불일치가 있을 수 있습니다. 이러한 차이는 관련LDM(언어 디자인 모임) 노트에서 캡처됩니다.

사양문서에서 기능 스펙릿을 C# 언어 표준으로 채택하는 과정에 대해 자세히 알아볼 수 있습니다.

챔피언 이슈: https://github.com/dotnet/csharplang/issues/4465

요약

지정된 §9.4 및 명확한 할당에는 사용자에게 불편을 초래한 몇 가지 결함이 있습니다. 부울 상수, 조건부 접근 및 널 병합 연산과의 비교를 포함하는 시나리오에서 특히 그렇습니다.

이 제안에 대한 csharplang 토론: https://github.com/dotnet/csharplang/discussions/4240

이 쿼리 또는 유사한 쿼리를 통해 12개 정도의 사용자 보고서를 찾을 수 있습니다(예: "CS0165" 대신 "명확한 할당"을 검색하거나 csharplang에서 검색). https://github.com/dotnet/roslyn/issues?q=is%3Aclosed+is%3Aissue+label%3A%22Resolution-By+Design%22+cs0165

각 시나리오의 상대적 영향을 파악하기 위해 아래 시나리오에 관련 문제를 포함했습니다.

시나리오

참고로, 명확한 과제와 nullable에서 작동하는 잘 알려진 "해피 케이스"로 시작해 보겠습니다.

#nullable enable

C c = new C();
if (c != null && c.M(out object obj0))
{
    obj0.ToString(); // ok
}

public class C
{
    public bool M(out object obj)
    {
        obj = new object();
        return true;
    }
}

bool 상수와 비교

if ((c != null && c.M(out object obj1)) == true)
{
    obj1.ToString(); // undesired error
}

if ((c != null && c.M(out object obj2)) is true)
{
    obj2.ToString(); // undesired error
}

조건부 액세스와 상수 값 간의 비교

이 시나리오는 아마도 가장 큰 시나리오일 것입니다. nullable에서는 지원하지만 명확한 할당에서는 지원하지 않습니다.

if (c?.M(out object obj3) == true)
{
    obj3.ToString(); // undesired error
}

조건부 액세스가 부울 상수에 병합됨

이 시나리오는 이전 시나리오와 매우 유사합니다. nullable에서도 지원되지만 명확한 할당에서는 지원되지 않습니다.

if (c?.M(out object obj4) ?? false)
{
    obj4.ToString(); // undesired error
}

한 쪽 팔이 부울 상수인 조건식

조건 식이 상수인 경우(즉, true ? a : b)에 대한 특별한 동작이 이미 있음을 지적할 필요가 있습니다. 우리는 무조건 일정한 조건으로 표시된 팔을 방문하고 다른 팔을 무시합니다.

또한 nullable에서 이 시나리오를 처리하지 않았습니다.

if (c != null ? c.M(out object obj4) : false)
{
    obj4.ToString(); // undesired error
}

사양

?. (null 조건부 연산자) 식

새 섹션 을 소개합니다. (널 조건부 연산자) 식. 참고로 null 조건부 연산자 사양(§12.8.8) 및 명확한 할당을 결정하는 규칙(§9.4.4)을 참조하세요.

위에 연결된 명확한 할당 규칙에서와 같이 처음에 할당되지 않은 지정된 변수를 v참조합니다.

우리는 "직접 포함"이라는 개념을 소개합니다. 사용자 정의 변환 §10.5이 매개변수가 nullable이 아닌 값 유형이 아닌 경우 적용되지 않고, 다음 조건 중 하나를 만족할 때, 식 E는 하위 식 E1을 "직접 포함"한다고 합니다.

  • E is E1. 예를 들어 a?.b()a?.b()직접 포함합니다.
  • E(E2)괄호로 묶인 식이고, E2E1을 직접 포함하고 있습니다.
  • E null-허용 연산자 식 E2!이고, E2E1을 직접 포함합니다.
  • E(T)E2캐스트 식이고 캐스트가 E2 매개 변수가 nullable이 아닌 값 형식이 아닌 해제되지 않은 사용자 정의 변환을 적용하지 않는 경우 E2 직접 E1포함합니다.

primary_expression null_conditional_operations형태의 E 식의 경우, 선행 구문을 텍스트에서 제거하여 E0 식이 되도록 하십시오. 각 null_conditional_operations 중 조건에 맞는 E의, 위에서 연결된 사양과 같이.

이후 섹션에서는 E0를 null 조건식에 대한 비조건부 대응 항목으로 참조합니다. 후속 섹션의 일부 식에는 피연산자 중 하나에 null 조건식이 직접 포함된 경우에만 적용되는 추가 규칙이 적용됩니다.

  • E 내의 모든 지점에서 v 명확한 할당 상태는 E0내의 해당 지점에서 명확한 할당 상태와 동일합니다.
  • E 이후의 v 명확한 할당 상태는 primary_expressionv 명확한 할당 상태와 동일합니다.

발언

우리가 조건부 액세스를 다른 값과 비교할 때 비교적 간단한 "래퍼" 식을 건너뛸 수 있도록 "직접 포함"이라는 개념을 사용합니다. 예를 들어, ((a?.b(out x))!) == true가 일반적으로 a?.b == true과 동일한 흐름 상태를 발생시킬 것으로 예상됩니다.

또한 조건부 액세스에서 가능한 여러 변환이 있는 상태에서 분석이 작동하도록 허용하려고 합니다. 변환이 사용자 정의일 때는 "null이 아닌 상태"를 전파할 수 없습니다. 이는 입력이 null이 아닐 때만 출력이 null이 아닌 제약 조건을 사용자 정의 변환이 준수할 것이라고 확신할 수 없기 때문입니다. 유일한 예외는 사용자 정의 변환의 입력이 null 가능성이 없는 값 형식인 경우입니다. 예를 들어:

public struct S1 { }
public struct S2 { public static implicit operator S2?(S1 s1) => null; }

여기에는 다음과 같은 해제된 변환도 포함됩니다.

string x;

S1? s1 = null;
_ = s1?.M1(x = "a") ?? s1.Value.M2(x = "a");

x.ToString(); // ok

public struct S1
{
    public S1 M1(object obj) => this;
    public S2 M2(object obj) => new S2();
}
public struct S2
{
    public static implicit operator S2(S1 s1) => default;
}

변수가 null 조건식 내의 지정된 지점에서 할당되는지 여부를 고려할 때 동일한 null 조건식 내의 이전 null 조건부 작업이 성공했다고 가정합니다.

예를 들어 조건식 a?.b(out x)?.c(x)경우 비 조건부 대응은 a.b(out x).c(x). 예를 들어 ?.c(x)전에 x의 명확한 할당 상태를 알고 싶다면, a.b(out x)에 대한 "가상" 분석을 수행한 결과를 ?.c(x)에 입력으로 사용합니다.

부울 상수 표현식

새 섹션 "부울 상수 식"을 소개합니다.

expr 경우 expr 부울 값이 있는 상수 식입니다.

  • exprv 명확한 할당 상태는 다음을 통해 결정됩니다.
    • expr이 true 상수 표현식이고, expr 이전의 v 상태가 "확실히 할당되지 않음"이라면, expr 이후의 v 상태는 "false인 경우에 확실히 할당"됩니다.
    • expr이 false 값을 가진 상수식인 경우, expr 이전에 v의 상태가 "확실히 할당되지 않음"이라면, expr 이후에 v의 상태는 "true일 때 확실히 할당됨"입니다.

발언

예를 들어 식에 상수 값 부울 값 false이 있을 경우, 식이 true을 반환해야 하는 분기에 도달할 수 없다고 가정합니다. 따라서 변수는 이러한 분기점에서 확실히 할당된 것으로 간주됩니다. 이렇게 하면 ???: 같은 식의 사양 변경 내용과 잘 결합되고 많은 유용한 시나리오를 사용할 수 있습니다.

또한 상수 식을 방문하기 전에 조건부 상태에 있을 것으로 예상하지. 따라서 "expr 상수에서 값이 true인 식이며, expr "true일 때 확실히 할당"되기 전에 v 상태가 "확실히 할당됨"과 같은 시나리오는 고려하지 않습니다.

?? (null 병합 식) 확장

다음과 같이 섹션 §9.4.4.29 보강합니다.

형식의 exprexpr_first ?? expr_second:

  • ...
  • exprv 명확한 할당 상태는 다음을 통해 결정됩니다.
    • ...
    • expr_first E null 조건식이 직접 포함되어 있고 v 조건부 대응 E0후에 확실히 할당되는 경우 exprv 명확한 할당 상태는 expr_secondv 명확한 할당 상태와 동일합니다.

발언

위의 규칙은 a?.M(out x) ?? (x = false)같은 식에 대해, a?.M(out x)이 완전히 평가되어 null이 아닌 값을 생성한 경우 x가 할당되며, 그렇지 않으면 x = false이 평가된 경우에는 x가 할당됩니다. 따라서 x 항상 이 식 후에 할당됩니다.

또한 vdict.TryGetValue(key, out var value)후에 확실히 할당되고 vfalse후에 "true일 때 확실히 할당"되는 것을 관찰하고 v "true일 때 확실히 할당"되어야 한다는 결론을 내어 dict?.TryGetValue(key, out var value) ?? false 시나리오를 처리합니다.

보다 일반적인 공식을 사용하면 다음과 같은 좀 더 특이한 시나리오를 처리할 수도 있습니다.

  • if (x?.M(out y) ?? (b && z.M(out y))) y.ToString();
  • if (x?.M(out y) ?? z?.M(out y) ?? false) y.ToString();

?: (조건부) 식

다음과 같이 섹션 §9.4.4.30 보강합니다.

형식의 exprexpr_cond ? expr_true : expr_false:

  • ...
  • exprv 명확한 할당 상태는 다음을 통해 결정됩니다.
    • ...
    • expr_true 이후의 v 상태가 "true일 때 확실히 할당됨"이고, expr_false 이후의 v 상태가 "true일 때 확실히 할당됨"이라면, expr 이후의 v 상태도 "true일 때 확실히 할당됨"입니다.
    • expr_true 후에 v 상태가 "false일 때 확실히 할당되는 경우"이고, expr_false 후에 v 상태도 "false일 때 확실히 할당되는 경우"라면, expr 후에 v의 상태도 "false일 때 확실히 할당됩니다."

발언

조건식의 양쪽 결과가 조건부 상태가 될 때, 상태를 분리하여 최종 상태가 비조건부가 되도록 하는 대신, 해당 조건부 상태들을 결합하여 외부로 전파합니다. 이렇게 하면 다음과 같은 시나리오가 가능합니다.

bool b = true;
object x = null;
int y;
if (b ? x != null && Set(out y) : x != null && Set(out y))
{
  y.ToString();
}

bool Set(out int x) { x = 0; return true; }

이 시나리오는 본래 네이티브 컴파일러에서 오류 없이 컴파일되지만, 당시에 사양과 일치시키기 위해 Roslyn에서 수정된 틈새 시나리오입니다.

==/!= (관계형 같음 연산자) 식

==/!=(관계형 같음 연산자) 식 새 섹션을 소개합니다.

포함된 식이 있는 식에 대한 일반 규칙은 아래 설명된 시나리오를 제외하고 §9.4.4.23 적용됩니다.

== 미리 정의된 비교 연산자(§12.12) 또는 해제된 연산자(§12.4.8)인 양식 expr_first == expr_secondexpr 식의 경우 exprv 명확한 할당 상태는 다음으로 결정됩니다.

  • expr_first가 null 조건식을 E 포함하고 있으며, expr_second는 값이 null인 상수식이고, 비조건적 상대식 E0이 "확실히 할당"된 후의 v의 상태입니다. 그런 다음 expr 후의 v 상태는 "false일 때 확실히 할당" 됩니다.
  • expr_first가 null 조건식을 직접적으로 E로 포함하고 있을 때, expr_second가 null이 될 수 없는 값 타입의 식이거나 null이 아닌 값을 가지는 상수 식일 경우, 비조건부 대항의 E0 후에 v의 상태가 "확실히 할당됨"이라면, exprv의 상태는 "true일 때 확실히 할당됨"입니다.
  • expr_firstboolean형식의 경우이고, expr_second 가 true 값을 가진 상수 식인 경우에, expr 이후의 명확한 할당 상태는 expr_first이후의 명확한 할당 상태와 동일합니다.
  • expr_first 부울 형식이고 expr_second 값이 false 상수 식인 경우 expr 이후의 명확한 할당 상태는 논리 부정 식이 !expr_firstv 명확한 할당 상태와 동일합니다.

!= 미리 정의된 비교 연산자(§12.12) 또는 해제된 연산자((§12.4.8))인 양식 expr_first != expr_secondexpr 식의 경우 exprv 명확한 할당 상태가 다음과 같이 결정됩니다.

  • expr_first가 직접적으로 null 조건식 E을 포함하고, 상수 식 expr_second의 값이 null일 때, 비 조건부 대응 E0 이후 v의 상태가 "확실히 할당"되었다면, 그런 다음 expr 이후 v의 상태는 "true일 때 확실히 할당"됩니다.
  • expr_first가 직접적으로 null 조건식 E을 포함하고, expr_second가 nullable이 아닌 값 형식으로 된 식인 경우 또는 null이 아닌 값을 가진 상수 식이며 비 조건부 대응 E0 후에 v의 상태가 "확실히 할당"되었다면, expr 후에 v의 상태는 "false일 때 확실히 할당"됩니다.
  • expr_first 부울 형식이고 expr_second 값이 true 상수 식인 경우 expr 이후의 명확한 할당 상태는 논리 부정 식이 !expr_firstv 명확한 할당 상태와 동일합니다.
  • expr_first 이 부울 형식이고, expr_second 가 값이 false인 상수 표현식인 경우, expr 이후의 명확한 할당 상태는 expr_first후의 명확한 할당 상태와 동일합니다.

이 섹션의 위의 모든 규칙은 커밋됩니다. 즉, expr_second op expr_first양식에서 평가할 때 규칙이 적용되는 경우 expr_first op expr_second양식에도 적용됩니다.

발언

이러한 규칙으로 표현되는 일반적인 개념은 다음과 같습니다.

  • 조건부 액세스가 null와 비교되는 경우, 비교 결과가 false일 때 작업이 확실히 발생했음을 알 수 있습니다.
  • 조건부 액세스가 비nullable 값 형식 또는 null이 아닌 상수와 비교되는 경우, 비교 결과가 true일 경우 해당 작업이 확실히 발생했음을 알 수 있습니다.
  • 사용자 정의 연산자를 신뢰하여 초기화 안전성이 우려되는 신뢰할 수 있는 답변을 제공할 수 없으므로 새 규칙은 미리 정의된 ==/!= 연산자를 사용하는 경우에만 적용됩니다.

결국 멤버 액세스 또는 호출의 끝에 있는 조건부 상태를 통해 스레드로 이러한 규칙을 구체화할 수 있습니다. 이러한 시나리오는 명확한 할당에서 실제로 발생하지 않지만 [NotNullWhen(true)] 및 유사한 특성이 있는 경우 nullable에서 발생합니다. 이렇게 하려면 null/null이 아닌 상수에 대한 처리 외에 bool 상수에 대한 특별한 처리가 필요합니다.

이러한 규칙의 일부 결과:

  • 'else' 분기에서 if (a?.b(out var x) == true)) x() else x(); 코드에 오류가 발생합니다.
  • if (a?.b(out var x) == 42)) x() else x(); 'else' 조건에서 오류가 발생합니다.
  • if (a?.b(out var x) == false)) x() else x();는 'else' 분기에서 오류를 일으킵니다.
  • if (a?.b(out var x) == null)) x() else x();는 'then' 분기에서 오류가 날 것입니다.
  • if (a?.b(out var x) != true)) x() else x();는 '그렇다면' 분기에서 오류가 발생합니다.
  • if (a?.b(out var x) != 42)) x() else x();는 'then' 분기에서 오류가 발생합니다.
  • if (a?.b(out var x) != false)) x() else x();는 'then' 분기에서 오류를 일으킬 것입니다.
  • if (a?.b(out var x) != null)) x() else x();는 'else' 분기에서 오류를 발생시킵니다.

is 연산자 및 is 패턴 식

새 섹션is 연산자 및 is 패턴 식을 소개합니다.

E is T양식의 expr 식의 경우 T 형식 또는 패턴입니다.

  • E 이전의 v 명확한 할당 상태는 expr전에 v 명확한 할당 상태와 동일합니다.
  • exprv 명확한 할당 상태는 다음을 통해 결정됩니다.
    • E에 null 조건 표현식이 직접 포함되어 있고, 비 조건부 대응 E0 이후에 v 상태가 "확실히 할당된" 경우, Tnull 입력과 일치하지 않는 모든 유형이나 패턴이라면, expr 이후의 v 상태는 "참일 때 확실히 할당된" 상태가 됩니다.
    • E가 null 조건식을 직접 포함하고 있으며, 비 조건부 대응인 E0 이후 v의 상태가 "확실히 할당"되는 경우, 그리고 Tnull 입력과 일치하는 패턴이라면, expr 이후 v의 상태는 "false일 때 확실히 할당"됩니다.
    • E이 boolean 형식이고 Ttrue 입력과만 일치하는 패턴인 경우, expr 후의 v 명확한 할당 상태는 E 이후 v의 명확한 할당 상태와 동일합니다.
    • E이 부울 형식이고 Tfalse 입력만을 일치시키는 패턴인 경우, expr 후의 변수 v의 확정된 할당 상태는 논리적 부정 표현식 !expr후의 변수 v의 확정된 할당 상태와 동일합니다.
    • 그렇지 않으면 E 이후의 v 명백한 할당 상태가 "분명히 할당"된 경우 exprv 명백한 할당 상태는 "분명히 할당"됩니다.

발언

이 섹션은 위의 ==/!= 섹션에서와 유사한 시나리오를 해결하기 위한 것입니다. 이 사양은 재귀 패턴(예: (a?.b(out x), c?.d(out y)) is (object, object))을 다루지 않습니다. 이러한 지원은 시간이 허락하는 경우 나중에 올 수 있습니다.

추가 시나리오

이 사양은 현재 패턴 스위치 식 및 switch 문과 관련된 시나리오를 다루지 않습니다. 예를 들어:

_ = c?.M(out object obj4) switch
{
    not null => obj4.ToString() // undesired error
};

시간이 허락한다면 이에 대한 지원이 나중에 올 수 있을 것 같습니다.

nullable에 대해 여러 범주의 버그가 제기되어 기본적으로 패턴 분석의 정교함을 높여야 합니다. 명확한 임무를 개선하는 모든 판결도 nullable로 이월될 가능성이 높습니다.

https://github.com/dotnet/roslyn/issues/49353
https://github.com/dotnet/roslyn/issues/46819
https://github.com/dotnet/roslyn/issues/44127

단점

일반적으로 흐름 분석 상태는 위로 전파되도록 되어 있지만, 분석이 "아래로 내려가서" 조건부 액세스를 특별히 인식하도록 하는 방식이 이상하게 느껴집니다. 이와 같은 솔루션이 null 검사를 수행하는 향후 언어 기능과 어떻게 고통스럽게 교차할 수 있는지 우려하고 있습니다.

대안

이 제안의 두 가지 대안:

  1. 언어 및 컴파일러에 "null인 경우 상태" 및 "null이 아닌 상태"를 소개합니다. 우리가 해결하려는 시나리오에 비해 지나치게 많은 노력이 필요하다고 판단되었지만, 위의 제안을 잠재적으로 구현한 후, 추후 사람들에게 혼란을 주지 않고 "null/미입력" 모델로 전환할 수 있을 것입니다.
  2. 아무 것도 하지 않습니다.

해결되지 않은 질문

지정해야 하는 스위치 식에 영향을 줍니다. https://github.com/dotnet/csharplang/discussions/4240#discussioncomment-343395

디자인 회의

https://github.com/dotnet/csharplang/discussions/4243