Automatische Standardstrukturen
Anmerkung
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 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.
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();
}
}
Vorherige Diskussion
Eine kleine Gruppe hat sich dieses Problem angesehen und einige mögliche Lösungen berücksichtigt:
- Benutzer müssen
this = default
zuweisen, wenn halbautomatische Eigenschaften manuell implementierte Setzer haben. Wir stimmen zu, dass dies die falsche Lösung ist, da sie Werte, die in Feldinitialisierern festgelegt sind, wegweht. - 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: "Initialisieren Sie meine Felder nicht implizit, aber initialisieren Sie meine Auto-Eigenschaften implizit."
- 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 Beispielen (4) und (5) weist der resultierende Codegen manchmal "doppelte Zuordnungen" von Feldern auf. 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 kann fixierbar sein. https://github.com/dotnet/runtime/issues/13727
Es lohnt sich zu beachten, dass das Initialisieren einzelner Felder anstelle der gesamten Instanz wirklich 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, ein initobj
auf das gesamte Objekt anzuwenden, als implizit initobj
für die 99 anderen Felder zu verwenden. Eine Implementierung, die implizit initobj
für die 99 anderen Felder ausgibt, wäre jedoch weiterhin 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 aufweist, verhält sich die
this
Variable genauso wie einout
Parameter des Strukturtyps. Dies bedeutet insbesondere, dass die Variable in jedem Ausführungspfad des Instanzkonstruktors definitiv zugewiesen werden soll.
Wir passen diese Sprache an Folgendes an:
Wenn die Konstruktordeklaration keinen Konstruktorinitialisierer hat, verhält sich die this
-Variable ähnlich wie ein out
-Parameter des Strukturtyps, jedoch ist es kein Fehler, wenn die Anforderungen an die endgültige 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 Punkten, an denen die Anforderungen verletzt werden, implizit auf den Standardwert (§9.3) in einer Initialisierungsphase initialisiert, bevor anderer Code im Konstruktor ausgeführt wird. - Wenn eine Instanzvariable v innerhalb
this
die Anforderungen nicht erfüllt oder eine Instanzvariable auf einer beliebigen Schachtelungsebene innerhalb v die Anforderungen nicht erfüllt, wird v implizit während der Initialisierungsphase auf den Standardwert gesetzt, bevor anderer Code im Konstruktor ausgeführt wird.
Design-Meetings
C# feature specifications