Sdílet prostřednictvím


Automatické výchozí struktury

Poznámka

Tento článek je specifikace funkce. Specifikace slouží jako návrhový dokument pro funkci. Zahrnuje navrhované změny specifikace spolu s informacemi potřebnými při návrhu a vývoji funkce. Tyto články se publikují, dokud nebudou navrhované změny specifikace finalizovány a začleněny do aktuální specifikace ECMA.

Mezi specifikací funkce a dokončenou implementací může docházet k nějakým nesrovnalostem. Tyto rozdíly jsou zachyceny v poznámkách ze schůzky návrhu jazyka (LDM).

Další informace o procesu přijetí specifikací funkcí do jazyka C# najdete v článku o specifikacích .

https://github.com/dotnet/csharplang/issues/5737

Shrnutí

Tato funkce zajišťuje, že v konstruktorech struktury identifikujeme pole, která uživatel před vrácením hodnoty nebo před jejím použitím explicitně nepřiřadil, a inicializujeme je implicitně na default namísto vyvolání určitých chyb přiřazení.

Motivace

Tento návrh je předložen jako možné zmírnění problémů s použitelností nalezených v dotnet/csharplang#5552 a dotnet/csharplang#5635 a zároveň řeší #5563 (všechna pole musí mít jistě přiřazené hodnoty, ale field není v konstruktoru přístupný).


Od verze C# 1.0 musí konstruktory struktur jednoznačně přiřadit this, jako by šlo o parametr out.

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

To představuje problémy, když jsou ručně definovány settery u částečně automatických vlastností, protože kompilátor nemůže zacházet s přiřazením vlastnosti jako s ekvivalentem přiřazení backingového pole.

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'.
    {
    }
}

Předpokládáme, že zavedení podrobnějších omezení pro settery, jako je schéma, ve kterém setter nebere ref this, ale spíše vezme out field jako parametr, bude pro některé případy použití příliš specializovaný a neúplný.

Jedním ze základních rozporů, s nimiž se potýkáme, je, že když mají vlastnosti struktury ručně implementované nastavovače, uživatelé často musí opakovaně přiřazovat hodnoty nebo znovu provádět svou logiku.

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

Předchozí diskuze

Malá skupina se na tento problém podívala a považovala několik možných řešení:

  1. Požadovat, aby uživatelé přiřadili this = default, pokud mají poloautomatické vlastnosti s ručně implementovanými settery. Souhlasíme s tím, že toto je nesprávné řešení, protože přepíše hodnoty nastavené v inicializátorech polí.
  2. Implicitně inicializuje všechna zálohovaná pole automatických/poloautomatních vlastností.
    • Tím se vyřeší problém "polovičně automatických setterů vlastností" a explicitně deklarovaná pole postaví pod odlišná pravidla: "Neinizializujte moje pole implicitně, ale implicitně inicializujte moje auto-vlastnosti."
  3. Zajistěte způsob, jak přiřadit podkladové pole poloautomatické vlastnosti a zajistit, aby jej uživatelé přiřadili.
    • V porovnání s (2) to může být těžkopádné. Automatická vlastnost má být "automatická", a možná zahrnuje "automatickou" inicializaci pole. Mohlo by to vést k nejasnostem ohledně toho, kdy je podkladové pole přiřazováno přiřazením k vlastnosti a kdy je volána nastavovací metoda vlastnosti.

Také jsme obdrželi zpětnou vazbu od uživatelů, kteří například chtějí zahrnout několik inicializátorů polí do struktur, aniž by bylo nutné explicitně přiřazovat vše. Tento problém můžeme vyřešit stejně jako problém s částečně automatickou vlastností s ručně implementovaným setterem, a to současně.

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

Úprava určitého přiřazení

Místo provádění analýzy definitivního přiřazení k oznámení chyb pro nepřiřazená pole v thisto děláme k určení , která pole musí být implicitně inicializována. Tato inicializace je vložena na začátek konstruktoru.

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

V příkladech (4) a (5) výsledná funkce codegen někdy obsahuje "dvojitá přiřazení" polí. To je obecně v pořádku, ale pro uživatele, kteří se obávají takových dvojitých přiřazení, můžeme zobrazit to, co bylo dříve diagnostikováno jako chyby přiřazení, jako výchozí je vypnuto diagnostiky upozornění.

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

Uživatelé, kteří nastaví závažnost této diagnostiky na chybu, si zvolí chování předcházející verzi C# 11. Tito uživatelé jsou v podstatě vyloučeni z používání poloautomatických vlastností s ručně implementovanými settery.

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

Na první pohled to vypadá jako "díra" ve funkci, ale je to vlastně správné řešení. Když povolíte diagnostiku, uživatel nám říká, že nechce, aby kompilátor implicitně inicializoval pole v konstruktoru. Neexistuje způsob, jak se zde vyhnout implicitní inicializaci, takže řešením pro ně je použít jiný způsob inicializace pole než ručně implementované setter, například ruční deklarování pole a jeho přiřazení, nebo zahrnutím inicializátoru pole.

V současné době JIT neodstraňuje zbytečné zápisy prostřednictvím "refs", což znamená, že tyto implicitní inicializace mají skutečné náklady. Ale to by mohlo být opravitelné. https://github.com/dotnet/runtime/issues/13727

Stojí za zmínku, že inicializace jednotlivých polí místo celé instance je opravdu jen optimalizace. Kompilátor by měl mít možnost libovolně implementovat jakoukoli heuristiku, pokud splní invariant, že pole, která nejsou jednoznačně přiřazena ve všech návratových bodech nebo před jakýmkoli přístupem k ne-polnímu členu this, jsou implicitně inicializována.

Pokud má například struktura 100 polí a pouze jeden z nich je explicitně inicializován, může mít větší smysl udělat initobj na celou věc, než implicitně generovat initobj pro 99 dalších polí. Implementace, která implicitně generuje initobj pro 99 dalších polí, by však stále byla platná.

Změny specifikace jazyka

Upravíme následující část standardu:

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

Pokud deklarace konstruktoru nemá žádný inicializátor konstruktoru, this proměnná se chová přesně stejně jako out parametr typu struktury. Konkrétně to znamená, že proměnná musí být rozhodně přiřazena v každé cestě provádění konstruktoru instance.

Tento jazyk upravíme tak, aby četl:

Pokud deklarace konstruktoru nemá žádný inicializátor konstruktoru, this proměnná se chová podobně jako out parametr typu struktury, s tím rozdílem, že není chybou, pokud nejsou splněny určité požadavky přiřazení (§9.4.1). Místo toho zavádíme následující chování:

  1. Pokud samotná proměnná this nesplňuje požadavky, všechny nepřiřazené proměnné instance v rámci this ve všech bodech, kde jsou požadavky porušeny, jsou implicitně inicializovány na výchozí hodnotu (§9.3) ve fázi inicializace před spuštěním jakéhokoli jiného kódu v konstruktoru.
  2. Pokud proměnná instance v v rámci this nesplňuje požadavky nebo žádná proměnná instance na jakékoli úrovni vnoření v rámci v nesplňuje požadavky, v se implicitně inicializuje na výchozí hodnotu ve fázi inicializace před spuštěním jakéhokoli jiného kódu v konstruktoru.

Designérské schůzky

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