Delen via


field trefwoord in eigenschappen

Kampioenprobleem: https://github.com/dotnet/csharplang/issues/8635

Samenvatting

Breid alle eigenschappen uit zodat ze kunnen verwijzen naar een automatisch gegenereerd backingveld met behulp van het nieuwe contextuele trefwoord field. Eigenschappen kunnen nu ook een accessor bevatten zonder implementatie, naast een accessor met implementatie.

Motivatie

Automatische eigenschappen staan alleen toe dat het back-end veld rechtstreeks kan worden ingesteld of opgehaald, waarbij enige controle wordt geboden door toegangsmodificatoren op de accessoren te plaatsen. Soms is het nodig om extra controle te hebben over wat er gebeurt in een of beide accessors, maar dit confronteert gebruikers met de overhead van het declareren van een back-upveld. De naam van het backing-veld moet vervolgens gesynchroniseerd worden gehouden met de eigenschap en het backing-veld is van toepassing op de gehele klasse, wat kan leiden tot het onbedoeld omzeilen van de accessors vanuit binnen de klas zelf.

Er zijn verschillende veelvoorkomende scenario's. Binnen de getter is er luie initialisatie of worden standaardwaarden gebruikt wanneer de eigenschap nog nooit is ingesteld. In de setter wordt een beperking toegepast om de geldigheid van een waarde te garanderen of updates te detecteren en door te geven, bijvoorbeeld door de INotifyPropertyChanged.PropertyChanged gebeurtenis op te halen.

In deze gevallen moet u nu altijd een instantieveld maken en de hele eigenschap zelf definiëren. Dit voegt niet alleen een behoorlijke hoeveelheid code toe, maar het lekt ook het ondersteunende veld naar de rest van het bereik van dit type, terwijl het vaak wenselijk is deze alleen beschikbaar te maken voor de bodies van de accessoren.

Glossarium

  • Auto-eigenschap: Afkorting voor "automatisch geïmplementeerde eigenschap" (§15.7.4). Accessors op een automatische eigenschap hebben geen hoofdtekst. De implementatie- en back-upopslag worden beide geleverd door de compiler. Automatische eigenschappen hebben { get; }, { get; set; }of { get; init; }.

  • automatische toegangsfunctie: afkorting voor 'automatisch geïmplementeerde toegangsfunctie'. Dit is een accessor die geen lichaam heeft. De implementatie- en back-upopslag worden beide geleverd door de compiler. get;, set; en init; zijn automatische toegangselementen.

  • volledige accessor: Dit is een accessor met een lichaam. De implementatie wordt niet geleverd door de compiler, hoewel de back-upopslag mogelijk nog steeds is (zoals in het voorbeeld set => field = value;).

  • eigenschap met veldsteun: dit is een eigenschap die gebruikmaakt van het field trefwoord in een accessor-lichaam of een automatisch gegenereerde eigenschap.

  • ondersteunende veld: dit is de variabele die wordt aangeduid door de keyword field in de accessors van een property, die ook impliciet wordt gelezen of geschreven in automatisch geïmplementeerde accessors (get;, set;of init;).

Gedetailleerd ontwerp

Voor eigenschappen met een init accessor is alles wat hieronder van toepassing is op set in plaats daarvan van toepassing op de init accessor.

Er zijn twee syntaxiswijzigingen:

  1. Er is een nieuw contextueel trefwoord, field, dat kan worden gebruikt in eigenschapstoegangsorganen voor toegang tot een backingveld voor de eigenschapsdeclaratie (LDM-beslissing).

  2. Eigenschappen kunnen nu automatische accessors combineren en matchen met volledige accessors (LDM-beslissing). "Auto-eigenschap" betekent nog steeds een eigenschap waarvan de toegangsfuncties geen lichaam hebben. Geen van de onderstaande voorbeelden wordt beschouwd als automatische eigenschappen.

Voorbeelden:

{ get; set => Set(ref field, value); }
{ get => field ?? parent.AmbientValue; set; }

Beide accessors kunnen volledige accessors zijn, waarbij een of beide gebruikmaken van field.

{ get => field; set => field = value; }
{ get => field; set => throw new InvalidOperationException(); }
{ get => overriddenValue; set => field = value; }
{
    get;
    set
    {
        if (field == value) return;
        field = value;
        OnXyzChanged();
    }
}

Eigenschappen die tot uitdrukking worden gebracht en eigenschappen met alleen een get accessormethode kunnen ook fieldgebruiken:

public string LazilyComputed => field ??= Compute();
public string LazilyComputed { get => field ??= Compute(); }

Eigenschappen die alleen zijn ingesteld, kunnen ook gebruikmaken van field:

{
    set
    {
        if (field == value) return;
        field = value;
        OnXyzChanged(new XyzEventArgs(value));
    }
}

Belangrijke wijzigingen

Het bestaan van het field contextuele trefwoord in eigenschappentoegangsteksten is een potentieel belangrijke wijziging.

Omdat field een trefwoord en geen identificator is, kan deze alleen worden overschaduwd door een identificator via de normale manier van trefwoorden escapen: @field. Alle identifiers met de naam field gedeclareerd binnen eigenschappentoegangsmethoden kunnen bescherming bieden tegen storingen bij het upgraden van C#-versies vóór 14 door de initiële @toe te voegen.

Als een variabele met de naam field wordt gedeclareerd in een eigenschapstoegangsmethode, wordt er een fout gerapporteerd.

In taalversie 14 of hoger wordt een waarschuwing gerapporteerd als een primaire uitdrukkingfield naar het onderliggende veld verwijst, maar in een eerdere taalversie naar een ander symbool zou verwijzen.

Op velden gerichte kenmerken

Net als bij automatische eigenschappen kan elke eigenschap die gebruikmaakt van een back-upveld in een van de bijbehorende accessors gebruikmaken van op velden gerichte kenmerken:

[field: Xyz]
public string Name => field ??= Compute();

[field: Xyz]
public string Name { get => field; set => field = value; }

Een attribuut gericht op een veld blijft ongeldig, tenzij een accessor een ondersteunend veld gebruikt.

// ❌ Error, will not compile
[field: Xyz]
public string Name => Compute();

Initializers voor eigenschappen

Eigenschappen met initializers kunnen fieldgebruiken. Het backing field wordt direct geïnitialiseerd in plaats van dat de setter wordt aangeroepen (LDM-beslissing).

Het aanroepen van een setter voor een initialisatiefunctie is geen optie; initializers worden verwerkt voordat basisconstructors worden aangeroepen en het is illegaal om een instantiemethode aan te roepen voordat de basisconstructor wordt aangeroepen. Dit is ook belangrijk voor standaard initialisatie/definitieve toewijzing van structs.

Dit levert flexibele controle over initialisatie op. Als u wilt initialiseren zonder de setter aan te roepen, gebruikt u een initialisatiefunctie voor eigenschappen. Als u de eigenschap wilt initialiseren door de setter aan te roepen, wijst u in de constructor een initiële waarde aan de eigenschap toe.

Hier volgt een voorbeeld van waar dit nuttig is. We geloven dat het field trefwoord veel gebruikt zal worden met viewmodels vanwege de elegante oplossing die het biedt voor het INotifyPropertyChanged-patroon. Weergavemodeleigenschappensetters zijn waarschijnlijk gegevensgebonden naar de gebruikersinterface en veroorzaken waarschijnlijk het bijhouden van wijzigingen of het activeren van ander gedrag. De volgende code moet de standaardwaarde van IsActive initialiseren zonder HasPendingChanges in te stellen op true:

class SomeViewModel
{
    public bool HasPendingChanges { get; private set; }

    public bool IsActive { get; set => Set(ref field, value); } = true;

    private bool Set<T>(ref T location, T value)
    {
        if (RuntimeHelpers.Equals(location, value))
            return false;

        location = value;
        HasPendingChanges = true;
        return true;
    }
}

Dit verschil in gedrag tussen een initialisator voor eigenschappen en het toewijzen vanuit de constructor kan ook worden gezien bij virtuele auto-eigenschappen in eerdere versies van de taal.

using System;

// Nothing is printed; the property initializer is not
// equivalent to `this.IsActive = true`.
_ = new Derived();

class Base
{
    public virtual bool IsActive { get; set; } = true;
}

class Derived : Base
{
    public override bool IsActive
    {
        get => base.IsActive;
        set
        {
            base.IsActive = value;
            Console.WriteLine("This will not be reached");
        }
    }
}

Constructortoewijzing

Net als bij automatische eigenschappen, roept de toewijzing in de constructor, als er een (mogelijk virtuele) setter bestaat, deze aan; en als er geen setter is, wordt er direct toegewezen aan het onderliggende veld.

class C
{
    public C()
    {
        P1 = 1; // Assigns P1's backing field directly
        P2 = 2; // Assigns P2's backing field directly
        P3 = 3; // Calls P3's setter
        P4 = 4; // Calls P4's setter
    }

    public int P1 => field;
    public int P2 { get => field; }
    public int P4 { get => field; set => field = value; }
    public int P3 { get => field; set; }
}

Definitieve toewijzing in structs

Hoewel er niet naar kan worden verwezen in de constructor, zijn back-upvelden die worden aangeduid door het trefwoord field onderworpen aan standaard-initialisatie- en uitgeschakelde standaardwaarschuwingen onder dezelfde voorwaarden als andere structvelden (LDM-beslissing 1, LDM-beslissing 2).

Bijvoorbeeld (deze diagnostische gegevens zijn standaard stil):

public struct S
{
    public S()
    {
        // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
        // assignments of 'default' to non-explicitly assigned fields.
        _ = P1;
    }

    public int P1 { get => field; }
}
public struct S
{
    public S()
    {
        // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
        // assignments of 'default' to non-explicitly assigned fields.
        P2 = 5;
    }

    public int P2 { get => field; set => field = value; }
}

Eigenschappen die opnieuw worden geretourneerd

Net als bij automatische eigenschappen is het field trefwoord niet beschikbaar voor gebruik in de eigenschappen voor het retourneren van ref. Eigenschappen die opnieuw worden geretourneerd, kunnen geen toegangsrechten hebben ingesteld en zonder een set accessor zijn de get-accessor en de initialisatiefunctie van de eigenschap de enige die toegang hebben tot het back-upveld. Er ontbreken gebruiksvoorbeelden hiervoor, nu is het niet het moment voor eigenschappen die waarden teruggeven om geschreven te worden als automatische eigenschappen.

Nullbaarheid

Een principe van de functie Nullable Reference Types was om bestaande idiomatische coderingspatronen in C# te begrijpen en om zo weinig mogelijk ceremonie rond deze patronen te vereisen. Het field voorstel voor trefwoorden maakt eenvoudige, idiomatische patronen mogelijk om veelgevraagde scenario's aan te pakken, zoals lui geïnitialiseerde eigenschappen. Het is belangrijk voor de Nullable Reference Types om goed samen te gaan met deze nieuwe coderingspatronen.

Doelen:

  • Een redelijk niveau van null-veiligheid moet worden gegarandeerd voor verschillende gebruikspatronen van de field trefwoordfunctie.

  • Patronen die het trefwoord field gebruiken, moeten het gevoel geven dat ze altijd deel van de taal zijn geweest. Vermijd dat de gebruiker door hoepels moet springen om Nullable Reference Types in te schakelen in code die perfect idiomatisch is voor de field trefwoordfunctie.

Een van de belangrijkste scenario's is vertraagd geïnitialiseerde eigenschappen:

public class C
{
    public C() { } // It would be undesirable to warn about 'Prop' being uninitialized here

    string Prop => field ??= GetPropValue();
}

De volgende regels voor nullabiliteit zijn niet alleen van toepassing op eigenschappen die gebruikmaken van het field trefwoord, maar ook op bestaande automatische eigenschappen.

Null-waarde van het ondersteunend veld

Zie woordenlijst voor definities van nieuwe termen.

Het backing field heeft hetzelfde type als de property. De nullable annotatie kan echter verschillen van de eigenschap. Om deze null-aantekening te bepalen, introduceren we het concept van null-resilience. Null-weerstand betekent intuïtief dat de get accessor van de eigenschap de null-veiligheid behoudt, zelfs als het veld de default waarde voor zijn type bevat.

Een veldgesteunde eigenschap wordt bepaald als zijnde nul-tolerant of niet, door een speciale analyse van zijn get accessor uit te voeren.

  • Voor deze analyse wordt ervan uitgegaan dat field tijdelijk geannoteerde nullability heeft, bijvoorbeeld string?. Dit zorgt ervoor dat fieldmisschien null- of standaard initiële status in de get accessor hebben, afhankelijk van het type.
  • Als een null-analyse van de getter geen null-waarschuwingen oplevert, wordt de eigenschap null-bestendig. Anders is het niet null-bestendig.
  • Als de eigenschap geen get accessor heeft, is deze (leeg) null-tolerant.
  • Als de get-accessor automatisch is geïmplementeerd, is de eigenschap niet null-resilient.

De nullbaarheid van het backing field wordt als volgt bepaald:

  • Als het veld null-abilitykenmerken heeft, zoals [field: MaybeNull], AllowNull, NotNullof DisallowNull, is de null-aantekening van het veld hetzelfde als de null-aantekening van de eigenschap.
    • Dit komt doordat wanneer de gebruiker begint met het toepassen van null-abilitykenmerken op het veld, we niets meer willen afleiden, we willen alleen dat de null-waarde wat de gebruiker zei.
  • Als de bevatte eigenschap niet-opvallend of geannoteerde nullability heeft, dan heeft het ondersteuningsveld dezelfde nullability als de eigenschap.
  • Als de bevatte eigenschap niet-geannoteerde nullabiliteit (bijvoorbeeld string of T) of het kenmerk [NotNull] heeft, en de eigenschap null-resilient is, heeft het backingveld geannoteerde nullabiliteit.
  • Als de bevatte eigenschap niet-geannoteerde nullability (bijvoorbeeld string of T) of het kenmerk [NotNull] heeft en de eigenschap niet null-bestendig is, heeft het backing-veld niet-geannoteerde null-beschikbaarheid.

Constructoranalyse

Momenteel wordt een automatische eigenschap op vergelijkbare wijze behandeld als een gewoon veld in nullable constructor-analyse. We breiden deze behandeling uit naar eigenschapen met veldsteun, door elke eigenschap met veldsteun te beschouwen als een proxy voor het ondersteunende veld.

We werken de volgende specificatietaal bij van de vorige voorgestelde benadering om dit te bereiken:

Bij elke expliciete of impliciete 'return' in een constructor geven we een waarschuwing voor elk lid waarvan de flow-toestand niet compatibel is met de bijbehorende annotaties en nullability-kenmerken. Als het lid een veldondersteunde eigenschap is, wordt de nullability-aantekening van het ondersteunend veld gebruikt voor deze controle. Anders wordt de nullable-annotatie van het lid zelf gebruikt. Een geschikt equivalent hiervoor is: als het toewijzen van het lid aan zichzelf op het terugkeerpunt een waarschuwing voor nullbaarheid zou opleveren, dan wordt er een waarschuwing voor nullbaarheid gegenereerd op het terugkeerpunt.

Houd er rekening mee dat dit in feite een beperkte interprocedurale analyse is. We verwachten dat het bij het analyseren van een constructor nodig zal zijn om binding- en null-resilience-analyse uit te voeren voor alle toepasselijke get-accessors binnen hetzelfde type, die gebruikmaken van het contextuele trefwoord field en zonder annotaties zijn voor nulliteit. We speculeren dat dit niet onbetaalbaar duur is omdat getter-functies meestal niet erg complex zijn en dat de analyse 'null-resilience' slechts één keer hoeft te worden uitgevoerd, ongeacht het aantal constructors in het type.

Setter-analyse

Ter vereenvoudiging gebruiken we de termen 'setter' en 'set accessor' om te verwijzen naar een set of init accessor.

Er moet worden gecontroleerd of de setters van eigenschappen met een achterliggende veld, zoals en, het veld daadwerkelijk initialiseren.

class C
{
    string Prop
    {
        get => field;

        // getter is not null-resilient, so `field` is not-annotated.
        // We should warn here that `field` may be null when exiting.
        set { }
    }

    public C()
    {
        Prop = "a"; // ok
    }

    public static void Main()
    {
        new C().Prop.ToString(); // NRE at runtime
    }
}

De initiële stroomstatus van het back-upveld in de setter van een eigenschap met veldsteun wordt als volgt bepaald:

  • Als de eigenschap een initialisatiefunctie heeft, is de initiële stroomstatus hetzelfde als de stroomstatus van de eigenschap na het bezoeken van de initialisatiefunctie.
  • Anders is de initiële stroomstatus hetzelfde als de stroomstatus die wordt gegeven door field = default;.

Bij elke expliciete of impliciete 'return' in de setter wordt een waarschuwing gerapporteerd als de stroomstatus van het back-upveld niet compatibel is met de annotaties en nullability-kenmerken.

Opmerkingen

Deze formulering lijkt met opzet sterk op gewone velden binnen constructors. Omdat alleen de eigenschapstoegangen daadwerkelijk naar het backingveld kunnen verwijzen, wordt de setter gezien als een 'mini-constructor' voor het backingveld.

Net als bij gewone velden weten we meestal dat de eigenschap in de constructor is geïnitialiseerd omdat deze is ingesteld, maar niet noodzakelijkerwijs. Simpelweg terugkeren binnen een vertakking waar Prop != null waar was, is ook goed genoeg voor onze constructoranalyse, omdat we begrijpen dat niet-bijgehouden mechanismen mogelijk zijn gebruikt om de eigenschap in te stellen.

Alternatieven werden overwogen; zie de sectie Alternatieven voor null-baarheid.

nameof

Op plaatsen waar field een trefwoord is, kan nameof(field) niet compileren (LDM-beslissing), zoals nameof(nint). Het is niet zoals nameof(value), waar je het best van gebruik kunt maken wanneer property-setters een ArgumentException geven, zoals sommige dat in de .NET Core-bibliotheken doen. Daarentegen heeft nameof(field) geen verwachte gebruiksvoorbeelden.

Overschrijvingen

Het kan zijn dat het overschrijven van eigenschappen fieldgebruikt. Dergelijke gebruiksopties van field verwijzen naar het backingveld voor de overschrijvende eigenschap, gescheiden van het backingveld van de basiseigenschap als deze er een heeft. Er is geen ABI voor het blootstellen van het ondersteunende veld van een basiseigenschap aan overervende klassen, omdat hierdoor de inkapseling wordt verbroken.

Net als bij automatische eigenschappen, moeten eigenschappen die gebruikmaken van het field trefwoord en een basiseigenschap overschrijven, alle toegangsmethodes overschrijven (LDM-beslissing).

Vangt

field moeten kunnen worden vastgelegd in lokale functies en lambdas, en verwijzingen naar field vanuit lokale functies en lambdas zijn toegestaan, zelfs als er geen andere verwijzingen zijn (LDM-beslissing 1, LDM-beslissing 2):

public class C
{
    public static int P
    {
        get
        {
            Func<int> f = static () => field;
            return f();
        }
    }
}

Waarschuwingen voor veldgebruik

Wanneer het field trefwoord wordt gebruikt in een accessor, bevat de bestaande compileranalyse van niet-toegewezen of ongelezen velden dat veld.

  • CS0414: Het backing-veld voor eigenschap 'Xyz' is toegewezen, maar de waarde wordt nooit gebruikt
  • CS0649: Het ondersteunende veld voor eigenschap 'Xyz' wordt nooit toegewezen en heeft altijd de standaardwaarde.

Specificatiewijzigingen

Syntaxis

Bij het compileren met taalversie 14 of hoger wordt field als een trefwoord beschouwd wanneer het wordt gebruikt als primaire uitdrukking (LDM-beslissing) op de volgende locaties (LDM-beslissing):

  • In methodelichamen van get, seten init accessors in eigenschappen maar niet indexeerfuncties
  • In kenmerken die zijn toegepast op de accessors
  • In geneste lambda-expressies en lokale functies, en in LINQ-expressies binnen die accessors

In alle andere gevallen, waaronder bij het compileren met taalversie 12 of lager, wordt field beschouwd als een id.

primary_no_array_creation_expression
    : literal
+   | 'field'
    | interpolated_string_expression
    | ...
    ;

Eigenschappen

§15.7.1Eigenschappen - Algemene

Een property_initializer kan alleen worden gegeven voor een automatisch geïmplementeerde eigenschap eneen eigenschap met een back-upveld dat wordt verzonden. De property_initializer veroorzaakt de initialisatie van het onderliggende veld van dergelijke eigenschappen met de waarde die is opgegeven door de expressie.

§15.7.4Automatisch geïmplementeerde eigenschappen

Een automatisch geïmplementeerde eigenschap (of automatische eigenschap voor de korte aanduiding), is een niet-abstracte, niet-externe, niet-ref-waarded eigenschap met alleen-scheidingsteken-accessors. Automatische eigenschappen hebben een get-accessor en kunnen eventueel een set-accessor hebben.of beide van:

  1. een accessor met een hoofdtekst met alleen puntkomma's
  2. gebruik van het contextuele trefwoord field in de hoofdtekst van de eigenschap ofexpressie van de eigenschap

Wanneer een eigenschap wordt opgegeven als een automatisch geïmplementeerde eigenschap, is een verborgen niet-benoemd backingveld automatisch beschikbaar voor de eigenschap en worden de accessors geïmplementeerd om te lezen van en schrijven naar dat backingveld. Voor automatische eigenschappen wordt elke puntkomma alleen get accessor geïmplementeerd om van te lezen en elke puntkomma alleenset accessor om naar het backingveld te schrijven.

Het verborgen backingveld is niet toegankelijk, het kan alleen worden gelezen en geschreven via de automatisch geïmplementeerde eigenschapstoegangsmethoden, zelfs binnen het betreffende type.Het backingveld kan rechtstreeks worden verwezen met behulp van het field trefwoordbinnen alle accessors en binnen de hoofdtekst van de eigenschapsexpressie. Omdat het veld geen naam heeft, kan het niet worden gebruikt in eennameof-expressie.

Als de automatische eigenschap geen set-accessor heeftalleen een get-accessor met een puntkomma, wordt het onderliggende veld beschouwd als readonly (§15.5.3). Net als bij een readonly-veld kan een alleen-lezen auto-eigenschap (zonder set-accessor of init-accessor) ook worden toegewezen binnen de constructor van de omhullende klasse. Een dergelijke opdracht wijst rechtstreeks toe aan het alleen-lezen achterveld van de eigenschap.

Een automatische eigenschap mag niet slechts één puntkomma hebben set accessor zonder een get accessor.

Een automatische eigenschap kan eventueel een property_initializerhebben, die rechtstreeks op het backingveld wordt toegepast als een variable_initializer (§17,7).

Het volgende voorbeeld:

// No 'field' symbol in scope.
public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

is gelijk aan de volgende declaratie:

// No 'field' symbol in scope.
public class Point
{
    public int X { get { return field; } set { field = value; } }
    public int Y { get { return field; } set { field = value; } }
}

dat gelijk is aan:

// No 'field' symbol in scope.
public class Point
{
    private int __x;
    private int __y;
    public int X { get { return __x; } set { __x = value; } }
    public int Y { get { return __y; } set { __y = value; } }
}

Het volgende voorbeeld:

// No 'field' symbol in scope.
public class LazyInit
{
    public string Value => field ??= ComputeValue();
    private static string ComputeValue() { /*...*/ }
}

is gelijk aan de volgende declaratie:

// No 'field' symbol in scope.
public class Point
{
    private string __value;
    public string Value { get { return __value ??= ComputeValue(); } }
    private static string ComputeValue() { /*...*/ }
}

Alternatieven

Alternatieven voor null-baarheid

Naast de benadering van null-weerbaarheid die wordt beschreven in de sectie Nullability, stelde de werkgroep de volgende alternatieven voor ter overweging van de LDM:

Niets doen

We konden hier helemaal geen speciaal gedrag introduceren. In feite:

  • Behandel een door een veld ondersteunde eigenschap op dezelfde manier als automatische eigenschappen vandaag de dag behandeld worden: moet worden geïnitialiseerd in de constructor, behalve als ze als vereist zijn gemarkeerd, enzovoort.
  • Geen speciale behandeling van de veldvariabele bij het analyseren van eigenschapstoegangsors. Het is gewoon een variabele met hetzelfde type en dezelfde null-waarde als de eigenschap.

Houd er rekening mee dat dit zou leiden tot nutteloze waarschuwingen voor 'lazy property-scenario's', in welk geval gebruikers waarschijnlijk null! of iets dergelijks moeten toewijzen om constructorwaarschuwingen te onderdrukken.
Een subalternatief waar we rekening mee kunnen houden, is om eigenschappen ook volledig te negeren door behulp van het field trefwoord voor analyse van nullable constructors. In dat geval zijn er nergens waarschuwingen over de gebruiker die iets moet initialiseren, maar ook geen hinder voor de gebruiker, ongeacht het initialisatiepatroon dat ze kunnen gebruiken.

Omdat we alleen van plan zijn om de field trefwoordfunctie onder de Preview LangVersion in .NET 9 te verzenden, verwachten we dat we het null-gedrag voor de functie in .NET 10 kunnen wijzigen. Daarom kunnen we overwegen om op korte termijn een 'goedkopere' oplossing zoals deze te gebruiken en op lange termijn tot een van de complexere oplossingen te groeien.

field-gerichte nullbaarheidattributen

We kunnen de volgende standaardwaarden introduceren, waardoor een redelijk niveau van null-veiligheid wordt bereikt, zonder dat hiervoor een interprocedurale analyse is betrokken:

  1. De variabele field heeft altijd dezelfde null-aantekening als de eigenschap.
  2. Nullability-kenmerken zoals [field: MaybeNull, AllowNull] enzovoort kunnen worden gebruikt om de nullbaarheid van het backing field aan te passen.
  3. eigenschappen met veldsteun worden gecontroleerd op initialisatie in constructors op basis van de null-aantekening en kenmerken van het veld.
  4. setters in eigenschappen met veldsteun controleren op initialisatie van field vergelijkbaar met constructors.

Dit zou betekenen dat het 'kleine-l lazy scenario' er in plaats daarvan als volgt uit zou zien:

class C
{
    public C() { } // no need to warn about initializing C.Prop, as the backing field is marked nullable using attributes.

    [field: AllowNull, MaybeNull]
    public string Prop => field ??= GetPropValue();
}

Een van de redenen waarom we hier geen nullability-kenmerken hebben gebruikt, is dat de kenmerken die we hebben, echt zijn gericht op het beschrijven van invoer en uitvoer van handtekeningen. Ze zijn omslachtig om te gebruiken om de null-waarde van variabelen met een lange levensduur te beschrijven.

  • In de praktijk is [field: MaybeNull, AllowNull] vereist om ervoor te zorgen dat het veld zich 'redelijk' gedraagt als een null-variabele, wat de mogelijke initiële stroomstatus null geeft en mogelijke null-waarden naar het veld kan worden geschreven. Het voelt omslachtig om gebruikers te vragen dit te doen voor relatief veelvoorkomende triviale scenario's.
  • Als we deze aanpak hebben gevolgd, zouden we overwegen om een waarschuwing toe te voegen wanneer [field: AllowNull] wordt gebruikt, waarbij wordt voorgesteld om ook MaybeNulltoe te voegen. Dit komt doordat AllowNull zelf niet doet wat gebruikers nodig hebben uit een null-variabele: er wordt ervan uitgegaan dat het veld in eerste instantie niet null is wanneer we nog nooit iets naar het veld hebben geschreven.
  • We kunnen ook overwegen om het gedrag van [field: MaybeNull] op het field trefwoord of zelfs velden in het algemeen aan te passen, zodat null-waarden ook naar de variabele kunnen worden geschreven, alsof AllowNull impliciet ook aanwezig waren.

Beantwoorde LDM-vragen

Syntaxislocaties voor trefwoorden

In toegangsfuncties waarbij field en value kunnen binden aan een gesynthetiseerd achterliggend veld of een impliciete setterparameter, in welke syntaxislocaties moeten de identificatoren als trefwoorden worden beschouwd?

  1. altijd
  2. primaire expressies alleen
  3. nooit

De eerste twee gevallen zijn ingrijpende veranderingen.

Als de identificatoren altijd als trefwoorden worden beschouwd, is dat bijvoorbeeld een ingrijpende verandering voor het volgende:

class MyClass
{
    private int field;
    public int P => this.field; // error: expected identifier

    private int value;
    public int Q
    {
        set { this.value = value; } // error: expected identifier
    }
}

Als de id's trefwoorden zijn wanneer deze worden gebruikt als primaire expressies alleen, is de wijziging die fouten veroorzaken kleiner. De meest voorkomende onderbreking kan ongekwalificeerd gebruik zijn van een bestaand lid met de naam field.

class MyClass
{
    private int field;
    public int P => field; // binds to synthesized backing field rather than 'this.field'
}

Er is ook een onderbreking wanneer field of value opnieuw wordt aangegeven in een geneste functie. Dit kan de enige onderbreking zijn voor value voor primaire expressies.

class MyClass
{
    private IEnumerable<string> _fields;
    public bool HasNotNullField
    {
        get => _fields.Any(field => field is { }); // 'field' binds to synthesized backing field
    }
    public IEnumerable<string> Fields
    {
        get { return _fields; }
        set { _fields = value.Where(value => Filter(value)); } // 'value' binds to setter parameter
    }
}

Als de id's worden nooit als trefwoorden beschouwd, binden de id's alleen aan een gesynthetiseerd backingveld of de impliciete parameter wanneer de id's niet aan andere leden binden. Er is geen belangrijke wijziging voor dit geval.

Antwoorden

field is een trefwoord in de juiste accessors wanneer deze wordt gebruikt als een primaire expressie alleen; value wordt nooit beschouwd als een trefwoord.

Scenario's die vergelijkbaar zijn met { set; }

{ set; } is momenteel niet toegestaan en dit is logisch: het veld dat hiermee wordt gemaakt, kan nooit worden gelezen. Er zijn nu nieuwe manieren om te eindigen in een situatie waarin de setter een back-upveld introduceert dat nooit wordt gelezen, zoals de uitbreiding van { set; } in { set => field = value; }.

Welke van deze scenario's mag worden gecompileerd? Stel dat de waarschuwing 'veld is nooit gelezen' van toepassing is, net zoals bij een handmatig gedeclareerd veld.

  1. { set; } - Niet toegestaan vandaag, blijf niet toestaan
  2. { set => field = value; }
  3. { get => unrelated; set => field = value; }
  4. { get => unrelated; set; }
  5. {
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    
  6. {
        get => unrelated;
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    

Antwoorden

Alleen verbieden wat vandaag al verboden is in automatische eigenschappen, de lichaamloze set;.

field in gebeurtenistoegangsor

Moet field een trefwoord zijn in een gebeurtenistoegangspunt en moet de compiler een back-upveld genereren?

class MyClass
{
    public event EventHandler E
    {
        add { field += value; }
        remove { field -= value; }
    }
}

aanbeveling: field wordt geen trefwoord in een gebeurtenistoegangsfunctie en er wordt geen back-upveld gegenereerd.

Antwoorden

Aanbeveling genomen. field is geen trefwoord binnen een gebeurtenistoegangsmethode en er wordt geen ondersteuningsveld gegenereerd.

Nullbaarheid van field

Moet de voorgestelde null-waarde van field worden geaccepteerd? Zie de sectie Nullability, en de open vraag daarin.

Antwoorden

Er wordt een algemeen voorstel aangenomen. Specifiek gedrag heeft nog steeds meer controle nodig.

field in initialisatiefunctie voor eigenschappen

Moet field een trefwoord zijn in een initializer voor eigenschap en verbinding maken met het ondersteunende veld?

class A
{
    const int field = -1;

    object P1 { get; } = field; // bind to const (ok) or backing field (error)?
}

Zijn er nuttige scenario's voor het verwijzen naar het backingveld in de initialisatiefunctie?

class B
{
    object P2 { get; } = (field = 2);        // error: initializer cannot reference instance member
    static object P3 { get; } = (field = 3); // ok, but useful?
}

In het bovenstaande voorbeeld moet de binding met het backingveld resulteren in een fout: 'initializer kan niet verwijzen naar een niet-statisch veld'.

Antwoorden

We verbinden de initialisatiefunctie zoals in eerdere versies van C#. We plaatsen het backingveld niet binnen het bereik, en we voorkomen ook dat er wordt verwezen naar andere leden met de naam field.

Interactie met gedeeltelijke eigenschappen

Initialisatoren

Wanneer een gedeeltelijke eigenschap fieldgebruikt, welke onderdelen moeten een initialisatiefunctie hebben?

partial class C
{
    public partial int Prop { get; set; } = 1;
    public partial int Prop { get => field; set => field = value; } = 2;
}
  • Het lijkt duidelijk dat er een fout optreedt wanneer beide onderdelen een initialisatiefunctie hebben.
  • We kunnen gebruiksvoorbeelden bedenken waarbij het definitie- of implementatieonderdeel mogelijk de initiële waarde van de fieldwil instellen.
  • Het lijkt erop dat als we de initializer bij de definitie toestaan, het de programmeur in feite dwingt om field te gebruiken, zodat het programma geldig is. Is dat prima?
  • We denken dat het gebruikelijk is voor generatoren om field te gebruiken wanneer een backingveld van hetzelfde type nodig is in de implementatie. Dit is deels omdat generatoren hun gebruikers vaak in staat willen stellen om [field: ...] doelkenmerken te gebruiken in het eigenschapsdefinitieonderdeel. Met behulp van het field trefwoord bespaart de generator-implementer de moeite van het doorsturen van dergelijke attributen naar een gegenereerd veld en worden de daaropvolgende waarschuwingen onderdrukt. Deze generatoren willen waarschijnlijk ook toestaan dat de gebruiker een initiële waarde voor het veld opgeeft.

aanbeveling: een initialisatiefunctie toestaan op een van beide delen van een gedeeltelijke eigenschap wanneer het implementatieonderdeel fieldgebruikt. Meld een fout als beide onderdelen een initialisatiefunctie hebben.

Antwoorden

Aanbeveling geaccepteerd. Het declareren of implementeren van eigenschapslocaties kan een initialisatiefunctie gebruiken, maar niet beide tegelijk.

Automatische toegangsfuncties

Volgens het oorspronkelijke ontwerp moet de implementatie van gedeeltelijke eigenschappen code bevatten voor alle accessors. Recente iteraties van de field trefwoordfunctie bevatten echter het begrip 'auto-accessors'. Moeten gedeeltelijke implementaties van eigenschappen dergelijke accessors kunnen gebruiken? Als ze uitsluitend worden gebruikt, is deze niet te onderscheiden van een definiërende verklaring.

partial class C
{
    public partial int Prop0 { get; set; }
    public partial int Prop0 { get => field; set => field = value; } // this is equivalent to the two "semi-auto" forms below.

    public partial int Prop1 { get; set; }
    public partial int Prop1 { get => field; set; } // is this a valid implementation part?

    public partial int Prop2 { get; set; }
    public partial int Prop2 { get; set => field = value; } // what about this? will there be disagreement about which is the "best" style?

    public partial int Prop3 { get; }
    public partial int Prop3 { get => field; } // it will only be valid to use at most 1 auto-accessor, when a second accessor is manually implemented.

Aanbeveling: Auto-accessors niet toestaan in gedeeltelijke eigenschapsimplementaties, omdat de beperkingen rond wanneer ze bruikbaar zouden zijn, verwarrender zijn dan het voordeel dat ze worden toegestaan.

Antwoorden

Ten minste één implementatietoegangsfunctie moet handmatig worden geïmplementeerd, maar de andere toegangsfunctie kan automatisch worden geïmplementeerd.

Alleen lezen-veld

Wanneer moet het gesynthetiseerde backingveld worden beschouwd als alleen-lezen?

struct S
{
    readonly object P0 { get => field; } = "";         // ok
    object P1          { get => field ??= ""; }        // ok
    readonly object P2 { get => field ??= ""; }        // error: 'field' is readonly
    readonly object P3 { get; set { _ = field; } }     // ok
    readonly object P4 { get; set { field = value; } } // error: 'field' is readonly
}

Wanneer het ondersteunend veld wordt beschouwd als alleen-lezen, wordt het veld dat in de metadata wordt opgenomen gemarkeerd initonlyen wordt er een fout gerapporteerd als field wordt gewijzigd behalve in een initializer of constructor.

Aanbeveling: het gesynthetiseerde rugsteunveld is alleen lezen wanneer het bevatte type een struct is en de eigenschap of het bevatte type wordt gedeclareerd readonly.

Antwoorden

Aanbeveling wordt geaccepteerd.

Leescontext en set

Moet een set accessor worden toegestaan in een readonly context voor een eigenschap die gebruikmaakt van field?

readonly struct S1
{
    readonly object _p1;
    object P1 { get => _p1; set { } }   // ok
    object P2 { get; set; }             // error: auto-prop in readonly struct must be readonly
    object P3 { get => field; set { } } // ok?
}

struct S2
{
    readonly object _p1;
    readonly object P1 { get => _p1; set { } }   // ok
    readonly object P2 { get; set; }             // error: auto-prop with set marked readonly
    readonly object P3 { get => field; set { } } // ok?
}

Antwoorden

Er kunnen scenario's zijn waarin u een set accessor implementeert op een readonly struct en deze doorgeeft of gooit. We zullen dit toestaan.

[Conditional] code

Moet het gesynthetiseerde veld worden gegenereerd wanneer field alleen wordt gebruikt in weggelaten aanroepen naar voorwaardelijke methoden?

Moet er bijvoorbeeld een ondersteunend veld worden gegenereerd voor het volgende in een geen-FOUTOPSPORING build?

class C
{
    object P
    {
        get
        {
            Debug.Assert(field is null);
            return null;
        }
    }
}

Ter referentie worden de velden voor primaire constructorparameters gegenereerd in vergelijkbare gevallen – zie sharplab.io.

Aanbeveling: het ondersteuningsveld wordt gegenereerd wanneer field alleen wordt gebruikt in weggelaten oproepen naar voorwaardelijke methoden .

Antwoorden

Conditional code kan invloed hebben op onvoorwaardelijke code, zoals Debug.Assert het veranderen van nullbaarheid. Het zou vreemd zijn als field geen vergelijkbare gevolgen had. Het is ook onwaarschijnlijk dat dit in de meeste code voorkomt, dus we kiezen voor de eenvoudige oplossing en accepteren de aanbeveling.

Interface-eigenschappen en automatische toegangsfuncties

Wordt een combinatie van handmatig en automatisch geïmplementeerde toegangsfuncties herkend voor een interface-eigenschap waarbij de automatisch geïmplementeerde functie verwijst naar een gesynthetiseerd achterliggend veld?

Voor een exemplaareigenschap wordt een fout gerapporteerd dat exemplaarvelden niet worden ondersteund.

interface I
{
           object P1 { get; set; }                           // ok: not an implementation
           object P2 { get => field; set { field = value; }} // error: instance field

           object P3 { get; set { } } // error: instance field
    static object P4 { get; set { } } // ok: equivalent to { get => field; set { } }
}

Aanbeveling: Auto-accessors worden herkend in interface eigenschappen en de auto-accessors verwijzen naar een gesynthetiseerd achtergrondveld. Voor een exemplaareigenschap wordt een fout gerapporteerd dat exemplaarvelden niet worden ondersteund.

Antwoorden

Standaardiseren rond het exemplaarveld zelf is de oorzaak van de fout, is consistent met gedeeltelijke eigenschappen in klassen en we vinden dat resultaat leuk. De aanbeveling wordt geaccepteerd.