Primära konstruktorer
Anteckning
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 fångas upp i den relevanta LDM-anteckningen (Language Design Meeting).
Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.
Sammanfattning
Klasser och structs kan ha en parameterlista och deras basklassspecifikation kan ha en argumentlista. Primära konstruktorparametrar finns i omfånget i hela klassen eller structdeklarationen, och om de fångas upp av en funktionsmedlem eller anonym funktion lagras de på lämpligt sätt (t.ex. som obeskringliga privata fält i den deklarerade klassen eller structen).
Förslaget omskriver de primära konstruktorerna som redan är tillgängliga på poster i form av denna mer allmänna funktion med några ytterligare syntetiserade medlemmar.
Motivation
Möjligheten för en klass eller struct i C# att ha mer än en konstruktor ger generalitet, men på bekostnad av något tedium i deklarationssyntaxen, eftersom konstruktorns indata och klasstillståndet måste separeras rent.
Primära konstruktorer tillhandahåller parametrarna för en konstruktor i hela klassens eller strukturens omfång för att användas vid initiering eller direkt som objektets tillstånd. Följden av detta är att alla andra konstruktorer måste anropa genom den primära konstruktorn.
public class B(bool b) { } // base class
public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(S));
}
public C(string s) : this(true, 0, s) { } // must call this(...)
}
Detaljerad design
Detta beskriver den generaliserade designen för både poster och andra typer av data, och förklarar sedan hur de befintliga primära konstruktorerna för poster specificeras genom att lägga till en uppsättning syntetiserade medlemmar när det finns en primär konstruktor.
Syntax
Klass- och structdeklarationer utökas för att tillåta en parameterlista för typnamnet, en argumentlista i basklassen och en brödtext som bara består av en ;
:
class_declaration
: attributes? class_modifier* 'partial'? class_designator identifier type_parameter_list?
parameter_list? class_base? type_parameter_constraints_clause* class_body
;
class_designator
: 'record' 'class'?
| 'class'
class_base
: ':' class_type argument_list?
| ':' interface_type_list
| ':' class_type argument_list? ',' interface_type_list
;
class_body
: '{' class_member_declaration* '}' ';'?
| ';'
;
struct_declaration
: attributes? struct_modifier* 'partial'? 'record'? 'struct' identifier type_parameter_list?
parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body
;
struct_body
: '{' struct_member_declaration* '}' ';'?
| ';'
;
interface_declaration
: attributes? interface_modifier* 'partial'? 'interface'
identifier variant_type_parameter_list? interface_base?
type_parameter_constraints_clause* interface_body
;
interface_body
: '{' interface_member_declaration* '}' ';'?
| ';'
;
enum_declaration
: attributes? enum_modifier* 'enum' identifier enum_base? enum_body
;
enum_body
: '{' enum_member_declarations? '}' ';'?
| '{' enum_member_declarations ',' '}' ';'?
| ';'
;
Obs! Dessa produktioner ersätter record_declaration
i Records och record_struct_declaration
i Record structs, som båda blir föråldrade.
Det är ett fel om en class_base
har en argument_list
när den omgivande class_declaration
inte innehåller en parameter_list
. Högst en partiell typdeklaration av en partiell klass eller struct kan ge en parameter_list
. Parametrarna i parameter_list
för en record
-deklaration måste alla vara värdeparametrar.
Observera att enligt det här förslaget får class_body
, struct_body
, interface_body
och enum_body
bestå av bara en ;
.
En klass eller struct med en parameter_list
har en implicit offentlig konstruktor vars signatur motsvarar värdeparametrarna i typdeklarationen. Detta kallas primära konstruktorn för typen och gör att den implicit deklarerade parameterlösa konstruktorn, om den finns, ignoreras. Det är ett fel att ha en primär konstruktor och en konstruktor med samma signatur som redan finns i typdeklarationen.
Sökning
Den sökningen av enkla namn utökas för att hantera primära konstruktorparametrar. Ändringarna är markerade i fetstil i följande utdrag:
- Annars, för varje instanstyp
T
(§15.3.2), som börjar med instanstypen för den direkt omslutande typdeklarationen och fortsätter med instanstypen för varje omslutande klass- eller structdeklaration (om någon):
- Om deklarationen av
T
innehåller en primär konstruktorparameterI
och referensen inträffar inomargument_list
förT
class_base
eller inom en initierare av ett fält, en egenskap eller händelse avT
, är resultatet den primära konstruktorparameternI
- Annars om
e
är noll och deklarationen avT
innehåller en typparameter med namnetI
, refererar simple_name till den typparametern.- Annars, om en medlemssökning (§12.5) av
I
iT
med argument ave
typ genererar en matchning:
- Om
T
är instanstypen för den omedelbart omslutande klassen eller structtypen och sökningen identifierar en eller flera metoder, är resultatet en metodgrupp med ett associerat instansuttryck avthis
. Om en typargumentlista angavs används den för att anropa en generisk metod (§12.8.10.2).- Annars, om
T
är instanstypen för den omedelbart omslutande klassen eller structtypen, om sökningen identifierar en instansmedlem och om referensen inträffar inom blocket för en instanskonstruktor, en instansmetod eller en instansaccessor (§12.2.1), blir resultatet detsamma som en medlemsåtkomst (§12.8.7) av typenthis.I
. Detta kan bara inträffa näre
är noll.- Annars är resultatet detsamma som vid medlemsåtkomst (§12.8.7) av formuläret
T.I
ellerT.I<A₁, ..., Aₑ>
.- Om deklarationen av
T
innehåller en primär konstruktorparameterI
är resultatet annars den primära konstruktorparameternI
.
Det första tillägget motsvarar den ändring som uppstår för primära konstruktorer på posteroch ser till att primära konstruktorparametrar hittas före motsvarande fält inom initialiserare och basklassargument. Den här regeln utökas även till statiska initierare. Men eftersom poster alltid har en instansmedlem med samma namn som parametern kan tillägget bara leda till en ändring i ett felmeddelande. Ogiltig åtkomst till en parameter jämfört med olaglig åtkomst till en instansmedlem.
Det andra tillägget gör att primära konstruktorparametrar kan hittas någon annanstans i typens kropp, men bara om de inte överskuggas av medlemmar.
Det är ett fel att referera till en primär konstruktorparameter om referensen inte sker inom något av följande:
- ett
nameof
argument - en initialiserare av ett instansfält, en egenskap eller händelse av deklareringstypen (typen som deklarerar den primära konstruktorn med parametern).
-
argument_list
avclass_base
för den deklarerande typen. - brödtexten för en instansmetod (observera att instanskonstruktorer undantas) av deklareringstypen.
- brödtexten för en instansåtkomstor av deklareringstypen.
Med andra ord är primära konstruktorparametrar tillgängliga i deklarationstypens kropp. De skuggar medlemmar av deklareringstypen inom en initiering av ett fält, en egenskap eller händelse av deklareringstypen eller inom argument_list
av class_base
av deklareringstypen. De skuggas av medlemmar av deklareringstypen överallt annars.
Alltså, i följande deklaration:
class C(int i)
{
protected int i = i; // references parameter
public int I => i; // references field
}
Initiatorn för fältet i
refererar till parametern i
, medan egenskapens brödtext I
refererar till fältet i
.
Varna för skuggning av en medlem från basen
Kompilatorn skapar en varning om användningen av en identifierare när en basmedlem skuggar en primär konstruktorparameter om den primära konstruktorparametern inte skickades till bastypen via konstruktorn.
En primär konstruktorparameter anses skickas till bastypen via konstruktorn när alla följande villkor gäller för ett argument i class_base:
- Argumentet representerar en implicit eller explicit identitetskonvertering av en primär konstruktorparameter.
- Argumentet är inte en del av ett expanderat
params
argument.
Semantik
En primär konstruktor leder till genereringen av en instanskonstruktor på den omslutande typen med de angivna parametrarna. Om class_base
har en argumentlista har den genererade instanskonstruktorn en base
initierare med samma argumentlista.
Primära konstruktorparametrar i klass/struct-deklarationer kan deklareras ref
, in
eller out
. Det är fortfarande olagligt att deklarera ref
- eller out
parametrar i primära konstruktorer för postdeklaration.
Alla instansmedlemsinitieringar i klasstexten blir tilldelningar i den genererade konstruktorn.
Om en primär konstruktorparameter refereras inifrån en instansmedlem och referensen inte finns i ett nameof
argument, registreras den i tillståndet för den omslutande typen, så att den förblir tillgänglig efter konstruktorns avslutning. En trolig implementeringsstrategi är genom ett privat fält med hjälp av ett förvrängt namn. I en skrivskyddad struct kommer avbildningsfälten att vara skrivskyddade. Därför har åtkomst till fångade parametrar för en skrivskyddad struktur liknande begränsningar som åtkomst till skrivskyddade fält. Åtkomst till insamlade parametrar inom en readonly-medlem har liknande begränsningar som åtkomst till instansfält i samma kontext.
Insamling tillåts inte för parametrar som har referensliknande typ, och insamling tillåts inte för parametrarna ref
, in
eller out
. Detta liknar en begränsning för insamling i lambdas.
Om en primär konstruktorparameter endast refereras inifrån instansens medlemsinitierare kan de direkt referera till parametern för den genererade konstruktorn, eftersom de körs som en del av den.
Den primära konstruktorn utför följande åtgärdssekvens:
- Parametervärden lagras i eventuella avbildningsfält.
- Instansinitierare körs
- Baskonstruktorinitieraren anropas
Parameterreferenser i valfri användarkod ersätts med motsvarande avbildningsfältreferenser.
Till exempel den här deklarationen:
public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s) : this(true, 0, s) { } // must call this(...)
}
Genererar kod som liknar följande:
public class C : B
{
public int I { get; set; }
public string S
{
get => __s;
set => __s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s) : this(0, s) { ... } // must call this(...)
// generated members
private string __s; // for capture of s
public C(bool b, int i, string s)
{
__s = s; // capture s
I = i; // run I's initializer
B(b) // run B's constructor
}
}
Det är ett fel att en icke-primär konstruktordeklaration har samma parameterlista som den primära konstruktorn. Alla icke-primära konstruktordeklarationer måste använda en this
initierare, så att den primära konstruktorn slutligen anropas.
Poster ger en varning om en primär konstruktorparameter inte läss inom instansinitierarna (eventuellt genererade) eller basinitieraren. Liknande varningar rapporteras för primära konstruktorparametrar i klasser och strukturer:
- för en bivärdesparameter, om parametern inte samlas in och inte läss inom instansinitierare eller basinitierare.
- för en
in
parameter, om parametern inte läss inom instansinitierare eller basinitierare. - för en
ref
-parameter, om parametern inte har lästs eller skrivits till inom någon instansinitierare eller basinitierare.
Identiska enkla namn och typnamn
Det finns en särskild språkregel för scenarier som ofta kallas "Färgfärg"-scenarier – identiska enkla namn och typnamn.
I ett medlemsåtkomstformulär
E.I
, omE
är en enda identifierare, och om innebörden avE
som en simple_name (§12.8.4) är en konstant, fält, egenskap, lokal variabel eller parameter med samma typ som innebörden avE
som en type_name (§7.8.1), ), därefter tillåts båda möjliga betydelser avE
. Medlemssökningen avE.I
är aldrig tvetydig, eftersomI
nödvändigtvis ska vara medlem av typenE
i båda fallen. Med andra ord tillåter regeln bara åtkomst till statiska medlemmar och kapslade typer avE
där ett kompileringsfel annars skulle ha inträffat.
När det gäller primära konstruktorer påverkar regeln om en identifierare inom en instansmedlem ska behandlas som en typreferens eller som en primär konstruktorparameterreferens, som i sin tur samlar in parametern i tillståndet för den omslutande typen. Även om "medlemssökningen för E.I
är aldrig tvetydig", när sökningen ger en medlemsgrupp, är det i vissa fall omöjligt att avgöra om en medlemsåtkomst refererar till en statisk medlem eller en instansmedlem utan att fullt ut binda medlemsåtkomsten. Samtidigt ändrar insamling av en primär konstruktorparameter egenskaper för omslutande typ på ett sätt som påverkar semantisk analys. Till exempel kan typen bli ohanterad och misslyckas med vissa begränsningar på grund av detta.
Det finns även scenarier där bindningen kan lyckas oavsett om parametern anses vara infångade eller inte. Till exempel:
struct S1(Color Color)
{
public void Test()
{
Color.M1(this); // Error: ambiguity between parameter and typename
}
}
class Color
{
public void M1<T>(T x, int y = 0)
{
System.Console.WriteLine("instance");
}
public static void M1<T>(T x) where T : unmanaged
{
System.Console.WriteLine("static");
}
}
Om vi behandlar mottagare Color
som ett värde samlar vi in parametern och "S1" hanteras. Sedan blir den statiska metoden oanvändbar på grund av begränsningen och vi anropar instansmetoden. Men om vi behandlar mottagaren som en typ samlar vi inte in parametern och "S1" förblir ohanterad. Båda metoderna är tillämpliga, men den statiska metoden är "bättre" eftersom den inte har någon valfri parameter. Inget av alternativen leder till ett fel, men var och en skulle resultera i ett distinkt beteende.
Med tanke på detta skapar kompilatorn ett tvetydighetsfel för en medlemsåtkomst E.I
när alla följande villkor är uppfyllda:
- Medlemssökning av
E.I
ger en medlemsgrupp som innehåller instanser och statiska medlemmar samtidigt. Tilläggsmetoder som gäller för mottagartypen behandlas som instansmetoder för den här kontrollen. - Om
E
behandlas som ett enkelt namn, i stället för ett typnamn, refererar det till en primär konstruktorparameter och avbildar parametern i tillståndet för den omslutande typen.
Dubbla lagringsvarningar
Om en primär konstruktorparameter skickas till basen och även avbildas, finns det en hög risk att den oavsiktligt lagras två gånger i objektet.
Kompilatorn skapar en varning för in
eller för värdeargument i en class_base
argument_list
när alla följande villkor är sanna:
- Argumentet representerar en implicit eller explicit identitetskonvertering av en primär konstruktorparameter.
- Argumentet är inte en del av ett expanderat
params
argument. - Den primära konstruktorparametern infogas i tillståndet för den omslutande typen.
Kompilatorn skapar en varning för en variable_initializer
när alla följande villkor är uppfyllda:
- Variabelinitieraren representerar en implicit eller explicit identitetskonvertering av en primär konstruktorparameter.
- Den primära konstruktorparametern lagras i tillståndet för den omslutande typen.
Till exempel:
public class Person(string name)
{
public string Name { get; set; } = name; // warning: initialization
public override string ToString() => name; // capture
}
Attribut som riktar sig till primära konstruktorer
Vid https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md beslutade vi att ta till oss https://github.com/dotnet/csharplang/issues/7047 förslag.
Attributet "method" kan användas på en class_declaration/struct_declaration med parameter_list och leder till att den motsvarande primära konstruktorn har det attributet.
Attribut med method
mål på class_declaration/struct_declaration utan parameter_list ignoreras med en varning.
[method: FooAttr] // Good
public partial record Rec(
[property: Foo] int X,
[field: NonSerialized] int Y
);
[method: BarAttr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public partial record Rec
{
public void Frobnicate()
{
...
}
}
[method: Attr] // Good
public record MyUnit1();
[method: Attr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public record MyUnit2;
Primära konstruktorer för rekord
Med det här förslaget behöver poster inte längre separat ange en primär konstruktormekanism. I stället skulle deklarationer för (klass och struct) med primära konstruktorer följa de allmänna reglerna, med dessa enkla tillägg:
- Om det redan finns en medlem med samma namn för varje primär konstruktorparameter måste den vara en instansegenskap eller ett fält. Annars syntetiseras en offentlig init-only auto-egenskap med samma namn med en egenskapsinitierare som tilldelar från parametern.
- En dekonstruktor syntetiseras med utdataparametrar för att matcha de primära konstruktorparametrarna.
- Om en explicit konstruktordeklaration är en "kopieringskonstruktor" – en konstruktor som tar en enda parameter av den omslutande typen – krävs det inte att anropa en
this
-initialiserare, och den kommer inte att utföra de medlemsinitialiseringar som finns i postens deklaration.
Nackdelar
- Allokeringsstorleken för konstruerade objekt är mindre uppenbar eftersom kompilatorn avgör om ett fält ska allokeras för en primär konstruktorparameter baserat på klassens fullständiga text. Den här risken liknar den implicita avbildningen av variabler av lambda-uttryck.
- En vanlig frestelse (eller oavsiktligt mönster) kan vara att samla in parametern "samma" på flera nivåer av arv eftersom den skickas upp konstruktorkedjan i stället för att uttryckligen tilldela den ett skyddat fält i basklassen, vilket leder till duplicerade allokeringar för samma data i objekt. Detta liknar i hög grad dagens risk att åsidosätta automatiska egenskaper med autoegenskaper.
- Som vi föreslår här finns det ingen plats för ytterligare logik som vanligtvis kan uttryckas i konstruktororgan. Tillägget "primära konstruktorkroppar" nedan adresserar det.
- Som det föreslås skiljer sig den utförande ordningens semantik subtilt från hur det normalt är i vanliga konstruktorer, vilket fördröjer initialiseringen av medlemsvariabler till att ske efter basanropen. Detta skulle förmodligen kunna åtgärdas, men på bekostnad av vissa av tilläggsförslagen (särskilt "primära konstruktororgan").
- Förslaget fungerar bara för scenarier där en enskild konstruktor kan utses till primär.
- Det finns inget sätt att uttrycka separat tillgänglighet för klassen och den primära konstruktorn. Ett exempel är när offentliga konstruktorer alla delegerar till en privat "build-it-all"-konstruktor. Vid behov kan syntax föreslås för detta senare.
Alternativ
Ingen avbildning
En mycket enklare version av funktionen skulle förhindra att primära konstruktorparametrar förekommer i medlemsorgan. Att referera till dem skulle vara ett fel. Fält måste uttryckligen deklareras om lagring önskas utöver initieringskoden.
public class C(string s)
{
public string S1 => s; // Nope!
public string S2 { get; } = s; // Still allowed
}
Detta skulle fortfarande kunna utvecklas till det fullständiga förslaget vid ett senare tillfälle och skulle undvika ett antal beslut och komplexiteter, på bekostnad av att ta bort mindre standardtext från början, samt förmodligen också framstå som något ologiskt.
Explicita genererade fält
En alternativ metod är att primära konstruktorparametrar alltid och synligt genererar ett fält med samma namn. I stället för att stänga över parametrarna på samma sätt som lokala och anonyma funktioner skulle det uttryckligen finnas en genererad medlemsdeklaration som liknar de offentliga egenskaper som genereras för primära konstrukorparametrar i poster. Precis som för poster, om det redan finns en lämplig medlem, så skulle ingen genereras.
Om det genererade fältet är privat kan det fortfarande utelämnas när det inte används som ett fält i medlemsorganens element. I klasser skulle dock ett privat fält ofta inte vara rätt val, på grund av den tillståndsduplicering det kan orsaka i härledda klasser. Ett alternativ här skulle vara att i stället generera ett skyddat fält i klasser, vilket uppmuntrar återanvändning av lagring över arvslager. Men då skulle vi inte kunna utelämna deklarationen, och det skulle medföra en allokeringskostnad för varje primär konstruktorparameter.
Detta skulle närma icke-register primärkonstruktörer mer till registerkonstruktörer, eftersom medlemmar alltid (åtminstone konceptuellt) genereras, även om olika typer av medlemmar har olika åtkomstnivåer. Men det skulle också leda till överraskande skillnader från hur parametrar och lokala variabler hanteras på andra ställen i C#. Om vi någonsin skulle tillåta lokala klasser, skulle de till exempel implicit fånga omslutande parametrar och lokala variabler. Att synligt generera skuggfält för dem verkar inte vara ett rimligt beteende.
Ett annat problem som ofta uppstår med den här metoden är att många utvecklare har olika namngivningskonventioner för parametrar och fält. Vilken ska användas för den primära konstruktorparametern? Båda alternativen skulle leda till inkonsekvens med resten av koden.
Slutligen är synligt genererade medlemsdeklarationer verkligen det centrala för records, men mycket mer överraskande och "otippat" för icke-record klasser och strukturer. Sammantaget är det anledningen till att huvudförslaget väljer implicit avbildning, med förnuftigt beteende (konsekvent med poster) för explicita medlemsdeklarationer när de önskas.
Ta bort instansmedlemmar från initieringsomfånget
Uppslagsreglerna ovan är avsedda att tillåta det aktuella beteendet för primära konstruktorparametrar i poster när en motsvarande medlem har deklarerats manuellt och för att förklara beteendet för den genererade medlemmen när en sådan inte har deklarerats. Detta kräver att uppslag skiljer sig mellan "initieringsomfång" (detta/basinitierare, medlemsinitierare) och "brödtextomfång" (medlemsorgan), vilket ovanstående förslag uppnår genom att ändra när primära konstruktorparametrar söks efter, beroende på var referensen inträffar.
En observation är att hänvisning till en instansmedlem med ett enkelt namn i initieringsomfånget alltid leder till ett fel. I stället för att bara skugga instansmedlemmar på dessa platser, kan vi helt enkelt ta dem ur omfånget? Så skulle det inte finnas den här konstiga villkorliga ordningen av områden.
Detta alternativ är förmodligen möjligt, men det skulle få vissa konsekvenser som är något långtgående och potentiellt oönskade. Först och främst, om vi tar bort instansmedlemmar från initieringsomfånget kan ett enkelt namn som motsvarar en instansmedlem och inte till en primär konstruktorparameter oavsiktligt binda till något utanför typdeklarationen! Detta verkar som om det sällan skulle vara avsiktligt, och ett fel skulle vara bättre.
Dessutom är statiska medlemmar lämpliga att referera till i initialiseringsomfånget. Så vi skulle behöva skilja mellan statiska medlemmar och instansmedlemmar i uppslag, något vi inte gör idag. (Vi gör en åtskillnad vid överbelastningslösning, men det är inte aktuellt här). Så det måste också ändras, vilket leder till ännu fler situationer där t.ex. i statiska sammanhang skulle något binda "längre ut" snarare än fel eftersom den hittade en instansmedlem.
Allt som allt skulle denna "förenkling" leda till stora komplikationer nedströms som ingen bad om.
Möjliga tillägg
Detta är variationer eller tillägg till kärnförslaget som kan övervägas tillsammans med det, eller i ett senare skede om det anses användbart.
Primär konstruktorparameteråtkomst inom konstruktorer
Reglerna ovan gör det till ett fel att referera till en primär konstruktorparameter i en annan konstruktor. Detta kan dock tillåtas i kropp i andra konstruktorer, eftersom den primära konstruktorn körs först. Det skulle dock behöva förbli otillåtet i argumentlistan för this
-initieraren.
public class C(bool b, int i, string s) : B(b)
{
public C(string s) : this(b, s) // b still disallowed
{
i++; // could be allowed
}
}
Sådan åtkomst skulle fortfarande medföra infångning, eftersom det skulle vara det enda sättet för konstruktorkroppen att komma åt variabeln efter att den primära konstruktorn redan har körts.
Förbudet mot primära konstruktorparametrar i argumenten för den här initieraren skulle kunna försvagas för att tillåta dem, men göra dem inte definitivt tilldelade, men det verkar inte användbart.
Tillåt konstruktorer utan this
initierare
Konstruktorer utan this
initierare (dvs. med en implicit eller explicit base
initiator) kan tillåtas. En sådan konstruktor skulle inte köra instansfält, egenskaps- och händelseinitierare, eftersom de endast skulle anses vara en del av den primära konstruktorn.
I närvaro av sådana basanropande konstruktorer finns det ett par alternativ för hur primär konstruktorparameterfångst hanteras. Det enklaste är att helt neka fångst i den här situationen. Primära konstruktorparametrar är endast för initiering när sådana konstruktorer finns.
Om det kombineras med det tidigare beskrivna alternativet för att tillåta åtkomst till primära konstruktorparametrar inom konstruktorer, kan parametrarna inträda i konstruktorns kropp utan att vara definitivt tilldelade, och de som fångas upp måste definitivt tilldelas innan konstruktorns kropp avslutas. De skulle i princip vara implicita utparametrar. På så sätt skulle insamlade primära konstruktorparametrar alltid ha ett förnuftigt (dvs. uttryckligen tilldelat) värde när de förbrukas av andra funktionsmedlemmar.
En fördel med det här tillägget (i båda formerna) är att det helt generaliserar det nuvarande undantaget för "kopieringskonstruktorer" i dataposter, utan att leda till situationer där oinitialiserade primära konstruktorparametrar observeras. Konstruktorer som initierar objektet på alternativa sätt är i princip bra. De insamlingsrelaterade begränsningarna skulle inte vara en ändring för befintliga manuellt definierade kopieringskonstruktorer i records, eftersom records aldrig fångar sina primära konstruktionsparametrar. I stället genererar de fält.
public class C(bool b, int i, string s) : B(b)
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s2) : base(true) // cannot use `string s` because it would shadow
{
s = s2; // must initialize s because it is captured by S
}
protected C(C original) : base(original) // copy constructor
{
this.s = original.s; // assignment to b and i not required because not captured
}
}
Primära konstruktorkroppar
Konstruktorerna själva innehåller ofta parameterverifieringslogik eller annan icke-inledande initieringskod som inte kan uttryckas som initialiserare.
Primära konstruktorer kan utökas för att tillåta att instruktionsblock visas direkt i klasstexten. Dessa instruktioner infogas i den genererade konstruktorn vid den punkt där de visas mellan initieringsuppgifter och körs därmed varvat med initialiseringarna. Till exempel:
public class C(int i, string s) : B(s)
{
{
if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
}
int[] a = new int[i];
public int S => s;
}
En stor del av detta scenario kan täckas tillräckligt om vi skulle införa "slutliga initialiserare" som körs efter att konstruktorerna och samt eventuella objekt-/samlingsinitialiseringar är slutförda. Argumentverifiering är dock en sak som helst skulle ske så tidigt som möjligt.
Primära konstruktörsblock kan också ge en möjlighet att använda en åtkomstmodifierare för den primära konstruktorn, vilket tillåter den att skilja sig från åtkomstnivån för den omslutande typen.
Kombinerade parameter- och medlemsdeklarationer
Ett möjligt och ofta nämnt tillägg kan vara att tillåta att primära konstruktorparametrar kommenteras så att de också deklarera en medlem på typen. Oftast föreslås att en åtkomstspecificerare på parametrarna ska utlösa medlemsgenereringen:
public class C(bool b, protected int i, string s) : B(b) // i is a field as well as a parameter
{
void M()
{
... i ... // refers to the field i
... s ... // closes over the parameter s
}
}
Det finns några problem:
- Vad händer om en egenskap är önskad, inte ett fält? Att ha
{ get; set; }
syntax infogad i en parameterlista förefaller inte lockande. - Vad händer om olika namngivningskonventioner används för parametrar och fält? Då skulle den här funktionen vara värdelös.
Detta är ett potentiellt framtida tillägg som kan antas eller inte. Det nuvarande förslaget lämnar möjligheten öppen.
Öppna frågor
Uppslagsordning för typparametrar
Avsnittet https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup anger att typparametrar av deklareringstyp ska komma före typens primära konstruktorparametrar i varje kontext där dessa parametrar finns i omfånget. Vi har dock redan befintligt beteende med poster – primära konstruktorparametrar kommer före typparametrar i basinitierare och fältinitierare.
Vad ska vi göra åt den här avvikelsen?
- Justera reglerna så att de matchar beteendet.
- Justera beteendet (en eventuell icke-bakåtkompatibel ändring).
- Tillåt inte att en parameter i primärkonstruktorn använder en typparameters namn (en potentiellt bakåtkompatibilitetsbrytande ändring).
- Gör ingenting, acceptera inkonsekvensen mellan specifikationen och implementeringen.
Slutsats:
Justera reglerna så att de matchar beteendet (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors).
Fältmålattribut för insamlade primära konstruktorparametrar
Ska vi tillåta fältmålattribut för insamlade primära konstruktorparametrar?
class C1([field: Test] int x) // Parameter is captured, the attribute goes to the capture field
{
public int X => x;
}
class C2([field: Test] int x) // Parameter is not captured, the attribute is ignored with a warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
public int X = x;
}
Just nu ignoreras attributen med varningen oavsett om parametern registreras.
Observera att för poster tillåts attribut inriktade på fält när en egenskap syntetiseras för detta. Attributen går sedan till bakgrundsfältet.
record R1([field: Test]int X); // Ok, the attribute goes on the backing field
record R2([field: Test]int X) // warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
public int X = X;
}
Slutsats:
Tillåts inte (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#attributes-on-captured-parameters).
Varna för skuggning av en medlem från basen
Ska vi rapportera en varning när en medlem från basen skuggar en primär konstruktorparameter i en medlem (se https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621)?
Slutsats:
En alternativ design godkänns – https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors
Att fånga en instans av den omgivande typen i ett slutblock
När en parameter som fångas in i tillståndet för den omslutande typen också refereras till i en lambda-funktion inuti en instansinitierare eller en basinitierare, måste lambda-funktionen och tillståndet för den omslutande typen hänvisa till samma ställe för parametern. Till exempel:
partial class C1
{
public System.Func<int> F1 = Execute1(() => p1++);
}
partial class C1 (int p1)
{
public int M1() { return p1++; }
static System.Func<int> Execute1(System.Func<int> f)
{
_ = f();
return f;
}
}
Eftersom den naiva implementeringen av att samla in en parameter i typens tillstånd helt enkelt samlar in parametern i ett privat instansfält måste lambda referera till samma fält. Därför måste den kunna komma åt instansen av typen. Detta kräver att du samlar in this
i en stängning innan baskonstruktorn anropas. Det resulterar i sin tur i en säker men icke-verifierbar IL. Är detta acceptabelt?
Alternativt kan vi:
- Tillåt inte lambdas på det sättet;
- Alternativt kan du fånga parametrar som den i en instans av en separat klass (ännu en slutning) och dela den instansen mellan slutningen och instansen av den omgivande typen. Behovet av att inkludera
this
i en closure elimineras därmed.
Slutsats:
Vi känner oss bekväma med att samla in this
i en closure innan baskonstruktorn anropas (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).
Körmiljöteamet ansåg inte heller att IL-mönstret var problematiskt.
Tilldela till this
inom en struct
C# gör det möjligt att tilldela ett värde till this
inom en struktur. Om structen avbildar en primär konstruktorparameter kommer tilldelningen att skriva över dess värde, vilket kanske inte är uppenbart för användaren. Vill vi rapportera en varning för tilldelningar som detta?
struct S(int x)
{
int X => x;
void M(S s)
{
this = s; // 'x' is overwritten
}
}
Slutsats:
Tillåten, ingen varning (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).
Varning för dubbel lagring för initiering plus avbildning
Vi varnar om en primär konstruktorparameter används i basklassen och samt fångas, eftersom det finns en hög risk att den oavsiktligt lagras två gånger i objektet.
Det verkar som om det finns en liknande risk om en parameter används för att initiera en medlem och även fångas in. Här är ett litet exempel:
public class Person(string name)
{
public string Name { get; set; } = name; // initialization
public override string ToString() => name; // capture
}
För en viss instans av Person
återspeglas inte ändringar i Name
i utdata från ToString
, vilket förmodligen är oavsiktligt från utvecklarens sida.
Bör vi införa en varning om dubbel lagring för den här situationen?
Så här skulle det fungera:
Kompilatorn skapar en varning för en variable_initializer
när alla följande villkor är uppfyllda:
- Variabelinitieraren representerar en implicit eller explicit identitetskonvertering av en primär konstruktorparameter.
- Den primära konstruktorparametern införlivas i tillståndet för den omslutande typen.
Slutsats:
Godkänd, se https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors
LDM-möten
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-10-17.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-01-18.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-22.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors
C# feature specifications