Lezen in het Engels

Delen via


Alleen initiële setters

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 notulen van de taalontwerpvergadering.

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

Probleem met kampioen: https://github.com/dotnet/csharplang/issues/39

Samenvatting

Dit voorstel voegt het concept van alleen init-eigenschappen en indexeerfuncties toe aan C#. Deze eigenschappen en indexeerfuncties kunnen worden ingesteld op het moment van het maken van objecten, maar worden effectief get pas nadat het maken van objecten is voltooid. Dit maakt een veel flexibeler onveranderbaar model mogelijk in C#.

Motivatie

De onderliggende mechanismen voor het bouwen van onveranderbare gegevens in C# zijn sinds 1.0 niet gewijzigd. Ze blijven:

  1. Velden gedefinieerd als readonly.
  2. Eigenschappen declareren die alleen een get accessor bevatten.

Deze mechanismen zijn effectief in het creëren van onveranderbare gegevens, maar ze doen dit door extra complexiteit toe te voegen aan de standaardcode van typen en deze typen uit te sluiten van functies zoals object- en verzamelingsinitialiseringen. Dit betekent dat ontwikkelaars moeten kiezen tussen gebruiksgemak en onveranderbaarheid.

Een eenvoudig onveranderbaar object zoals Point twee keer zoveel ketelplaatcode nodig heeft om de constructie te ondersteunen als voor het declareren van het type. Hoe groter het type hoe groter de kosten van deze boilerplaat:

cs
struct Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }
}

De init-accessor maakt immutabele objecten flexibeler doordat de aanroeper de leden kan muteren tijdens de constructie. Dit betekent dat de onveranderbare eigenschappen van het object kunnen deelnemen aan object-initializers en daardoor de noodzaak voor de standaardcode van de constructor in het type vervangt. Het Point type is nu simpelweg:

cs
struct Point
{
    public int X { get; init; }
    public int Y { get; init; }
}

De consument kan vervolgens object initializers gebruiken om het object te maken

cs
var p = new Point() { X = 42, Y = 13 };

Gedetailleerd ontwerp

initialize accessoren

Een init only eigenschap (of indexeerfunctie) wordt gedeclareerd met behulp van de init accessor in plaats van de set accessor:

cs
class Student
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

Een instantieseigenschap met een init accessor wordt beschouwd als instelbaar in de volgende omstandigheden, behalve in een lokale functie of lambda:

  • Tijdens een object-initialisatie
  • Tijdens een with-expressie-initialisatie
  • Binnen een exemplaarconstructor van het betreffende of afgeleide type, op this of base
  • Binnen de init accessor van een eigenschap, op this of base
  • Gebruik van kenmerken binnen een specifieke context met benoemde parameters

De hierboven genoemde tijden waarin de init accessors in te stellen zijn, worden in dit document gezamenlijk aangeduid als de constructiefase van het object.

Dit betekent dat de Student klasse op de volgende manieren kan worden gebruikt:

cs
var s = new Student()
{
    FirstName = "Jared",
    LastName = "Parosns",
};
s.LastName = "Parsons"; // Error: LastName is not settable

De regels over wanneer init-accessors instelbaar zijn, breiden zich uit over typehiërarchieën. Als het lid toegankelijk is en bekend is dat het object in de constructiefase verkeert, is het lid instelbaar. Dit maakt specifiek het volgende mogelijk:

cs
class Base
{
    public bool Value { get; init; }
}

class Derived : Base
{
    public Derived()
    {
        // Not allowed with get only properties but allowed with init
        Value = true;
    }
}

class Consumption
{
    void Example()
    {
        var d = new Derived() { Value = true };
    }
}

Op het moment dat een init accessor wordt aangeroepen, is het bekend dat de instantie zich in de open bouwfase bevindt. Daarom mag een init accessor de volgende acties uitvoeren, naast wat een normale set accessor kan doen:

  1. Andere init accessors aanroepen die beschikbaar zijn via this of base
  2. Wijs readonly velden toe die zijn gedeclareerd voor hetzelfde type via this
cs
class Complex
{
    readonly int Field1;
    int Field2;
    int Prop1 { get; init; }
    int Prop2
    {
        get => 42;
        init
        {
            Field1 = 13; // okay
            Field2 = 13; // okay
            Prop1 = 13; // okay
        }
    }
}

De mogelijkheid om readonly velden toe te wijzen vanuit een init-accessor, is beperkt tot die velden die zijn gedeclareerd voor hetzelfde type als de accessor. Het kan niet worden gebruikt om readonly velden in een basistype toe te wijzen. Deze regel zorgt ervoor dat auteurs van het type de controle houden over de mutabiliteit van hun type. Ontwikkelaars die geen gebruik willen maken van init kunnen niet worden beïnvloed door andere typen die hiervoor kiezen:

cs
class Base
{
    internal readonly int Field;
    internal int Property
    {
        get => Field;
        init => Field = value; // Okay
    }

    internal int OtherProperty { get; init; }
}

class Derived : Base
{
    internal readonly int DerivedField;
    internal int DerivedProperty
    {
        get => DerivedField;
        init
        {
            DerivedField = 42;  // Okay
            Property = 0;       // Okay
            Field = 13;         // Error Field is readonly
        }
    }

    public Derived()
    {
        Property = 42;  // Okay 
        Field = 13;     // Error Field is readonly
    }
}

Wanneer init in een virtuele eigenschap wordt gebruikt, moeten alle overschrijvingen ook als initworden gemarkeerd. Het is ook niet mogelijk om een eenvoudig set met initte overschrijven.

cs
class Base
{
    public virtual int Property { get; init; }
}

class C1 : Base
{
    public override int Property { get; init; }
}

class C2 : Base
{
    // Error: Property must have init to override Base.Property
    public override int Property { get; set; }
}

Een interface declaratie kan ook deelnemen aan init stijlinitialisatie via het volgende patroon:

cs
interface IPerson
{
    string Name { get; init; }
}

class Init
{
    void M<T>() where T : IPerson, new()
    {
        var local = new T()
        {
            Name = "Jared"
        };
        local.Name = "Jraed"; // Error
    }
}

Beperkingen van deze functie:

  • De init accessor kan alleen worden gebruikt voor instantie-eigenschappen
  • Een eigenschap kan niet zowel een init als set accessor bevatten
  • Alle overschrijvingen van een eigenschap moeten init hebben als de basis init. Deze regel is ook van toepassing op interface-implementatie.

Alleen-lezen structuren

init accessors (zowel automatisch geïmplementeerde accessors als handmatig geïmplementeerde accessors) zijn toegestaan op readonly struct-eigenschappen en readonly-eigenschappen. init accessors mogen niet zelf readonly aangemerkt worden, zowel in readonly als in niet-readonlystruct types.

cs
readonly struct ReadonlyStruct1
{
    public int Prop1 { get; init; } // Allowed
}

struct ReadonlyStruct2
{
    public readonly int Prop2 { get; init; } // Allowed

    public int Prop3 { get; readonly init; } // Error
}

Metagegevenscodering

Eigenschap init accessors worden verzonden als een standaard set accessor met het retourtype gemarkeerd met een modreq van IsExternalInit. Dit is een nieuw type dat de volgende definitie heeft:

cs
namespace System.Runtime.CompilerServices
{
    public sealed class IsExternalInit
    {
    }
}

De compiler komt overeen met het type op volledige naam. Er is geen vereiste dat deze wordt weergegeven in de kernbibliotheek. Als er meerdere typen met deze naam zijn, zal de compiler ze in de volgende volgorde ontwarren:

  1. De gedefinieerde in het project dat wordt gecompileerd
  2. De in corelib gedefinieerde

Als geen van deze twee bestaat, wordt er een type ambiguïteitsfout weergegeven.

Het ontwerp voor IsExternalInit wordt verder in dit nummer behandeld.

Vragen

Belangrijke wijzigingen

Een van de belangrijkste draaipunten in de manier waarop deze functie wordt gecodeerd, komt neer op de volgende vraag:

Is het een breaking change om init te vervangen door set?

Bij het vervangen van init door set en het daarmee volledig beschrijfbaar maken van een eigenschap, is er nooit sprake van een bronbrekende verandering bij een niet-virtuele eigenschap. De scenario's waarin de eigenschap geschreven kan worden, worden eenvoudigweg uitgebreid. De enige vraag is of dit al dan niet een binaire onderbreking blijft.

Als we van de wijziging van init naar set een bron- en binaire compatibele wijziging willen maken, dan dwingt dat ons om een beslissing te nemen over de keuze tussen modreqs en attributen hieronder, omdat dit modreqs als een oplossing uitsluit. Als dit aan de andere kant wordt gezien als iets niet-interessants, zal dit de beslissing tussen modreq en kenmerk minder impactvol maken.

oplossing Dit scenario wordt niet gezien als overtuigend door LDM.

Modreqs versus kenmerken

De uitvoerstrategie voor init-eigenschapstoegangsmethoden moet kiezen tussen het gebruik van kenmerken of modreqs bij het genereren van metagegevens. Deze hebben verschillende afwegingen die moeten worden overwogen.

Annoteren van een eigenschapssettoegangsfunctie met een modreq-declaratie betekent dat CLI-compatibele compilers de toegangsfunctie negeren, tenzij de compiler de modreq begrijpt. Dat betekent dat alleen compilers die op de hoogte zijn van init het lid lezen. Compilers die zich niet bewust zijn van init negeren de set accessor en behandelen de eigenschap daarom niet per ongeluk als lezen/schrijven.

Het nadeel van modreq is, init wordt onderdeel van de binaire signatuur van de set accessor. Als u init toevoegt of verwijdert, wordt de binaire compatibiliteit van de toepassing verbroken.

Door attributen toe te passen op de set-accessor, zullen alleen compilers die het attribuut begrijpen weten hoe zij de toegang moeten beperken. Een compiler die zich niet bewust is van init ziet deze als een eenvoudige lees-/schrijfeigenschap en staat toegang toe.

Dit zou schijnbaar betekenen dat deze beslissing een keuze is tussen extra veiligheid ten koste van binaire compatibiliteit. Bij nader onderzoek is de extra veiligheid niet precies wat het lijkt. Het zal niet beschermen tegen de volgende omstandigheden:

  1. Reflectie op public-leden
  2. Het gebruik van dynamic
  3. Compilers die modreqs niet herkennen

Het moet ook worden overwogen dat wanneer we de IL-verificatieregels voor .NET 5 voltooien, init een van deze regels is. Dat betekent dat er extra afdwinging wordt verkregen van het simpelweg verifiëren van compilers die verifieerbare IL verzenden.

De primaire talen voor .NET (C#, F# en VB) worden allemaal bijgewerkt om deze init accessors te herkennen. Daarom is het enige realistische scenario hier wanneer een C# 9-compiler init eigenschappen verzendt en ze worden gezien door een oudere toolset zoals C# 8, VB 15, enzovoort... C# 8. Dat is de afweging die je moet maken en afwegen tegen binaire compatibiliteit.

Opmerking Deze discussie is voornamelijk alleen van toepassing op leden, niet op velden. Hoewel init velden zijn geweigerd door LDM, zijn ze nog steeds interessant om rekening te houden met de modreq versus kenmerkdiscussie. De init functie voor velden is een versoepeling van de bestaande beperking van readonly. Dat betekent dat als we de velden verzenden als readonly + een kenmerk, er geen risico bestaat dat oudere compilers het veld verkeerd gebruiken, omdat ze readonlyal zouden herkennen. Daarom voegt het gebruik van een modreq hier geen extra beveiliging toe.

Resolution De functie gebruikt een modreq om de eigenschap init setter te coderen. De overtuigende factoren waren (in geen bepaalde volgorde):

  • Verlangen om oudere compilers te ontmoedigen om init semantiek te schenden
  • Wens om het toevoegen of verwijderen van init in een virtual-declaratie of interface zowel een bron- als binaire breukverandering te maken.

Aangezien er ook geen aanzienlijke ondersteuning was voor het verwijderen van init om een binaire compatibele wijziging te zijn, maakte het de keuze om modreq rechtstreeks te gebruiken.

init tegen initonly

Er waren drie syntaxisformulieren die belangrijke aandacht kregen tijdens onze LDM-vergadering:

cs
// 1. Use init 
int Option1 { get; init; }
// 2. Use init set
int Option2 { get; init set; }
// 3. Use initonly
int Option3 { get; initonly; }

resolutie Er was geen syntaxis die een overweldigende voorkeur had in LDM.

Een punt dat veel aandacht kreeg, was hoe de keuze van syntaxis van invloed zou zijn op ons vermogen om in de toekomst init-leden als een algemene eigenschap in te zetten. Als u optie 1 kiest, zou het lastig zijn om een eigenschap te definiëren met een init stijl get methode in de toekomst. Uiteindelijk werd besloten dat als we ervoor zouden kiezen om in de toekomst door te gaan met generieke init-leden, we init als modificator in de lijst met eigenschapstoegang konden toestaan, evenals als afkorting voor init set. In wezen zouden de volgende twee declaraties identiek zijn.

cs
int Property1 { get; init; }
int Property1 { get; init set; }

De beslissing is genomen om door te gaan met init als zelfstandige accessor in de lijst van eigenschappentoegangselementen.

Waarschuwen bij mislukte initialisatie

Houd rekening met het volgende scenario. Een type declareert een init alleenstaand lid dat niet is ingesteld in de constructor. Moet de code waarmee het object wordt gemaakt, een waarschuwing krijgen als ze de waarde niet konden initialiseren?

Op dat moment is het duidelijk dat het veld nooit wordt ingesteld en daarom veel overeenkomsten heeft met de waarschuwing omtrent het falen om private data te initialiseren. Daarom zou een waarschuwing hier schijnbaar enige waarde hebben?

Er zijn echter aanzienlijke nadelen voor deze waarschuwing:

  1. Het bemoeilijkt het compatibiliteitsverhaal van het wijzigen van readonly in init.
  2. Hiervoor moeten extra metadata worden meegenomen om aan te geven welke leden moeten worden geïnitialiseerd door de aanroeper.

Verder, als we geloven dat er in het algemene scenario waarde is om objectmakers te verplichten zich bewust te zijn van fouten of waarschuwingen over specifieke velden, dan is dit waarschijnlijk logisch als een algemene functie. Er is geen reden dat het beperkt moet zijn tot alleen init leden.

Oplossing Er wordt geen waarschuwing gegeven over het verbruik van init velden en eigenschappen.

LDM wil een bredere discussie voeren over het idee van vereiste velden en eigenschappen. Dat kan ertoe leiden dat we terugkomen en ons standpunt over init leden en validatie herzien.

Init toestaan als een veldmodificator

Op dezelfde manier kan init fungeren als een eigenschapstoegang, het kan ook dienen als een aanduiding voor velden om ze vergelijkbaar gedrag te geven als init-eigenschappen. Hierdoor kan het veld worden toegewezen voordat de constructie voltooid is, door middel van het type, afgeleide typen of objectinitialisaties.

cs
class Student
{
    public init string FirstName;
    public init string LastName;
}

var s = new Student()
{
    FirstName = "Jarde",
    LastName = "Parsons",
}

s.FirstName = "Jared"; // Error FirstName is readonly

In metagegevens worden deze velden op dezelfde manier gemarkeerd als readonly velden, maar met een extra kenmerk of modreq om aan te geven dat ze init stijlvelden zijn.

resolutie LDM stemt ermee in dat dit voorstel goed is, maar over het algemeen leek het scenario los te staan van eigenschappen. De beslissing was om voorlopig alleen door te gaan met init eigenschappen. Dit heeft een geschikt flexibiliteitsniveau omdat een init eigenschap een readonly veld kan muteren op het type waarin de eigenschap is gedeclareerd. Dit wordt opnieuw bekeken als er aanzienlijke feedback van klanten is die het scenario rechtvaardigt.

Init toestaan als een typewijziging

Op dezelfde manier kan de readonly modifier worden toegepast op een struct om automatisch alle velden als readonlyte declareren, kan de init alleen modifier worden gedeclareerd op een struct of class om automatisch alle velden als initte markeren. Dit betekent dat de volgende twee typedeclaraties gelijkwaardig zijn:

cs
struct Point
{
    public init int X;
    public init int Y;
}

// vs. 

init struct Point
{
    public int X;
    public int Y;
}

Oplossing Deze functie is hier te schattig en conflicteert met de readonly struct functie waarop deze is gebaseerd. De readonly struct functie is eenvoudig omdat deze readonly toepast op alle leden: velden, methoden, enzovoort... De functie init struct is alleen van toepassing op eigenschappen. Dit maakt het uiteindelijk verwarrend voor gebruikers.

Gezien het feit dat init alleen geldig is voor bepaalde aspecten van een type, hebben we het idee afgewezen om het als een type modifier te gebruiken.

Overwegingen

Compatibiliteit

De functie init is ontworpen om alleen compatibel te zijn met bestaande get eigenschappen. Het is met name bedoeld als een geheel aanvullende verandering voor een eigenschap die momenteel get is, maar een flexibelere objectcreatiesemantiek wenst.

Denk bijvoorbeeld aan het volgende type:

cs
class Name
{
    public string First { get; }
    public string Last { get; }

    public Name(string first, string last)
    {
        First = first;
        Last = last;
    }
}

Het toevoegen van init aan deze eigenschappen is een niet-belangrijke wijziging:

cs
class Name
{
    public string First { get; init; }
    public string Last { get; init; }

    public Name(string first, string last)
    {
        First = first;
        Last = last;
    }
}

IL-verificatie

Wanneer .NET Core besluit il-verificatie opnieuw te implementeren, moeten de regels worden aangepast om rekening te houden met init leden. Dit moet worden opgenomen in de regelwijzigingen voor niet-muteren toegang tot de readonly-gegevens.

De IL-verificatieregels moeten in twee delen worden onderverdeeld:

  1. Hiermee kunnen init leden een readonly veld instellen.
  2. Bepalen wanneer een init lid wettelijk kan worden aangeroepen.

De eerste is een eenvoudige aanpassing van de bestaande regels. De IL-verificator kan worden geleerd om init leden te herkennen en van daaruit moet alleen een readonly veld worden ingesteld op this in een dergelijk lid.

De tweede regel is ingewikkelder. In het eenvoudige geval van object-initialiseerders is de regel eenvoudig. Het moet legaal zijn om init-leden aan te roepen wanneer het resultaat van een new-uitdrukking nog op de stapel staat. Dat is totdat de waarde is opgeslagen in een lokaal, matrixelement of veld of als argument is doorgegeven aan een andere methode, is het nog steeds legaal om init leden aan te roepen. Dit zorgt ervoor dat zodra het resultaat van de new-expressie is gepubliceerd naar een benoemde identifier (anders dan this), het niet langer toegestaan is om de leden van init aan te roepen.

Echter, het ingewikkelder geval is wanneer we init-leden, object initializers en awaitcombineren. Dit kan ertoe leiden dat het zojuist gemaakte object tijdelijk in een statusmachine wordt gehesen en dus in een veld wordt geplaatst.

cs
var student = new Student() 
{
    Name = await SomeMethod()
};

Hier wordt het resultaat van new Student() in een statusmachine gehesen als veld voordat de set Name plaatsvindt. De compiler moet dergelijke hoistvelden markeren op een manier die de IL-verificator begrijpt dat ze niet toegankelijk zijn voor gebruikers en dus niet in strijd is met de beoogde semantiek van init.

init-leden

De init modificator zou kunnen worden uitgebreid om van toepassing te zijn op alle instantieleden. Hiermee wordt het concept van init tijdens de objectconstructie generaliseren en kunnen typen helpermethoden declareren die kunnen deelnemen aan het bouwproces om init velden en eigenschappen te initialiseren.

Dergelijke leden zouden alle beperkingen hebben die een init accessor in dit ontwerp heeft. De noodzaak is echter twijfelbaar en dit kan veilig worden toegevoegd in een toekomstige versie van de taal op een compatibele manier.

Drie accessors genereren

Een mogelijke implementatie van init eigenschappen is om init volledig gescheiden te maken van set. Dat betekent dat een eigenschap mogelijk drie verschillende toegangspunten kan hebben: get, seten init.

Dit heeft het mogelijke voordeel dat het gebruik van modreq de juistheid afdwingt terwijl binaire compatibiliteit behouden blijft. De implementatie zou ongeveer het volgende zijn:

  1. Een init accessor wordt altijd verzonden als er een setis. Wanneer deze niet is gedefinieerd door de ontwikkelaar, is het gewoon een verwijzing naar set.
  2. De set van een eigenschap in een object-initializer gebruikt altijd init indien aanwezig, maar valt terug op set als deze ontbreekt.

Dit betekent dat een ontwikkelaar init altijd veilig uit een eigenschap kan verwijderen.

Het nadeel van dit ontwerp is dat alleen nuttig is als init altijd altijd verzonden wanneer er een setis. De taal kan niet weten of init in het verleden is verwijderd, moet ervan worden uitgegaan dat het was en daarom moet de init altijd worden verzonden. Dit zou leiden tot een aanzienlijke uitbreiding van metagegevens en is simpelweg de kosten van de compatibiliteit hier niet waard.