Megosztás a következőn keresztül:


Automatikus alapértelmezett szerkezetek

Jegyzet

Ez a cikk egy funkcióspecifikáció. A specifikáció a funkció tervezési dokumentumaként szolgál. Tartalmazza a specifikáció javasolt módosításait, valamint a funkció tervezése és fejlesztése során szükséges információkat. Ezeket a cikkeket mindaddig közzéteszik, amíg a javasolt specifikációmódosításokat nem véglegesítik, és be nem építik a jelenlegi ECMA-specifikációba.

A szolgáltatás specifikációja és a befejezett implementáció között eltérések lehetnek. Ezeket a különbségeket a vonatkozó nyelvi tervezési értekezlet (LDM) megjegyzései rögzítik.

A funkcióspektusok C# nyelvi szabványba való bevezetésének folyamatáról a specifikációkcímű cikkben olvashat bővebben.

Bajnoki probléma: https://github.com/dotnet/csharplang/issues/5737

Összefoglalás

Ez a funkció lehetővé teszi, hogy a szerkezetkonstruktorokban azonosítsuk azokat a mezőket, amelyeket a felhasználó nem osztott ki explicit módon a visszatérés vagy használat előtt, és implicit módon inicializáljuk őket a default ahelyett, hogy határozott hozzárendelési hibákat adnánk meg.

Motiváció

Ez a javaslat a dotnet/csharplang#5552 és a dotnet/csharplang#5635 használhatósági problémáinak lehetséges enyhítése, valamint a #5563 címke kezelése érdekében lett felvetve (minden mezőnek egyértelműen hozzá kell lennie rendelve, de a field nem elérhető a konstruktoron belül).


A C# 1.0 óta a szerkezetkonstruktoroknak egyértelműen this kell hozzárendelniük, mintha out paraméter lenne.

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

Ez problémákat okozhat, ha a beállítók manuálisan vannak definiálva félautomata tulajdonságokon, mivel a fordító nem tudja a tulajdonság hozzárendelését a háttérmező hozzárendelésével egyenértékűként kezelni.

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

Feltételezzük, hogy a beállítókra vonatkozó részletesebb korlátozások bevezetése, például egy olyan séma, amelyben a beállító nem veszi ref this, hanem inkább paraméterként veszi out field, bizonyos használati esetekben túl szűk és hiányos lesz.

Az egyik alapvető feszültség, amellyel küzdünk, hogy amikor a strukturális tulajdonságokhoz manuálisan implementálnak settereket, a felhasználóknak gyakran valamilyen "ismétlődést" kell végezniük, vagy újra kell hozzárendelniük, vagy megismételniük a logikájukat.

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

Előző vitafórum

Egy kis csoport megvizsgálta ezt a problémát, és megfontolt néhány lehetséges megoldást:

  1. Megköveteli a felhasználóktól, hogy this = default-t rendeljenek hozzá, ha a félig automatikus tulajdonságok esetén manuálisan implementált beállítókat használnak. Egyetértünk abban, hogy ez nem a megfelelő megoldás, mivel elfújja a mező inicializálóiban beállított értékeket.
  2. Implicit módon inicializálja az automatikus/félig automatikus tulajdonságok összes háttérmezőjét.
    • Ez megoldja a "félig automatikus tulajdonság beállítók" problémát, és a kifejezetten deklarált mezőket más szabályok alá helyezi: "ne inicializálja implicit módon a mezőket, azonban implicit módon inicializálja az automatikus tulajdonságokat."
  3. Adjon meg módot egy félig automatikus tulajdonság háttérmezőjének hozzárendelésére, és megkövetelheti a felhasználóktól a hozzárendelést.
    • Ez nehézkes lehet a (2) ponthoz képest. Az automatikus tulajdonságnak "automatikusnak" kell lennie, és lehet, hogy magában foglalja a mező "automatikus" inicializálását. Zavart okozhat, hogy mikor rendeli hozzá a mögöttes mezőt egy hozzárendelés a tulajdonsághoz, és mikor hívja meg a tulajdonság-beállítót.

visszajelzést is kaptunk felhasználóktól, akik például néhány mező inicializálót szeretnének belefoglalni a szerkezetekbe anélkül, hogy mindent explicit módon kellene hozzárendelni. Ezt a problémát, valamint a "félig automatikus tulajdonság manuálisan implementált beállítóval" problémát is meg tudjuk oldani egyszerre.

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

Határozott hozzárendelés módosítása

Ahelyett, hogy a nem hozzárendelt mezők hibáinak meghatározása érdekében határozott hozzárendelés-elemzést végeznénk this, azt végezzük el, hogy meghatározzuk, mely mezőket kell implicit módon inicializálni. Az ilyen inicializálás a konstruktor elején történik.

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

A (4) és (5) példákban az eredményül kapott kódgenerálás néha a mezők duplikált hozzárendelésével jár. Ez általában rendben van, de azoknak a felhasználóknak, akik aggódnak az ilyen kettős hozzárendelések miatt, kibocsáthatunk egy korábban határozott hozzárendelési hibaként ismert diagnosztikasorozatot, amelyet alapértelmezés szerint letiltott figyelmeztető diagnosztikává formáltunk.

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

Azok a felhasználók, akik a diagnosztikát "hiba" értékre állítják be, a C# 11 előtti viselkedést fogják választani. Az ilyen felhasználók lényegében nem férhetnek hozzá a félig automatizált tulajdonságokhoz, ahol a beállítók manuálisan lettek implementálva.

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

Első pillantásra ez olyan, mint egy "lyuk" a funkción, de ez valójában ez a helyes megoldás. A diagnosztika engedélyezésével a felhasználó azt mondja nekünk, hogy nem szeretné, hogy a fordító implicit módon inicializálja a mezőket a konstruktorban. Itt nem lehet elkerülni az implicit inicializálást, ezért a megoldás számukra az, hogy más módon inicializálják a mezőt, mint egy manuálisan implementált beállító, például a mező manuális deklarálása és hozzárendelése, vagy egy mező inicializálása.

A JIT jelenleg nem szünteti meg a holt tárolókat a ref-eken keresztül, ami azt jelenti, hogy ezek az implicit inicializálások valós költségekkel járnak. De ez javítható. https://github.com/dotnet/runtime/issues/13727

Érdemes megjegyezni, hogy az egyes mezők inicializálása a teljes példány helyett valójában csak optimalizálás. A fordítónak valószínűleg szabadon alkalmazhat bármilyen heurisztikát, amennyiben megfelel annak az invariánsnak, hogy azokat a mezőket, amelyek nincsenek egyértelműen hozzárendelve egyik visszatérési pontnál sem vagy mielőtt a this bármely nem mezőtaghoz való hozzáférés történne, implicit módon inicializálni kell.

Ha például egy struktúra 100 mezővel rendelkezik, és csak az egyiket inicializálja explicit módon, logikusabb lehet az egészre initobj-t alkalmazni, mint hogy a másik 99 mezőhöz initobj-et implicit módon kibocsássunk. A 99 további mezőhöz implicit módon initobj kibocsátó implementáció azonban továbbra is érvényes lenne.

A nyelvi specifikáció módosítása

A szabvány következő szakaszát módosítjuk:

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

Ha a konstruktor deklarációja nem rendelkezik konstruktor inicializálóval, a this változó pontosan ugyanúgy viselkedik, mint a szerkezettípus out paramétere. Ez különösen azt jelenti, hogy a változót mindenképpen hozzá kell rendelni a példánykonstruktor minden végrehajtási útvonalához.

Ezt a nyelvet a következőképpen módosítjuk:

Ha a konstruktor deklarációja nem rendelkezik konstruktor-inicializálóval, a this változó a szerkezettípus out paraméteréhez hasonlóan viselkedik, azzal a kivételével, hogy nem hiba, ha a határozott hozzárendelési követelmények (§9.4.1) nem teljesülnek. Ehelyett a következő viselkedéseket vezetjük be:

  1. Ha maga a this változó nem felel meg a követelményeknek, akkor a rendszer implicit módon inicializálja az this fázisban lévő összes nem hozzárendelt példányváltozót az minden olyan pontján, ahol a követelmények sérülnek, implicit módon inicializálódik az alapértelmezett értékre (§9.3) egy this fázisban, mielőtt a konstruktor bármely más kódja lefut.
  2. Ha egy példányváltozó vthis nem felel meg a követelményeknek, vagy a v bármely beágyazási szintjén lévő példányváltozó nem felel meg a követelményeknek, akkor v implicit módon inicializálódik egy inicializálási fázis alapértelmezett értékére, mielőtt a konstruktor bármely más kódja lefut.

Tervezői értekezletek

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