Automatische Standardstrukturen
Hinweis
Dieser Artikel ist eine Feature-Spezifikation. 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 Feature-Spezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede sind in den entsprechenden Hinweisen zum Language Design Meeting (LDM) festgehalten.
Mehr über den Prozess der Adaption von Funktionen in den C#-Sprachstandard erfahren Sie in dem Artikel über die Spezifikationen.
Champion Issue: https://github.com/dotnet/csharplang/issues/5737
Zusammenfassung
Diese Funktion sorgt dafür, dass in Struct-Konstruktoren Felder, die vom Benutzer vor der Rückgabe oder vor der Verwendung nicht explizit zugewiesen wurden, identifiziert und implizit mit default
initialisiert werden, anstatt eindeutige Zuweisungsfehler zu verursachen.
Motivation
Dieser Vorschlag dient als mögliche Behebung der Probleme mit der Benutzerfreundlichkeit, die in dotnet/csharplang#5552 und dotnet/csharplang#5635 festgestellt wurden, sowie der Behebung von #5563 (alle Felder müssen definitiv zugewiesen werden, aber field
ist innerhalb des Konstruktors nicht zugreifbar).
Seit C# 1.0 müssen Struct-Konstruktoren this
definitiv zuweisen, als wäre es ein out
-Parameter.
public struct S
{
public int x, y;
public S() // error: Fields 'S.x' and 'S.y' must be fully assigned before control is returned to the caller
{
}
}
Dies führt zu Problemen, wenn Setter für halbautomatische Eigenschaften manuell definiert werden, da der Compiler die Zuweisung der Eigenschaft nicht mit der Zuweisung des Backing-Feldes gleichsetzen kann.
public struct S
{
public int X { get => field; set => field = value; }
public S() // error: struct fields aren't fully assigned. But caller can only assign 'this.field' by assigning 'this'.
{
}
}
Wir gehen davon aus, dass die Einführung detaillierterer Einschränkungen für Setter, wie z. B. ein Schema, bei dem der Setter nicht ref this
, sondern out field
als Parameter annimmt, für einige Anwendungsfälle zu nischenhaft und unvollständig sein wird.
Ein grundsätzliches Problem, mit dem wir zu kämpfen haben, ist, dass Benutzer bei Struct-Eigenschaften mit manuell implementierten Settern oft eine Art „Wiederholung“ vornehmen müssen, indem sie ihre Logik entweder wiederholt zuweisen oder wiederholen:
struct S
{
private int _x;
public int X
{
get => _x;
set => _x = value >= 0 ? value : throw new ArgumentOutOfRangeException();
}
// Solution 1: assign some value in the constructor before "really" assigning through the property setter.
public S(int x)
{
_x = default;
X = x;
}
// Solution 2: assign the field once in the constructor, repeating the implementation of the setter.
public S(int x)
{
_x = x >= 0 ? x : throw new ArgumentOutOfRangeException();
}
}
Frühere Diskussion
Eine kleine Gruppe hat sich mit diesem Problem befasst und einige mögliche Lösungen in Betracht gezogen:
- Benutzer müssen
this = default
zuweisen, wenn halbautomatische Eigenschaften manuell implementierte Setter haben. Wir sind uns einig, dass dies die falsche Lösung ist, da sie die in Feldinitialisierern festgelegten Werte ausblendet. - Alle Backing-Felder von auto/semi-auto-Eigenschaften implizit initialisieren.
- Dies löst das Problem der „halbautomatischen Eigenschafts-Setter"“ und stellt explizit deklarierte Felder unter andere Regeln: “Meine Felder nicht implizit initialisieren, aber meine Auto-Eigenschaften implizit initialisieren“.
- Bieten Sie eine Möglichkeit, das Backing-Feld einer halbautomatischen Eigenschaft zuzuweisen und verlangen Sie, dass der Benutzer es zuweist.
- Dies könnte im Vergleich zu (2) umständlich sein. Eine Auto-Eigenschaft soll „automatisch“ sein, und dazu gehört vielleicht auch die „automatische“ Initialisierung des Feldes. Dies könnte zu Verwirrung darüber führen, wann das zugrundeliegende Feld durch eine Zuweisung der Eigenschaft zugewiesen wird und wann der Eigenschafts-Setter aufgerufen wird.
Wir haben auch Feedback von Benutzenden erhalten, die z. B. einige Feldinitialisierungen in Structs aufnehmen wollen, ohne alles explizit zuweisen zu müssen. Wir können dieses Problem sowie das Problem der „halbautomatischen Eigenschaft mit manuell implementiertem Setter“ gleichzeitig lösen.
struct MagnitudeVector3d
{
double X, Y, Z;
double Magnitude = 1;
public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
{
}
}
Anpassen der definitiven Zuweisung
Anstatt eine definitive Zuweisungsanalyse durchzuführen, um Fehler für nicht zugewiesene Felder auf this
zu geben, tun wir dies, um zu bestimmen welche Felder implizit initialisiert werden müssen. Eine solche Initialisierung wird am Anfang des Konstruktors eingefügt.
struct S
{
int x, y;
// Example 1
public S()
{
// ok. Compiler inserts an assignment of `this = default`.
}
// Example 2
public S()
{
// ok. Compiler inserts an assignment of `y = default`.
x = 1;
}
// Example 3
public S()
{
// valid since C# 1.0. Compiler inserts no implicit assignments.
x = 1;
y = 2;
}
// Example 4
public S(bool b)
{
// ok. Compiler inserts assignment of `this = default`.
if (b)
x = 1;
else
y = 2;
}
// Example 5
void M() { }
public S(bool b)
{
// ok. Compiler inserts assignment of `y = default`.
x = 1;
if (b)
M();
y = 2;
}
}
In den Beispielen (4) und (5) hat der resultierende Codegen manchmal "doppelte Zuweisungen" von Feldern. Dies ist im Allgemeinen in Ordnung, aber für Benutzer, die sich mit solchen doppelten Zuweisungen befassen, können wir das, was früher eindeutige Zuweisungsfehlerdiagnosen waren, als disabled-by-default-Warndiagnosen ausgeben.
struct S
{
int x;
public S() // warning: 'S.x' is implicitly initialized to 'default'.
{
}
}
Benutzer, die den Schweregrad dieser Diagnose auf „Fehler“" festlegen, entscheiden sich für das Verhalten vor C# 11. Solche Benutzer sind im Wesentlichen von halbautomatischen Eigenschaften mit manuell implementierten Settern „ausgeschlossen“.
struct S
{
public int X
{
get => field;
set => field = field < value ? value : field;
}
public S() // error: backing field of 'S.X' is implicitly initialized to 'default'.
{
X = 1;
}
}
Auf den ersten Blick fühlt sich das wie ein „Loch“ in der Funktion an, aber es ist eigentlich die richtige Entscheidung. Durch Aktivieren der Diagnose teilt der Benutzer uns mit, dass der Compiler seine Felder im Konstruktor nicht implizit initialisieren soll. Es gibt keine Möglichkeit, die implizite Initialisierung hier zu vermeiden, sodass die Lösung für sie darin besteht, eine andere Art der Initialisierung des Feldes als einen manuell implementierten Setter zu verwenden, wie z. B. das Feld manuell zu deklarieren und zuzuweisen oder einen Feldinitialisierer einzubinden.
Derzeit eliminiert die JIT tote Stores nicht durch Refs, was bedeutet, dass diese impliziten Initialisierungen echte Kosten verursachen. Aber das könnte behebbar sein. https://github.com/dotnet/runtime/issues/13727
Es ist erwähnenswert, dass die Initialisierung einzelner Felder anstelle der gesamten Instanz eigentlich nur eine Optimierung ist. Der Compiler sollte wahrscheinlich kostenlos jede beliebige Heuristik implementieren können, solange er die Invariante erfüllt, dass Felder, die nicht an allen Rückgabestellen oder vor einem Nicht-Feld-Mitgliedszugriff von this
definitiv zugewiesen werden, implizit initialisiert werden.
Wenn eine Struktur beispielsweise 100 Felder enthält und nur eines davon explizit initialisiert wird, kann es sinnvoller sein, eine initobj
auf das gesamte Objekt anzuwenden, als implizit initobj
für die 99 übrigen Felder auszugeben. Eine Implementierung, die implizit initobj
für die 99 anderen Felder ausgibt, wäre jedoch immer noch gültig.
Änderungen an der Sprachspezifikation
Wir passen den folgenden Abschnitt des Standards an:
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12814-this-access
Wenn die Konstruktordeklaration keinen Konstruktorinitialisierer hat, verhält sich die
this
-Variable genau wie einout
-Parameter des Typs Struct. Das bedeutet insbesondere, dass die Variable in jedem Ausführungspfad des Instanz-Konstruktors definitiv zugewiesen werden muss.
Wir passen die Sprache so an, dass sie lautet:
Wenn die Konstruktordeklaration keinen Konstruktorinitialisierer aufweist, verhält sich die this
-Variable ähnlich einem out
-Parameter des Strukturtyps, außer dass es kein Fehler ist, wenn die Anforderungen der definitiven Zuweisung (§9.4.1) nicht erfüllt sind. Stattdessen führen wir die folgenden Verhaltensweisen ein:
- Wenn die
this
Variable selbst die Anforderungen nicht erfüllt, werden alle nicht zugewiesenen Instanzvariablen innerhalb vonthis
an allen Stellen, an denen Anforderungen verletzt werden, implizit auf den Standardwert (§9.3) in einer Initialisierungsphase initialisiert, bevor ein anderer Code im Konstruktor ausgeführt wird. - Wenn die Instanzvariable v innerhalb von
this
die Anforderungen nicht erfüllt oder eine Instanzvariable auf einer beliebigen Verschachtelungsebene innerhalb von v die Anforderungen nicht erfüllt, wird v implizit in der Initialisierungsphase zum Standardwert initialisiert, bevor anderer Code im Konstruktor ausgeführt wird.
Designbesprechungen
C# feature specifications