Freigeben über


Null-bedingte Zuweisung

Hinweis

Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.

Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den relevanten Sprachentwurfsbesprechungen (LDM)-Notizen erfasst.

Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.

Champion Issue: https://github.com/dotnet/csharplang/issues/8677

Zusammenfassung

Ermöglicht die Zuweisung bedingt innerhalb eines a?.b oder a?[b] Ausdrucks.

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();
}

Motivation

Eine Vielzahl motivierender Anwendungsfälle finden Sie in der Problemfrage. Zu den wichtigsten Motivationen gehören:

  1. Parität zwischen Eigenschaften und Set() Methoden.
  2. Anfügen von Ereignishandlern im UI-Code.

Detailliertes Design

  • Die rechte Seite der Zuweisung wird nur ausgewertet, wenn der Empfänger des bedingten Zugriffs nicht Null ist.
// 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();
  • Alle Formen der zusammengesetzten Zuordnung sind zulässig.
a?.b -= M(); // ok
a?.b += M(); // ok
// etc.
  • Wenn das Ergebnis des Ausdrucks verwendet wird, muss bekannt sein, dass der Typ des Ausdrucks ein Werttyp oder Verweistyp ist. Dies entspricht den vorhandenen Verhaltensweisen für bedingten Zugriff.
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
}
  • Bedingte Zugriffsausdrücke sind immer noch keine L-Werte und es ist immer noch nicht zulässig, ihnen beispielsweise einen ref zuweisen.
M(ref a?.b); // error
  • Es ist nicht zulässig, eine Referenzzuweisung an einen bedingten Zugriff vorzunehmen. Der Hauptgrund hierfür ist, dass der einzige Zugriff auf eine ref-Variable nur über ein ref-Feld erfolgen kann, und ref-Strukturen dürfen in nullable Wertetypen nicht verwendet werden. Wenn in Zukunft ein gültiges Szenario für eine bedingte Verweiszuweisung auftritt, könnten wir zu diesem Zeitpunkt Unterstützung hinzufügen.
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'.
}
  • Es ist z.B. nicht möglich, bedingten Zugriffen durch Destrukturzuweisung zuzuweisen. Wir gehen davon aus, dass es selten vorkommen wird, dass Menschen dies tun möchten, und dass es kein bedeutender Nachteil ist, was stattdessen erfordert, dies über mehrere separate Zuweisungsausdrücke zu tun.
(a?.b, c?.d) = (x, y); // error
a?.b++; // error
--a?.b; // error
  • Dieses Feature funktioniert in der Regel nicht, wenn der Empfänger des bedingten Zugriffs ein Werttyp ist. Dies liegt daran, dass sie in einen der folgenden beiden Fälle fällt:
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 schlägt vor, dies zu lockern. In diesem Fall könnten wir ein sinnvolles Verhalten für a?.b = c definieren, wenn a ein System.Nullable<T> ist und b eine Eigenschaft mit einem schreibgeschützten Setter ist.

Spezifikation

Die Grammatik für die nullbedingte Zuweisung ist wie folgt definiert:

null_conditional_assignment
    : null_conditional_member_access assignment_operator expression
    : null_conditional_element_access assignment_operator expression

Referenz finden Sie unter §11.7.7 und §11.7.11 .

Wenn die bedingte NULL-Zuordnung in einer Ausdrucksanweisung vorkommt, ist die Semantik wie folgt:

  • P?.A = B ist gleichbedeutend mit if (P is not null) P.A = B;der Ausnahme, dass P nur einmal ausgewertet wird.
  • P?[A] = B ist gleichbedeutend mit if (P is not null) P[A] = Bder Ausnahme, dass P nur einmal ausgewertet wird.

Andernfalls ist die Semantik wie folgt:

  • P?.A = B ist äquivalent zu (P is null) ? (T?)null : (P.A = B), wobei T der Ergebnistyp von P.A = B ist, mit der Ausnahme, dass P nur einmal ausgewertet wird.
  • P?[A] = B ist äquivalent zu (P is null) ? (T?)null : (P[A] = B), wobei T der Ergebnistyp von P[A] = B ist, mit der Ausnahme, dass P nur einmal ausgewertet wird.

Implementierung

Die Grammatik im Standard entspricht derzeit nicht stark dem syntaxdesign, das in der Implementierung verwendet wird. Wir erwarten, dass dies nach der Implementierung dieses Features weiterhin der Fall sein wird. Der Syntaxentwurf in der Implementierung wird nicht erwartet, dass er sich tatsächlich ändert – nur die Art und Weise, wie sie verwendet wird, ändert sich. Beispiel:

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

Komplexe Beispiele

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;

Nachteile

Die Wahl, die Zuweisung im bedingten Zugriff beizubehalten, führt einige zusätzliche Arbeit für die IDE ein, die viele Codepfade enthält, die von einer Aufgabe rückwärts arbeiten müssen, um das zugewiesene Objekt zu identifizieren.

Alternativen

Wir könnten stattdessen die ?. syntaktisch als ein Kind von = machen. Dies erfordert, dass die Behandlung von =-Ausdrücken die Bedingtheit der rechten Seite beachten muss, wenn ?. auf der linken Seite vorhanden ist. Sie macht es auch so, dass die Struktur der Syntax nicht so stark der Semantik entspricht.

Ungelöste Fragen

Planungsbesprechungen