Delen via


Primaire constructors

Notitie

Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.

Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Deze verschillen worden vastgelegd in de relevante LDM-notities (Language Design Meeting).

Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.

Kampioensprobleem: https://github.com/dotnet/csharplang/issues/2691

Samenvatting

Klassen en structs kunnen een parameterlijst hebben en hun basisklassespecificatie kan een argumentenlijst hebben. Primaire constructorparameters bevinden zich binnen het bereik van de klasse- of structdeclaratie en als ze worden vastgelegd door een functielid of anonieme functie, worden ze op de juiste wijze opgeslagen (bijvoorbeeld als onuitkenbare privévelden van de gedeclareerde klasse of struct).

Het voorstel herdefinieert met terugwerkende kracht de primaire constructors die al beschikbaar zijn voor records in termen van deze meer algemene functie, waarbij enkele extra leden worden gesynthetiseerd.

Motivatie

De mogelijkheid van een klasse of struct in C# om meer dan één constructor te hebben, biedt algemeenheid, maar ten koste van wat tedium in de syntaxis van de declaratie, omdat de constructorinvoer en de klassestatus schoon moeten worden gescheiden.

Primaire constructors stellen de parameters van een constructor beschikbaar binnen de klasse of struct voor gebruik bij initialisatie of direct als objecttoestand. De afweging is dat alle andere constructors de primaire constructor moeten aanroepen.

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

Gedetailleerd ontwerp

Hier wordt het algemene ontwerp voor zowel records als niet-records uitgelegd, en vervolgens wordt gedetailleerd hoe de bestaande primaire constructors voor records worden gespecificeerd door in aanwezigheid van een primaire constructor een set gesynthetiseerde leden toe te voegen.

Syntaxis

Klasse- en structdeclaraties worden uitgebreid om een parameterlijst toe te staan voor de typenaam, een lijst met argumenten op de basisklasse en een hoofdtekst die bestaat uit slechts een ;:

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 ',' '}' ';'?
  | ';'
  ;

Opmerking: Deze producties vervangen record_declaration in Records en record_struct_declaration in Record-structs, die beide verouderd zijn.

Het is een fout als een class_base een argument_list heeft terwijl de omsluitende class_declaration geen parameter_listbevat. Ten hoogste één gedeeltelijke typedeclaratie van een gedeeltelijke klasse of struct kan een parameter_listbieden. De parameters in de parameter_list van een record-declaratie moeten allemaal waardeparameters zijn.

Volgens dit voorstel class_body, struct_body, mogen interface_body en enum_body bestaan uit slechts een ;.

Een klasse of struct met een parameter_list heeft een impliciete openbare constructor waarvan de handtekening overeenkomt met de waardeparameters van de typedeclaratie. Dit wordt de primaire constructor voor het type genoemd en zorgt ervoor dat de impliciet gedeclareerde parameterloze constructor, indien aanwezig, wordt onderdrukt. Het is een fout om een primaire constructor en een constructor met dezelfde handtekening te hebben die al aanwezig is in de typedeclaratie.

Opzoeken

Het opzoeken van eenvoudige namen is uitgebreid om primaire constructorparameters te verwerken. De wijzigingen worden gemarkeerd in vetgedrukte in het volgende fragment:

  • Anders, voor elk exemplaartype T (§15.3.2), te beginnen met het exemplaartype van de direct insluitende typeverklaring, gevolgd door het exemplaartype van elke omsluitende class- of structverklaring (indien van toepassing):
    • Als de declaratie van T een primaire constructorparameter I bevat en de verwijzing plaatsvindt binnen de argument_list van Tclass_base of binnen een initialisatiefunctie van een veld, eigenschap of gebeurtenis van T, is het resultaat de primaire constructorparameter I
    • Anders als e nul is en de declaratie van T een typeparameter met de naam Ibevat, verwijst de simple_name naar die typeparameter.
    • Anders, als een lidzoekactie (§12,5) van I in T met e typeargumenten een overeenkomst oplevert:
      • Als T het exemplaartype is van het onmiddellijk ingesloten klasse- of structtype en de zoekactie een of meer methoden identificeert, is het resultaat een methodegroep met een bijbehorende exemplaarexpressie van this. Als een lijst met typeargumenten is opgegeven, wordt deze gebruikt bij het aanroepen van een algemene methode (§12.8.10.2).
      • Als T het exemplaartype is van de onmiddellijk insluitende klasse of structtype, als de opzoekactie een exemplaarlid identificeert en als de verwijzing optreedt in het blok van een exemplaarconstructor, een exemplaarmethode of een exemplaartoegangsor (§12.2.1), is het resultaat hetzelfde als een lidtoegang (§12.8.7) van de vorm this.I. Dit kan alleen gebeuren wanneer e nul is.
      • Anders is het resultaat hetzelfde als een lidmaattoegang (§12.8.7) in de vorm van T.I of T.I<A₁, ..., Aₑ>.
    • Als de declaratie van T een primaire constructorparameter Ibevat, is het resultaat de primaire constructorparameter I.

De eerste toevoeging komt overeen met de wijziging die is gemaakt door primaire constructors op records, en zorgt ervoor dat primaire constructorparameters worden gevonden voordat overeenkomstige velden in initializers en basisklasseargumenten worden gevonden. Deze regel wordt ook uitgebreid naar statische initialisaties. Omdat records echter altijd een exemplaarlid met dezelfde naam als de parameter hebben, kan de extensie alleen leiden tot een wijziging in een foutbericht. Ongeldige toegang tot een parameter versus ongeldige toegang tot een instantie-lid.

De tweede toevoeging stelt dat de primaire constructorparameters elders binnenin de typebody te vinden zijn, maar alleen als ze niet worden overschaduwd door leden.

Het is een fout om te verwijzen naar een primaire constructorparameter als de verwijzing niet op een van de volgende manieren voorkomt:

  • een nameof argument
  • een initialisatiefunctie van een exemplaarveld, eigenschap of gebeurtenis van het declaratietype (typ het declareren van de primaire constructor met de parameter).
  • de argument_list van class_base van het declaratietype.
  • de hoofdtekst van een instantiemethode (let op dat instance constructors zijn uitgesloten) van het declarerende type.
  • de hoofdtekst van een instantietoegangsor van het declaratietype.

Met andere woorden, primaire constructorparameters vallen binnen de reikwijdte van de declarerende typebody. Ze schaduwen leden van het declaratietype binnen een initializer van een field, property of event van het declaratietype, of binnen de argument_list van class_base van het declaratietype. Ze worden overschaduwd door leden van het declaratietype in alle andere gevallen.

Dus in de volgende verklaring:

class C(int i)
{
    protected int i = i; // references parameter
    public int I => i; // references field
}

De initialisatiefunctie voor het veld i verwijst naar de parameter i, terwijl de hoofdtekst van de eigenschap I verwijst naar het veld i.

Waarschuwen voor het overschaduwen van een lid vanuit de basis.

Compiler geeft een waarschuwing over het gebruik van een id wanneer een basislid een primaire constructorparameter schaduwt als die primaire constructorparameter niet is doorgegeven aan het basistype via de constructor.

Een primaire constructorparameter wordt beschouwd als doorgegeven aan het basistype via de constructor wanneer aan alle volgende voorwaarden wordt voldaan voor een argument in class_base:

  • Het argument vertegenwoordigt een impliciete of expliciete identiteitsconversie van een primaire constructorparameter;
  • Het argument maakt geen deel uit van een uitgevouwen params argument;

Semantiek

Een primaire constructor leidt tot het genereren van een instantieconstructor in het omhullende type met de opgegeven parameters. Als de class_base een argumentenlijst heeft, heeft de gegenereerde exemplaarconstructor een base initialisatiefunctie met dezelfde argumentenlijst.

Primaire constructorparameters in klassen-/structurendeclaraties kunnen gedeclareerd worden als ref, in of out. Het declareren van ref- of out-parameters blijft illegaal in de primaire constructors van recorddeclaraties.

Alle exemplaarlid-initialisatoren in de klassebody worden toewijzingen in de gegenereerde constructor.

Als er vanuit een exemplaarlid naar een primaire constructorparameter wordt verwezen en de verwijzing zich niet binnen een nameof argument bevindt, wordt deze vastgelegd in de status van het insluittype, zodat deze na beëindiging van de constructor toegankelijk blijft. Een waarschijnlijke implementatiestrategie is via een privéveld met behulp van een mangled-naam. In een alleen-lezen struct zullen de capture-velden alleen-lezen zijn. Daarom heeft de toegang tot vastgelegde parameters van een alleen-lezen struct vergelijkbare beperkingen als toegang tot alleen-lezen velden. Toegang tot vastgelegde parameters binnen een alleen-lezen lid heeft vergelijkbare beperkingen als toegang tot exemplaarvelden in dezelfde context.

Vastleggen is niet toegestaan voor parameters met ref-achtig type en vastleggen is niet toegestaan voor ref, in of out parameters. Dit is vergelijkbaar met een beperking voor het vastleggen in lambdas.

Als er alleen naar een primaire constructorparameter wordt verwezen vanuit exemplaarlid-initialisaties, kunnen deze initialisaties rechtstreeks verwijzen naar de parameter van de gegenereerde constructor, omdat ze worden uitgevoerd als onderdeel ervan.

Primaire constructor voert de volgende reeks bewerkingen uit:

  1. Parameterwaarden worden opgeslagen in capture-velden, indien van toepassing.
  2. Exemplaar-initialisatoren worden uitgevoerd
  3. Initialisatiefunctie voor basisconstructor wordt aangeroepen

Parameterverwijzingen in gebruikerscode worden vervangen door bijbehorende capture veldverwijzingen.

Bijvoorbeeld deze declaratie:

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

Hiermee wordt code gegenereerd die vergelijkbaar is met de volgende:

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
    }
}

Het is een fout voor een niet-primaire constructordeclaratie om dezelfde parameterlijst te hebben als de primaire constructor. Alle niet-primaire constructordeclaraties moeten een this initialisatiefunctie gebruiken, zodat de primaire constructor uiteindelijk wordt aangeroepen.

Records produceren een waarschuwing als een primaire constructorparameter niet wordt gelezen binnen de (mogelijk gegenereerde) initializers van het object of de basisinitializer. Vergelijkbare waarschuwingen worden gerapporteerd voor primaire constructorparameters in klassen en structuren:

  • voor een by-value-parameter, als de parameter niet wordt vastgelegd en niet wordt gelezen binnen initialisatieprogramma's of basis-initialisatieprogramma's van instanties.
  • voor een in parameter, als de parameter niet wordt gelezen binnen enige instantie-initialisatoren of basisinitialisatoren.
  • voor een ref-parameter, als de parameter niet wordt gelezen of naar geschreven binnen enige instantie-initialisators of de basis-initialisator.

Identieke eenvoudige namen en typenamen

Er is een speciale taalregel voor scenario's die vaak 'Kleurkleur' worden genoemd: Identieke eenvoudige namen en typenamen.

Als E in een lidtoegang van de vorm E.Ieen enkele identificator is en als de betekenis van E als een simple_name (§12.8.4) een constante, veld, eigenschap, lokale variabele of parameter is met hetzelfde type als dat van de betekenis van E als een type_name (§7.8.1), dan zijn beide mogelijke betekenissen van E toegestaan. Het opzoeken van E.I leden is nooit dubbelzinnig, omdat I in beide gevallen noodzakelijkerwijs lid is van het type E. Met andere woorden, de regel staat eenvoudigweg toegang toe tot de statische elementen en geneste typen van E, waar anders een compilatiefout zou zijn opgetreden.

Wat primaire constructors betreft, heeft de regel invloed op de vraag of een identificator binnen een instantiatie-lid moet worden behandeld als een typeverwijzing of als een verwijzing naar de primaire constructorparameter, waardoor de parameter wordt vastgelegd in de status van het omsluitende type. Hoewel 'het opzoeken van leden van E.I nooit dubbelzinnig is', is het bij het vinden van een ledengroep in sommige gevallen onmogelijk te bepalen of een lidtoegang naar een statisch lid of een instantie-lid verwijst zonder de lidtoegang volledig op te lossen (binden). Tegelijkertijd wijzigt het vastleggen van een primaire constructorparameter eigenschappen van het insluiten van een type op een manier die van invloed is op semantische analyse. Het type kan bijvoorbeeld niet beheerd worden en als gevolg daarvan niet aan bepaalde beperkingen voldoen. Er zijn zelfs scenario's waarvoor binding op beide manieren kan slagen, afhankelijk van of de parameter wordt beschouwd als vastgelegd of niet. Bijvoorbeeld:

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

Als we ontvanger Color als een waarde behandelen, leggen we de parameter vast en wordt 'S1' beheerd. Vervolgens wordt de statische methode niet toe te passen vanwege de beperking en wordt de instantiemethode aangeroepen. Als we de ontvanger echter behandelen als een type, leggen we de parameter niet vast en 'S1' blijft onbeheerd, dan zijn beide methoden van toepassing, maar de statische methode is 'beter' omdat deze geen optionele parameter heeft. Geen van beide opties leidt tot een fout, maar elk zou leiden tot verschillend gedrag.

Op basis hiervan produceert compiler een dubbelzinnigheidsfout voor een lidtoegang E.I wanneer aan alle volgende voorwaarden wordt voldaan:

  • Bij ledenopzoeking van E.I levert een ledengroep met zowel exemplaar- als statische leden op. Extensiemethoden die van toepassing zijn op het ontvangertype, worden behandeld als exemplaarmethoden voor deze controle.
  • Als E wordt behandeld als een eenvoudige naam, in plaats van een typenaam, verwijst deze naar een primaire constructorparameter en legt de parameter vast in de status van het omsluittype.

Dubbele opslagwaarschuwingen

Als een primaire constructorparameter wordt doorgegeven aan de basis en ook vastgelegd, is er een hoog risico dat deze per ongeluk tweemaal in het object wordt opgeslagen.

Compiler zal een waarschuwing produceren voor in of een waardeargument in een class_baseargument_list wanneer aan alle volgende voorwaarden wordt voldaan:

  • Het argument vertegenwoordigt een impliciete of expliciete identiteitsconversie van een primaire constructorparameter;
  • Het argument maakt geen deel uit van een uitgevouwen params argument;
  • De primaire constructorparameter wordt vastgelegd in de status van het insluittype.

Compiler produceert een waarschuwing voor een variable_initializer wanneer aan alle volgende voorwaarden wordt voldaan:

  • De variabele initialisatiefunctie vertegenwoordigt een impliciete of expliciete identiteitsconversie van een primaire constructorparameter;
  • De primaire constructorparameter wordt vastgelegd in de status van het insluittype.

Bijvoorbeeld:

public class Person(string name)
{
    public string Name { get; set; } = name;   // warning: initialization
    public override string ToString() => name; // capture
}

Attributen voor primaire constructors

Op https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md besloten we het https://github.com/dotnet/csharplang/issues/7047 voorstel te omarmen.

Het attribuutdoel 'method' is toegestaan op een class_declaration/struct_declaration met parameter_list en resulteert erin dat de bijbehorende primaire constructor dat attribuut krijgt. Kenmerken met het method doel op een class_declaration/struct_declaration zonder parameter_list worden genegeerd met een waarschuwing.

[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;

Primaire constructors van records

Met dit voorstel hoeven records niet langer afzonderlijk een primair constructormechanisme op te geven. In plaats daarvan zouden recorddeclaraties (klasse en struct) met primaire constructors de algemene regels volgen, met deze eenvoudige toevoegingen:

  • Als voor elke primaire constructorparameter al een lid met dezelfde naam bestaat, moet dit een exemplaareigenschap of -veld zijn. Als dat niet het probleem is, wordt een openbare automatische eigenschap van dezelfde naam gesynthetiseerd met een initialisatiefunctie voor eigenschappen die uit de parameter wordt toegewezen.
  • Een deconstructor wordt samengesteld met uitvoerparameters die overeenkomen met de parameters van de primaire constructor.
  • Als een expliciete constructordeclaratie een 'copy constructor' is, een constructor die één parameter van het omsluitende type gebruikt, is het niet vereist om een this-initialisatiefunctie aan te roepen en zullen de lidinitialisatoren die aanwezig zijn in de recorddeclaratie niet worden uitgevoerd.

Nadelen

  • De toewijzingsgrootte van samengestelde objecten is minder duidelijk, omdat de compiler bepaalt of een veld moet worden toegewezen voor een primaire constructorparameter op basis van de volledige tekst van de klasse. Dit risico is vergelijkbaar met de impliciete opname van variabelen door lambda-expressies.
  • Een veelvoorkomende verleiding (of onbedoeld patroon) is het vastleggen van de parameter 'dezelfde' op meerdere niveaus van overname, omdat deze wordt doorgegeven aan de constructorketen in plaats van het expliciet een beveiligd veld in de basisklasse toe te staan, wat leidt tot dubbele toewijzingen voor dezelfde gegevens in objecten. Dit is erg vergelijkbaar met het huidige risico van het overschrijven van automatische eigenschappen met automatische eigenschappen.
  • Zoals hier voorgesteld, is er geen plaats voor aanvullende logica die meestal kan worden uitgedrukt in constructorteksten. De extensie 'primaire constructorlichamen' hieronder lost dat op.
  • Zoals voorgesteld, verschillen de semantiek van de uitvoeringsvolgorde subtiel van die in gewone constructors, waardoor de initialisatie van leden pas na de basisaanroepen plaatsvindt. Dit zou waarschijnlijk kunnen worden opgelost, maar ten koste van enkele van de uitbreidingsvoorstellen (met name "primaire constructororganen").
  • Het voorstel werkt alleen voor scenario's waarbij één constructor primair kan worden aangewezen.
  • Er is geen manier om de afzonderlijke toegankelijkheid van de klasse en de primaire constructor te beschrijven. Een voorbeeld is wanneer publieke constructors allemaal doorverwijzen naar één privé alles-bouwende constructor. Indien nodig kan de syntaxis hiervoor later worden voorgesteld.

Alternatieven

Geen opname

Een veel eenvoudigere versie van de functie verbiedt dat primaire constructorparameters optreden in lidorganen. Het verwijzen naar hen zou een fout zijn. Velden moeten expliciet worden gedeclareerd als opslag buiten de initialisatiecode is vereist.

public class C(string s)
{
    public string S1 => s; // Nope!
    public string S2 { get; } = s; // Still allowed
}

Dit kan op een later tijdstip nog steeds worden ontwikkeld tot het volledige voorstel, en zou een aantal beslissingen en complexiteiten vermijden, tegen de prijs dat er aanvankelijk minder standaardtekst wordt verwijderd, en dat het waarschijnlijk ook onintuïtief lijkt.

Expliciet gegenereerde velden

Een alternatieve methode is dat primaire constructorparameters altijd en zichtbaar een veld met dezelfde naam genereren. In plaats van de parameters op dezelfde manier te sluiten als lokale en anonieme functies, zou er expliciet een gegenereerde liddeclaratie worden gegenereerd, vergelijkbaar met de openbare eigenschappen die zijn gegenereerd voor primaire construcor-parameters in records. Net als bij records, als er al een geschikt lid bestaat, wordt er geen lid gegenereerd.

Als het gegenereerde veld privé is, kan het nog steeds worden gewist wanneer het niet wordt gebruikt als een veld in lidorganen. In klassen zou een privéveld echter vaak niet de juiste keuze zijn, vanwege de statusduplicatie die het in afgeleide klassen kan veroorzaken. Een optie hier is om in plaats daarvan een beveiligd veld in klassen te genereren, waardoor het hergebruik van opslag tussen overnamelagen wordt aangemoedigd. We zouden de declaratie echter niet kunnen weglaten en zouden we toewijzingskosten maken voor elke primaire constructorparameter.

Hiermee worden niet-recordconstructors beter afgestemd op recordconstructors, omdat leden altijd (ten minste conceptueel) worden gegenereerd, hoewel er verschillende soorten leden met verschillende toegankelijkheden zijn. Maar het zou ook leiden tot verrassende verschillen van hoe parameters en lokale bevolking elders in C# worden vastgelegd. Als we bijvoorbeeld ooit lokale klassen zouden toestaan, zouden ze impliciet omringende parameters en lokale variabelen vastleggen. Het zichtbaar genereren van schaduwvelden voor hen lijkt geen redelijk gedrag te zijn.

Een ander probleem dat vaak optreedt bij deze benadering, is dat veel ontwikkelaars verschillende naamconventies hebben voor parameters en velden. Welke moet worden gebruikt voor de primaire constructorparameter? Beide keuzen leiden tot inconsistentie met de rest van de code.

Ten slotte is het zichtbaar genereren van ledenverklaringen echt het belangrijkste aspect voor records, maar veel verrassender en niet typisch voor niet-recordklassen en structs. Dat zijn allemaal de redenen waarom het belangrijkste voorstel kiest voor impliciete opname, met verstandig gedrag (consistent met records) voor expliciete liddeclaraties wanneer ze gewenst zijn.

Exemplaarleden verwijderen uit initialisatiebereik

De bovenstaande opzoekregels zijn bedoeld om het huidige gedrag van primaire constructorparameters in records toe te staan wanneer een overeenkomstig lid handmatig wordt gedeclareerd en om het gedrag van het gegenereerde lid uit te leggen wanneer er geen overeenkomstig lid is. Hiervoor is het nodig om onderscheid te maken tussen 'initialisatiebereik' (this/basis initialisatoren, lid-initialisatoren) en 'body scope' (ledenlichamen), wat in het bovenstaande voorstel wordt bereikt door te wijzigen wanneer primaire constructorparameters worden geïdentificeerd, afhankelijk van waar de verwijzing plaatsvindt.

Een waarneming is dat het verwijzen naar een exemplaarlid met een eenvoudige naam in het initialisatiebereik altijd tot een fout leidt. In plaats van instantieleden op die plaatsen alleen te overschrijven, kunnen we ze gewoon buiten beschouwing laten? Op die manier zou er geen rare voorwaardelijke volgorde van bereik zijn.

Dit alternatief is waarschijnlijk mogelijk, maar het zou enkele gevolgen hebben die enigszins vergaande en mogelijk ongewenst zijn. Als we eerst instantieleden uit het initialisatiebereik verwijderen, kan een eenvoudige naam die overeenkomt met een exemplaarlid en niet aan een primaire constructorparameter per ongeluk worden gekoppeld aan iets buiten de typedeclaratie! Dit lijkt erop dat het zelden opzettelijk zou zijn en dat een fout beter zou zijn.

Bovendien zijn statische leden prima te raadplegen in de initialisatiescope. We moeten dus onderscheid maken tussen statische leden en instantieleden in de zoekactie, iets wat we vandaag niet doen. (We maken een onderscheid in overbelastingresolutie, maar dat is hier niet van toepassing). Dat zou dus ook moeten worden aangepast, waardoor er nog meer situaties ontstaan waarin bijvoorbeeld in statische contexten iets 'verder weg' zou binden in plaats van een foutmelding te geven omdat er een lid van een instantie is gevonden.

Al deze "vereenvoudiging" zou leiden tot een behoorlijke downstreamcomplicatie waar niemand om vroeg.

Mogelijke uitbreidingen

Dit zijn variaties of toevoegingen aan het kernvoorstel dat in combinatie met het voorstel kan worden beschouwd, of in een later stadium, indien nuttig geacht.

Toegang tot primaire constructorparameter binnen constructors

De bovenstaande regels maken het een fout om te verwijzen naar een primaire constructorparameter binnen een andere constructor. Dit kan echter worden toegestaan binnen het lichaam van andere constructors, omdat de primaire constructor eerst wordt uitgevoerd. Het zou echter niet toegestaan moeten blijven binnen de argumentenlijst van de this initialisatiefunctie.

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
    }
}

Een dergelijke toegang zou nog steeds worden vastgelegd, omdat dat de enige manier is waarop de constructorbody bij de variabele kan komen nadat de primaire constructor al is uitgevoerd.

Het verbod op primaire constructorparameters in de argumenten van de this-initializer kan worden verzwakt om ze toe te staan, maar niet als definitief toegewezen te maken, maar dat lijkt niet nuttig.

Constructors zonder een this initialisatiefunctie toestaan

Constructors zonder een this-initialisator (d.w.z. met een impliciete of expliciete base-initialisator) zouden kunnen worden toegestaan. Een dergelijke constructor zou niet instantieveld-, eigenschaps- en gebeurtenis-initialisatoren uitvoeren, omdat deze alleen als onderdeel van de primaire constructor worden beschouwd.

In aanwezigheid van dergelijke basisoproepende constructors zijn er een aantal opties voor het vastleggen van primaire constructorparameters. Het eenvoudigste is om het vastleggen in deze situatie volledig te weigeren. Primaire constructorparameters zijn alleen bedoeld voor initialisatie wanneer dergelijke constructors bestaan.

U kunt ook, in combinatie met de eerder beschreven optie om toegang tot primaire constructorparameters binnen constructors toe te staan, ervoor zorgen dat de parameters de constructorbody binnenkomen als niet definitief toegewezen, en vastgelegde parameters moeten tegen het einde van de constructorbody definitief worden toegewezen. Ze zouden in wezen impliciete uit-parameters zijn. Op die manier zouden vastgelegde primaire constructorparameters altijd een verstandige waarde (dat wil gezegd expliciet toegewezen) hebben op het moment dat ze worden gebruikt door andere functieleden.

Een aantrekkingskracht van deze extensie (in beide vormen) is dat de huidige uitzondering voor 'copy constructors' volledig wordt gegeneraliseerd in records, zonder dat er situaties ontstaan waarin niet-geïnitialiseerde primaire constructorparameters worden waargenomen. In wezen zijn constructors die het object op alternatieve manieren initialiseren prima. De beperkingen met betrekking tot vastleggen zijn geen belangrijke wijziging voor bestaande handmatig gedefinieerde kopieerconstructors in records, omdat records nooit hun primaire constructorparameters vastleggen (in plaats daarvan genereren ze velden).

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
    }
}

Primaire constructorteksten

Constructors zelf bevatten vaak parametervalidatielogica of andere niet-triviale initialisatiecode die niet kan worden uitgedrukt als initialisatieprogramma's.

Primaire constructors kunnen worden uitgebreid om instructieblokken rechtstreeks in de klassebody te laten verschijnen. Deze instructies worden ingevoegd in de gegenereerde constructor op het punt waar ze verschijnen tussen initialisatietoewijzingen en zouden dus tussen de initialisatoren worden uitgevoerd. Bijvoorbeeld:

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

Veel van dit scenario kunnen voldoende worden behandeld als we 'definitieve initializers' introduceren die worden uitgevoerd nadat de constructors en alle initialisatiefuncties voor objecten/verzamelingen zijn voltooid. Argumentvalidatie is echter één ding dat idealiter zo vroeg mogelijk zou plaatsvinden.

Primaire constructorlichamen kunnen ook een plaats bieden om een toegangsmodificator voor de primaire constructor toe te staan, zodat deze kan afwijken van de toegankelijkheid van het omsluitende type.

Gecombineerde parameter- en liddeclaraties

Een mogelijke en vaak genoemde toevoeging kan zijn om toe te staan dat primaire constructorparameters worden geannoteerd, zodat ze ook ook een lid van het type declareren. Meestal wordt voorgesteld om een toegangsaanduiding toe te staan voor de parameters om het genereren van leden te activeren:

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
    }
}

Er zijn enkele problemen:

  • Wat als een eigenschap gewenst is en geen veld? Het inline opnemen van { get; set; }-syntaxis in een parameterlijst lijkt niet aantrekkelijk.
  • Wat gebeurt er als verschillende naamconventies worden gebruikt voor parameters en velden? Dan zou deze functie nutteloos zijn.

Dit is een potentiële toekomstige toevoeging die al dan niet kan worden aangenomen. Het huidige voorstel laat de mogelijkheid open.

Open vragen

Opzoekvolgorde voor typeparameters

De sectie https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup geeft aan dat de typeparameters van het declarerende type voor de primaire constructorparameters van het type moeten komen in elke context waarin deze parameters in scope zijn. We hebben echter al bestaand gedrag met records: primaire constructorparameters komen vóór typeparameters in basis initializers en veld initializers.

Wat moeten we doen aan deze discrepantie?

  • Pas de regels aan zodat deze overeenkomen met het gedrag.
  • Pas het gedrag aan (een mogelijke brekende verandering).
  • Sta niet toe dat een primaire constructorparameter de naam van een typeparameter gebruikt (een mogelijke wijziging die fouten kan veroorzaken).
  • Doe niets, accepteer de inconsistentie tussen de specificatie en de implementatie.

Conclusie:

Pas de regels aan zodat deze overeenkomen met het gedrag (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors).

Velddoelkenmerken voor vastgelegde primaire constructorparameters

Moeten we velddoelkenmerken toestaan voor vastgelegde primaire constructorparameters?

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

Op dit moment worden de attributen genegeerd met een waarschuwing, ongeacht of de parameter wordt opgevangen.

Houd er rekening mee dat voor records doelkenmerken voor velden zijn toegestaan wanneer een eigenschap voor deze eigenschap wordt gesynthetiseerd. De kenmerken gaan vervolgens naar het backingveld.

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

Conclusie:

Niet toegestaan (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#attributes-on-captured-parameters).

Waarschuwen bij overschaduwen door een lid van de basis

Moeten we een waarschuwing melden wanneer een lid van de basisstructuur een primaire constructorparameter in een lid overschaduwt (zie https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621)?

Conclusie:

Een alternatief ontwerp wordt goedgekeurd - https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors

Het vastleggen van de instantie van het omsluitende type in een closure.

Wanneer een parameter die is vastgelegd in de status van het omsluitende type ook wordt verwezen naar in een lambda binnen een initializer van een instantie of een basisinitializer, moeten de lambda en de status van het omsluitende type naar dezelfde locatie voor de parameter verwijzen. Bijvoorbeeld:

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

Omdat de naïeve implementatie van het vastleggen van een parameter in de toestand van het type eenvoudigweg de parameter in een privé-instantieve veld vastlegt, moet de lambda naar hetzelfde veld verwijzen. Als gevolg hiervan moet het toegang hebben tot het exemplaar van het type. Hiervoor moet this worden vastgelegd in een closure voordat de basisconstructor wordt aangeroepen. Dat resulteert op zijn beurt in een veilige, maar niet verifieerbare IL. Is dit acceptabel?

U kunt ook het volgende doen:

  • Sta zo'n gebruik van lambdas niet toe.
  • U kunt ervoor kiezen om parameters zoals deze vast te leggen in een instantie van een aparte klasse (nog een andere closure) en die instantie te delen tussen de closure en de instantie van het insluittype. Zo elimineert u de noodzaak om this vast te leggen in een afsluiting.

Conclusie:

We zijn vertrouwd met het vastleggen van this in een closure voordat de basisconstructor wordt aangeroepen (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md). Het runtimeteam vond het IL-patroon ook niet problematisch.

Toewijzen aan this binnen een struct

Met C# kunt u toewijzen aan this binnen een struct. Als de struct een primaire constructorparameter vastlegt, wordt de waarde van de toewijzing overschreven, wat mogelijk niet duidelijk is voor de gebruiker. Willen we een waarschuwing voor opdrachten als deze melden?

struct S(int x)
{
    int X => x;
    
    void M(S s)
    {
        this = s; // 'x' is overwritten
    }
}

Conclusie:

Toegestaan, geen waarschuwing (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).

Dubbele opslagwaarschuwing voor initialisatie plus vastleggen

We hebben een waarschuwing als een primaire constructorparameter wordt doorgegeven aan de basis en ook vastgelegd, omdat er een hoog risico is dat deze per ongeluk tweemaal in het object wordt opgeslagen.

Het lijkt erop dat er een vergelijkbaar risico is als een parameter wordt gebruikt om een lid te initialiseren en ook wordt vastgelegd. Hier volgt een klein voorbeeld:

public class Person(string name)
{
    public string Name { get; set; } = name;   // initialization
    public override string ToString() => name; // capture
}

Voor een bepaald exemplaar van Personworden wijzigingen in Name niet doorgevoerd in de uitvoer van ToString, wat waarschijnlijk onbedoeld is voor de ontwikkelaar.

Moeten we een waarschuwing voor dubbele opslag introduceren voor deze situatie?

Dit werkt als volgt:

De compiler produceert een waarschuwing voor een variable_initializer wanneer aan alle volgende voorwaarden wordt voldaan:

  • De variabele initialisatiefunctie vertegenwoordigt een impliciete of expliciete identiteitsconversie van een primaire constructorparameter;
  • De primaire constructorparameter wordt vastgelegd in de status van het insluittype.

Conclusie:

Goedgekeurd, zie https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors

LDM-vergaderingen