Automatiska standardstrukturer
Notis
Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.
Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader återges i de relevanta anteckningarna från LDM (Language Design Meeting).
Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.
Champion-fråga: https://github.com/dotnet/csharplang/issues/5737
Sammanfattning
Den här funktionen medför att vi i struct-konstruktorer identifierar fält som inte uttryckligen tilldelats av användaren innan de returneras eller används, och initierar dem implicit till default
istället för att ge definitiva tilldelningsfel.
Motivation
Det här förslaget lyfts fram som en möjlig åtgärd av användbarhetsproblem som finns i dotnet/csharplang#5552 och dotnet/csharplang#5635, samt för att adressera #5563 (alla fält måste definitivt tilldelas, men field
är inte tillgänglig inom konstruktorn).
Sedan C# 1.0 har struct-konstruktorer varit tvungna att tilldela this
på ett bestämt sätt som om det vore en 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
{
}
}
Detta ger upphov till problem när setters definieras manuellt för halvautomatiska egenskaper, eftersom kompilatorn inte kan behandla tilldelningen av egenskapen som likvärdig med tilldelningen av bakgrundsfältet.
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'.
{
}
}
Vi antar att införandet av mer detaljerade begränsningar för setters, till exempel ett schema där setter inte tar ref this
utan snarare tar out field
som en parameter, kommer att vara för nischat och ofullständigt för vissa användningsfall.
En grundläggande utmaning vi kämpar med är att när struct-egenskaper har genomförts manuellt måste användarna ofta göra någon form av "upprepning," antingen genom att upprepade gånger tilldela eller upprepa sin logik.
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();
}
}
Föregående diskussion
En liten grupp har tittat på det här problemet och övervägt några möjliga lösningar:
- Kräv att användare tilldelar
this = default
när halvautomatiska egenskaper har implementerats med manuella settermetoder. Vi är överens om att detta är fel lösning eftersom den blåser bort värden som anges i fältinitierare. - Initiera implicit alla bakgrundsfält för automatiska/semi-automatiska egenskaper.
- Detta löser problemet "semi-auto property setters" och placerar explicit deklarerade fält under olika regler: "initiera inte implicit mina fält, men initiera implicit mina automatiska egenskaper."
- Ange ett sätt att tilldela bakgrundsfältet för en semi-automatisk egenskap och kräva att användarna tilldelar det.
- Detta kan vara besvärligt jämfört med (2). En automatisk egenskap är tänkt att vara "automatisk" och kanske innehåller "automatisk" initiering av fältet. Det kan leda till förvirring när det underliggande fältet tilldelas genom en tilldelning till egenskapen och när sätt-metoden för egenskapen anropas.
Vi har också fått feedback från användare som till exempel vill inkludera några fältinitierare i structs utan att uttryckligen behöva tilldela allt. Vi kan lösa det här problemet samt problemet "semi-automatisk egenskap med manuellt implementerad setter" samtidigt.
struct MagnitudeVector3d
{
double X, Y, Z;
double Magnitude = 1;
public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
{
}
}
Justera bestämd tilldelning
I stället för att utföra en definitiv tilldelningsanalys för att ge felmeddelanden för fält som inte har tilldelats på this
, gör vi det för att fastställa vilka fält som behöver initieras implicit. En sådan initiering infogas i början av konstruktorn .
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;
}
}
I exempel (4) och (5) har det resulterande kodgenet ibland "dubbla tilldelningar" av fält. Detta är i allmänhet bra, men för användare som är bekymrade över sådana dubbla tilldelningar kan vi avge vad som tidigare var definitiva tilldelningsfelsdiagnostik som inaktiverad som standardinställning varningsdiagnostik.
struct S
{
int x;
public S() // warning: 'S.x' is implicitly initialized to 'default'.
{
}
}
Användare som anger allvarlighetsgraden för den här diagnostiken till "fel" kommer att välja beteendet pre-C# 11. Sådana användare är i huvudsak "utestängda" från semi-automatiska egenskaper med manuellt implementerade set-metoder.
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;
}
}
Vid första anblicken känns detta som ett "hål" i funktionen, men det är faktiskt rätt sak att göra. Genom att aktivera diagnostiken säger användaren att de inte vill att kompilatorn implicit ska initiera fälten i konstruktorn. Det går inte att undvika den implicita initieringen här, så lösningen för dem är att använda ett annat sätt att initiera fältet än en manuellt implementerad setter, till exempel att manuellt deklarera fältet och tilldela det, eller genom att inkludera en fältinitierare.
För närvarande eliminerar JIT inte döda lagringar genom referenser, vilket innebär att dessa implicita initieringar har en verklig kostnad. Men det kan vara fixerbart. https://github.com/dotnet/runtime/issues/13727
Det är värt att notera att initiering av enskilda fält i stället för hela instansen egentligen bara är en optimering. Kompilatorn bör förmodligen vara fri att implementera vilken heuristik som helst den vill, så länge den uppfyller det invarianta villkoret att fält som inte definitivt tilldelas vid alla returpunkter eller innan någon icke-fältmedlemsåtkomst till this
initieras implicit.
Om en struct till exempel har 100 fält, och bara ett av dem uttryckligen initieras, kan det vara mer meningsfullt att göra en initobj
på hela strukturen än att implicit emittera initobj
för de 99 andra fälten. En implementering som implicit genererar initobj
för de 99 andra fälten skulle dock fortfarande vara giltig.
Ändringar i språkspecifikationen
Vi justerar följande avsnitt av standarden:
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12814-this-access
Om konstruktordeklarationen inte har någon konstruktorinitierare fungerar variabeln
this
exakt på samma sätt som enout
parameter av struct-typen. Detta innebär särskilt att variabeln definitivt ska tilldelas i varje körningsväg för instanskonstruktorn.
Vi justerar det här språket så att det läser:
Om konstruktordeklarationen inte har någon konstruktorinitierare fungerar variabeln this
på samma sätt som en out
parameter av structtyp, förutom att det inte är ett fel när de slutgiltiga tilldelningskraven (§9.4.1) inte uppfylls. I stället introducerar vi följande beteenden:
- När själva
this
variabeln inte uppfyller kraven initieras alla otilldelade instansvariabler inomthis
på alla punkter där kraven överträds implicit till standardvärdet (§9.3) i en initiering fas innan någon annan kod i konstruktorn körs. - När en instansvariabel v inom
this
inte uppfyller kraven, eller om någon instansvariabel på någon kapslingsnivå inom v inte uppfyller kraven, initieras v implicit till standardvärdet i en initiering fas innan någon annan kod i konstruktorn körs.
Designa möten
C# feature specifications