Delen via


Structs met automatische standaardwaarden

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 (Language Design Meeting) notities .

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/5737

Samenvatting

Met deze functie kunnen we in structuurconstructors de velden identificeren die niet expliciet door de gebruiker zijn toegewezen voordat ze worden geretourneerd of vóór gebruik, en impliciet met default initialiseren in plaats van definitieve toewijzingsfouten te geven.

Motivatie

Dit voorstel wordt voorgesteld als mogelijke oplossing voor bruikbaarheidsproblemen in dotnet/csharplang#5552 en dotnet/csharplang#5635, evenals het aanpakken van #5563 (alle velden moeten zeker worden toegewezen, maar field niet toegankelijk is binnen de constructor).


Sinds C# 1.0 zijn struct-constructors vereist om definitief this toe te wijzen alsof het een out parameter is.

public struct S
{
    public int x, y;
    public S() // error: Fields 'S.x' and 'S.y' must be fully assigned before control is returned to the caller
    {
    }
}

Dit veroorzaakt problemen wanneer setters handmatig worden gedefinieerd voor semi-automatische eigenschappen, omdat de compiler de toewijzing van de eigenschap niet kan behandelen als equivalent aan de toewijzing van het onderliggende veld.

public struct S
{
    public int X { get => field; set => field = value; }
    public S() // error: struct fields aren't fully assigned. But caller can only assign 'this.field' by assigning 'this'.
    {
    }
}

We gaan ervan uit dat het introduceren van fijnmazige beperkingen voor setters, zoals een schema waarbij de setter niet ref this neemt, maar in plaats daarvan out field als parameter, te niche en onvolledig zal zijn voor sommige gebruiksscenario's.

Een fundamentele spanning waarmee we moeite hebben, is dat wanneer setters handmatig zijn geïmplementeerd in structeigenschappen, gebruikers vaak een vorm van herhaling moeten uitvoeren, zoals herhaaldelijk toewijzen of het herhalen van hun logica.

struct S
{
    private int _x;
    public int X
    {
        get => _x;
        set => _x = value >= 0 ? value : throw new ArgumentOutOfRangeException();
    }

    // Solution 1: assign some value in the constructor before "really" assigning through the property setter.
    public S(int x)
    {
        _x = default;
        X = x;
    }

    // Solution 2: assign the field once in the constructor, repeating the implementation of the setter.
    public S(int x)
    {
        _x = x >= 0 ? x : throw new ArgumentOutOfRangeException();
    }
}

Vorige discussie

Een kleine groep heeft dit probleem bekeken en een aantal mogelijke oplossingen overwogen:

  1. Zorg ervoor dat gebruikers this = default toewijzen wanneer de setters van semi-auto-eigenschappen handmatig zijn geïmplementeerd. We zijn het erover eens dat dit de verkeerde oplossing is, omdat de waarden die zijn ingesteld in veld initialisaties, worden weggeblazen.
  2. Initialiseer impliciet alle back-upvelden van auto-/semi-auto-eigenschappen.
    • Dit lost het probleem van 'semi-auto-eigenschapsinstellers' op en plaatst expliciet gedeclareerde velden onder andere regels: 'initialiseer mijn velden niet impliciet, maar initialiseer mijn automatisch gegenereerde eigenschappen wel impliciet'.
  3. Geef een manier op om het backingveld van een semi-auto-eigenschap toe te wijzen en gebruikers te verplichten deze toe te wijzen.
    • Dit kan lastig zijn in vergelijking met (2). Een auto property moet 'automatisch' zijn en omvat wellicht ook 'automatische' initialisatie van het bijbehorende veld. Het kan verwarring veroorzaken wanneer het onderliggende veld wordt toegewezen door een toewijzing aan de eigenschap en wanneer de eigenschapssetter wordt aangeroepen.

We hebben ook feedback ontvangen van gebruikers die bijvoorbeeld enkele veld-initialisators willen opnemen in structs zonder dat men expliciet alles hoeft toe te wijzen. We kunnen dit probleem en het probleem 'semi-autoeigenschap met handmatig geïmplementeerde setter' op hetzelfde moment oplossen.

struct MagnitudeVector3d
{
    double X, Y, Z;
    double Magnitude = 1;
    public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
    {
    }
}

Definitieve toewijzing aanpassen

In plaats van een definitieve toewijzingsanalyse uit te voeren om fouten te geven voor niet-toegewezen velden op this, doen we dit om te bepalen welke velden impliciet moeten worden geïnitialiseerd. Een dergelijke initialisatie wordt ingevoegd aan het begin van de constructor.

struct S
{
    int x, y;

    // Example 1
    public S()
    {
        // ok. Compiler inserts an assignment of `this = default`.
    }

    // Example 2
    public S()
    {
        // ok. Compiler inserts an assignment of `y = default`.
        x = 1;
    }

    // Example 3
    public S()
    {
        // valid since C# 1.0. Compiler inserts no implicit assignments.
        x = 1;
        y = 2;
    }

    // Example 4
    public S(bool b)
    {
        // ok. Compiler inserts assignment of `this = default`.
        if (b)
            x = 1;
        else
            y = 2;
    }

    // Example 5
    void M() { }
    public S(bool b)
    {
        // ok. Compiler inserts assignment of `y = default`.
        x = 1;
        if (b)
            M();

        y = 2;
    }
}

In voorbeelden (4) en (5) heeft het resulterende codegen soms dubbele toewijzingen van velden. Dit is over het algemeen prima, maar voor gebruikers die zich zorgen maken over dergelijke dubbele toewijzingen, kunnen we wat vroeger definitieve toewijzingsfoutdiagnoses waren, uitsturen als standaard uitgeschakelde waarschuwingsdiagnoses.

struct S
{
    int x;
    public S() // warning: 'S.x' is implicitly initialized to 'default'.
    {
    }
}

Gebruikers die de ernst van deze diagnose instellen op 'fout', kiezen voor het gedrag van pre-C# 11. Dergelijke gebruikers zijn in wezen uitgesloten van semi-automatische eigenschappen met handmatig geïmplementeerde setters.

struct S
{
    public int X
    {
        get => field;
        set => field = field < value ? value : field;
    }

    public S() // error: backing field of 'S.X' is implicitly initialized to 'default'.
    {
        X = 1;
    }
}

Op het eerste gezicht voelt dit als een "gat" in de functie, maar het is eigenlijk de juiste keuze. Door de diagnose in te schakelen, vertelt de gebruiker ons dat ze niet willen dat de compiler impliciet hun velden in de constructor initialiseert. Er is geen manier om de impliciete initialisatie hier te voorkomen, dus de oplossing hiervoor is het gebruik van een andere manier om het veld te initialiseren dan een handmatig geïmplementeerde setter, zoals het handmatig declareren van het veld en het toewijzen ervan, of door een veld initialisatiefunctie op te geven.

Op dit moment elimineert de JIT geen dode winkels via refs, wat betekent dat deze impliciete initialisaties wel echte kosten hebben. Maar dat kan wel worden opgelost. https://github.com/dotnet/runtime/issues/13727

Het is de moeite waard om te vermelden dat het initialiseren van afzonderlijke velden in plaats van het hele exemplaar echt een optimalisatie is. De compiler moet waarschijnlijk vrij zijn om elke heuristiek te implementeren die het wenst, zolang aan de invariant wordt voldaan dat velden die niet zeker zijn toegewezen bij alle retourpunten of voor elke niet-veldlidtoegang van this, impliciet worden geïnitialiseerd.

Als een struct bijvoorbeeld 100 velden bevat en slechts één van deze velden expliciet is geïnitialiseerd, kan het zinvoler zijn om een initobj uit te voeren op het hele item, dan impliciet initobj te verzenden voor de 99 andere velden. Een implementatie die impliciet initobj verzendt voor de 99 andere velden, is echter nog steeds geldig.

Wijzigingen in taalspecificatie

We passen de volgende sectie van de standaard aan:

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12814-this-access

Als de constructordeclaratie geen initialisatiefunctie voor constructor heeft, gedraagt de this variabele zich precies hetzelfde als een out parameter van het structtype. Dit betekent met name dat de variabele zeker in elk uitvoeringspad van de instantieconstructor moet worden toegewezen.

We passen deze taal aan om te lezen:

Als de constructordeclaratie geen initialisatiefunctie voor de constructor heeft, gedraagt de this variabele zich op dezelfde manier als een out parameter van het structtype, behalve dat er geen fout is wanneer aan de definitieve toewijzingsvereisten (§9.4.1) niet wordt voldaan. In plaats daarvan introduceren we het volgende gedrag:

  1. Wanneer de this variabele zelf niet aan de vereisten voldoet, worden alle niet-toegewezen instantievariabelen binnen this op alle punten waar aan vereisten niet wordt voldaan impliciet geïnitialiseerd tot de standaardwaarde (§9.3) in een initialisatiefase voordat alle andere code in de constructor wordt uitgevoerd.
  2. Wanneer een exemplaarvariabele v binnen this niet voldoet aan de vereisten of een instantievariabele op een willekeurig niveau van nesten binnen v voldoet niet aan de vereisten, wordt v impliciet geïnitialiseerd naar de standaardwaarde in een initialisatiefase voordat andere code in de constructor wordt uitgevoerd.

Ontwerpvergaderingen

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-14.md#definite-assignment-in-structs