proposals/csharp-14.0/null-conditional-assignment.md
[!INCLUDESpecletdisclaimer]
Champion issue: https://github.com/dotnet/csharplang/issues/8677
Permits assignment to occur conditionally within a a?.b or a?[b] expression.
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();
}
A variety of motivating use cases can be found in the championed issue. Major motivations include:
Set() methods.// 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
}
ref to them.M(ref a?.b); // error
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 proposes relaxing this, in which case we could define a reasonable behavior for a?.b = c, when a is a System.Nullable<T> and b is a property with a readonly setter.
The null conditional assignment grammar is defined as follows:
null_conditional_assignment
: null_conditional_member_access assignment_operator expression
: null_conditional_element_access assignment_operator expression
See §11.7.7 and §11.7.11 for reference.
When the null conditional assignment appears in an expression-statement, its semantics are as follows:
P?.A = B is equivalent to if (P is not null) P.A = B;, except that P is only evaluated once.P?[A] = B is equivalent to if (P is not null) P[A] = B, except that P is only evaluated once.Otherwise, its semantics are as follows:
P?.A = B is equivalent to (P is null) ? (T?)null : (P.A = B), where T is the result type of P.A = B, except that P is only evaluated once.P?[A] = B is equivalent to (P is null) ? (T?)null : (P[A] = B), where T is the result type of P[A] = B, except that P is only evaluated once.The grammar in the standard currently doesn't correspond strongly to the syntax design used in the implementation. We expect that to remain the case after this feature is implemented. The syntax design in the implementation isn't expected to actually change--only the way it is used will change. For example:
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;
The choice to keep the assignment within the conditional access introduces some additional work for the IDE, which has many code paths which need to work backwards from an assignment to identifying the thing being assigned.
We could instead make the ?. syntactically a child of the =. This makes it so any handling of = expressions needs to become aware of the conditionality of the right side in the presence of ?. on the left. It also makes it so the structure of the syntax doesn't correspond as strongly to the semantics.