備註
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。
功能規格與已完成實作之間可能有一些差異。 這些差異已記錄在相關的 語言設計會議(LDM)備忘錄中。
您可以在 規範的文章中深入瞭解將功能規範納入 C# 語言標準的過程。
Champion 期數: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-assign 指派給條件式存取。 主要原因是您有條件地存取 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 條件式指派出現在 expression-statement 中時,其語意如下所示:
-
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引進一些額外的工作,其中有許多程式碼路徑需要從指派向後工作,以識別所指派的專案。
替代選擇
我們可以讓?.在語法上成為=的一部分。 如此一來,任何對 = 表達式的處理,都必須在左側存在 ?. 時,考量右邊的條件性。 它也會導致語法結構不像語意那樣強烈地對應。
未解決的問題
設計會議
- 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