注释
本文是功能规格说明。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 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
}
- 条件访问表达式仍然不是左值,它仍然不允许使用,例如获取
ref
它们。
M(ref a?.b); // error
- 不允许将它重新分配给条件访问。 主要原因是,有条件地访问 ref 变量的唯一方法是 ref 字段,并且禁止 ref 结构在可为 null 的值类型中使用。 如果将来出现了条件 ref 分配的有效方案,我们可以在该时间添加支持。
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>
b
具有只读 setter 的属性。
规格
null 条件分配语法的定义如下:
null_conditional_assignment
: null_conditional_member_access assignment_operator expression
: null_conditional_element_access assignment_operator expression
当 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