다음을 통해 공유


Null 조건부 할당

비고

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

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

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

챔피언 발행호: https://github.com/dotnet/csharplang/issues/8677

요약

a?.b 또는 a?[b] 식 내에서 할당이 조건부로 발생하도록 허용합니다.

using System;

class C
{
    public object obj;
}

void M(C? c)
{
    c?.obj = new object();
}
using System;

class C
{
    public event Action E;
}

void M(C? c)
{
    c?.E += () => { Console.WriteLine("handled event E"); };
}
void M(object[]? arr)
{
    arr?[42] = new object();
}

동기

다양한 동기 부여 사용 사례는 옹호된 문제에서 찾을 수 있습니다. 주요 동기는 다음과 같습니다.

  1. 속성과 Set() 메서드 간의 패리티입니다.
  2. UI 코드에서 이벤트 처리기 연결

상세 디자인

  • 할당의 오른쪽은 조건부 액세스의 수신자가 null이 아닌 경우에만 평가됩니다.
// M() is only executed if 'a' is non-null.
// note: the value of 'a.b' doesn't affect whether things are evaluated here.
a?.b = M();
  • 모든 형태의 복합 할당이 허용됩니다.
a?.b -= M(); // ok
a?.b += M(); // ok
// etc.
  • 식의 결과를 사용하는 경우 식의 형식은 값 형식 또는 참조 형식으로 알려져 있어야 합니다. 이는 조건부 액세스의 기존 동작과 일치합니다.
class C<T>
{
    public T? field;
}

void M1<T>(C<T>? c, T t)
{
    (c?.field = t).ToString(); // error: 'T' cannot be made nullable.
    c?.field = t; // ok
}
  • 조건부 액세스 식은 여전히 lvalue가 아니며, 예를 들어 ref 연산을 수행하는 것이 여전히 허용되지 않습니다.
M(ref a?.b); // error
  • 조건부 액세스에 ref-assign할 수 없습니다. 주된 이유는 조건부로 ref 변수에 액세스하는 유일한 방법은 ref 필드이고 ref 구조체는 nullable 값 형식에서 사용할 수 없으므로 나중에 조건부 참조 할당에 대한 유효한 시나리오가 나온 경우 해당 시점에 지원을 추가할 수 있습니다.
ref struct RS
{
    public ref int b;
}

void M(RS a, ref int x)
{
  a?.b = ref x; // error: Operator '?' can't be applied to operand of type 'RS'.
}
  • 예를 들어 분해 할당을 통해 조건부 액세스에 할당할 수 없습니다. 우리는 사람들이 이 작업을 수행하기를 원하는 경우는 드물며, 대신 여러 개의 별도 할당 식을 통해 수행해야 하는 중요한 단점이 아닐 것으로 예상합니다.
(a?.b, c?.d) = (x, y); // error
  • 증가/감소 연산자는 지원되지 않습니다.
a?.b++; // error
--a?.b; // error
  • 조건부 액세스의 수신자가 값 형식인 경우 이 기능은 일반적으로 작동하지 않습니다. 이는 다음 두 가지 경우 중 하나에 속하기 때문입니다.
void Case1(MyStruct a)
    => a?.b = c; // a?.b is not allowed when 'a' is of non-nullable value type

void Case2(MyStruct? a)
    => a?.b = c; // `a.Value` is not a variable, so there's no reasonable meaning to define for the assignment

readonly-setter-calls-on-non-variables.md 는 이것을 완화할 것을 제안합니다. 이 경우 a?.b = c, a, System.Nullable<T>가 읽기 전용 setter를 가지는 속성일 때 적절한 동작을 정의할 수 있습니다.

규격

null 조건부 할당 문법은 다음과 같이 정의됩니다.

null_conditional_assignment
    : null_conditional_member_access assignment_operator expression
    : null_conditional_element_access assignment_operator expression

참조는 §11.7.7§11.7.11 을 참조하세요.

식 문에 null 조건부 할당 이 나타나면 해당 의미 체계는 다음과 같습니다.

  • P?.A = B은 한 번만 평가된다는 점을 제외하면 if (P is not null) P.A = B; 동일합니다P.
  • P?[A] = B은 한 번만 평가된다는 점을 제외하면 if (P is not null) P[A] = B 동일합니다P.

그렇지 않으면 의미 체계는 다음과 같습니다.

  • P?.A = B(P is null) ? (T?)null : (P.A = B)와 동일하며, T의 결과 형식은 P.A = B이지만, 단 P는 한 번만 평가됩니다.
  • P?[A] = B(P is null) ? (T?)null : (P[A] = B)와 동일하며, T의 결과 형식은 P[A] = B이지만, 단 P는 한 번만 평가됩니다.

이행

현재 표준의 문법은 구현에 사용되는 구문 디자인과 강하게 일치하지 않습니다. 이 기능이 구현된 후에도 계속 적용될 것으로 예상됩니다. 구현의 구문 디자인은 실제로 변경될 것으로 예상되지 않으며 사용되는 방식만 변경됩니다. 다음은 그 예입니다.

graph TD;
subgraph ConditionalAccessExpression
  whole[a?.b = c]
end
subgraph  
  subgraph WhenNotNull
    whole-->whenNotNull[".b = c"];
    whenNotNull-->.b;
    whenNotNull-->eq[=];
    whenNotNull-->c;
  end
  subgraph OperatorToken
    whole-->?;
  end
  subgraph Expression
    whole-->a;
  end
end

복잡한 예제

class C
{
    ref int M() => /*...*/;
}

void M1(C? c)
{
    c?.M() = 42; // equivalent to:
    if (c is not null)
        c.M() = 42;
}

int? M2(C? c)
{
    return c?.M() = 42; // equivalent to:
    return c is null ? (int?)null : c.M() = 42;
}
M(a?.b?.c = d); // equivalent to:
M(a is null
    ? null
    : (a.b is null
        ? null
        : (a.b.c = d)));
return a?.b = c?.d = e?.f; // equivalent to:
return a?.b = (c?.d = e?.f); // equivalent to:
return a is null
    ? null
    : (a.b = c is null
        ? null
        : (c.d = e is null
            ? null
            : e.f));
}
a?.b ??= c; // equivalent to:
if (a is not null)
{
    if (a.b is null)
    {
        a.b = c;
    }
}

return a?.b ??= c; // equivalent to:
return a is null
    ? null
    : a.b is null
        ? a.b = c
        : a.b;

단점

조건부 액세스 내에서 할당을 유지하기로 한 선택은 IDE에 추가로 요구되는 작업을 도입합니다. 이는 IDE가 할당된 대상을 식별하기 위해 역방향으로 작동해야 하는 많은 코드 경로를 필요로 하기 때문입니다.

대안

우리는 대신 ?.를 구문적으로 =의 자식으로 만들 수 있습니다. 이렇게 하면 모든 = 표현의 처리가 왼쪽의 ?. 존재에 따라 오른쪽의 조건부를 인식해야 합니다. 또한 구문의 구조가 의미 체계와 강하게 일치하지 않도록 합니다.

해결되지 않은 질문

디자인 회의