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
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#.
De onderliggende mechanismen voor het bouwen van onveranderbare gegevens in C# zijn sinds 1.0 niet gewijzigd. Ze blijven:
- Velden gedefinieerd als
readonly
. - 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:
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:
struct Point
{
public int X { get; init; }
public int Y { get; init; }
}
De consument kan vervolgens object initializers gebruiken om het object te maken
var p = new Point() { X = 42, Y = 13 };
Een init only eigenschap (of indexeerfunctie) wordt gedeclareerd met behulp van de init
accessor in plaats van de set
accessor:
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
ofbase
- Binnen de
init
accessor van een eigenschap, opthis
ofbase
- 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:
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:
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:
- Andere
init
accessors aanroepen die beschikbaar zijn viathis
ofbase
- Wijs
readonly
velden toe die zijn gedeclareerd voor hetzelfde type viathis
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:
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 init
worden gemarkeerd. Het is ook niet mogelijk om een eenvoudig set
met init
te overschrijven.
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:
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
alsset
accessor bevatten - Alle overschrijvingen van een eigenschap moeten
init
hebben als de basisinit
. Deze regel is ook van toepassing op interface-implementatie.
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-readonly
struct
types.
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
}
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:
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:
- De gedefinieerde in het project dat wordt gecompileerd
- 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.
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 doorset
?
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.
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:
- Reflectie op
public
-leden - Het gebruik van
dynamic
- 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 readonly
al 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 eenvirtual
-declaratie ofinterface
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.
Er waren drie syntaxisformulieren die belangrijke aandacht kregen tijdens onze LDM-vergadering:
// 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.
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.
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:
- Het bemoeilijkt het compatibiliteitsverhaal van het wijzigen van
readonly
ininit
. - 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.
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.
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.
Op dezelfde manier kan de readonly
modifier worden toegepast op een struct
om automatisch alle velden als readonly
te declareren, kan de init
alleen modifier worden gedeclareerd op een struct
of class
om automatisch alle velden als init
te markeren.
Dit betekent dat de volgende twee typedeclaraties gelijkwaardig zijn:
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.
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:
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:
class Name
{
public string First { get; init; }
public string Last { get; init; }
public Name(string first, string last)
{
First = first;
Last = last;
}
}
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:
- Hiermee kunnen
init
leden eenreadonly
veld instellen. - 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 await
combineren. Dit kan ertoe leiden dat het zojuist gemaakte object tijdelijk in een statusmachine wordt gehesen en dus in een veld wordt geplaatst.
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
.
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.
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
, set
en 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:
- Een
init
accessor wordt altijd verzonden als er eenset
is. Wanneer deze niet is gedefinieerd door de ontwikkelaar, is het gewoon een verwijzing naarset
. - De set van een eigenschap in een object-initializer gebruikt altijd
init
indien aanwezig, maar valt terug opset
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 set
is. 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.
C# feature specifications-feedback
C# feature specifications is een open source project. Selecteer een koppeling om feedback te geven: