비고
이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 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();
}
동기
다양한 동기 부여 사용 사례는 옹호된 문제에서 찾을 수 있습니다. 주요 동기는 다음과 같습니다.
- 속성과
Set()
메서드 간의 패리티입니다. - 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가 할당된 대상을 식별하기 위해 역방향으로 작동해야 하는 많은 코드 경로를 필요로 하기 때문입니다.
대안
우리는 대신 ?.
를 구문적으로 =
의 자식으로 만들 수 있습니다. 이렇게 하면 모든 =
표현의 처리가 왼쪽의 ?.
존재에 따라 오른쪽의 조건부를 인식해야 합니다. 또한 구문의 구조가 의미 체계와 강하게 일치하지 않도록 합니다.
해결되지 않은 질문
디자인 회의
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-04-27.md#null-conditional-assignment
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-08-31.md#null-conditional-assignment
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-10-26.md#null-conditional-assignment
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-10-28.md#increment-and-decrement-operators-in-null-conditional-access
C# feature specifications