Sdílet prostřednictvím


Primární konstruktory

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 příslušných 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 .

Shrnutí

Třídy a struktury mohou mít seznam parametrů a jejich specifikace základní třídy může mít seznam argumentů. Parametry primárního konstruktoru jsou v dosahu v rámci deklarace třídy nebo struktury a pokud je zachytí člen funkce nebo anonymní funkce, jsou vhodně uloženy (např. jako nepojmenovatelná soukromá pole deklarované třídy nebo struktury).

Návrh reinterpretuje primární konstruktory již k dispozici u záznamů v rámci této obecnější funkce s několika dalšími členy, které byly syntetizovány.

Motivace

Schopnost třídy nebo struktury v jazyce C# mít více než jeden konstruktor poskytuje univerzálnost, ale na úkor určité složitosti v syntaxi deklarace, protože vstup konstruktoru a stav třídy musí být čistě odděleny.

Primární konstruktory zpřístupňují parametry jednoho konstruktoru pro celou třídu nebo strukturu, aby mohly být použity k inicializaci nebo přímo jako stav objektu. Kompromisem je, že všechny ostatní konstruktory musí volat prostřednictvím primárního konstruktoru.

public class B(bool b) { } // base class

public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(S));
    }
    public C(string s) : this(true, 0, s) { } // must call this(...)
}

Podrobný návrh

Tento článek popisuje generalizovaný návrh napříč záznamy a záznamy, které nejsou záznamy, a poté podrobně popisuje, jak jsou stávající primární konstruktory pro záznamy určeny přidáním sady syntetizovaných členů v přítomnosti primárního konstruktoru.

Syntaxe

Deklarace tříd a struktur jsou rozšířeny tak, aby umožňovaly seznam parametrů pro název typu, seznam argumentů základní třídy a tělo sestávající pouze z ;:

class_declaration
  : attributes? class_modifier* 'partial'? class_designator identifier type_parameter_list?
  parameter_list? class_base? type_parameter_constraints_clause* class_body
  ;
  
class_designator
  : 'record' 'class'?
  | 'class'
  
class_base
  : ':' class_type argument_list?
  | ':' interface_type_list
  | ':' class_type  argument_list? ',' interface_type_list
  ;  
  
class_body
  : '{' class_member_declaration* '}' ';'?
  | ';'
  ;
  
struct_declaration
  : attributes? struct_modifier* 'partial'? 'record'? 'struct' identifier type_parameter_list?
    parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body
  ;

struct_body
  : '{' struct_member_declaration* '}' ';'?
  | ';'
  ;
  
interface_declaration
  : attributes? interface_modifier* 'partial'? 'interface'
    identifier variant_type_parameter_list? interface_base?
    type_parameter_constraints_clause* interface_body
  ;  
    
interface_body
  : '{' interface_member_declaration* '}' ';'?
  | ';'
  ;

enum_declaration
  : attributes? enum_modifier* 'enum' identifier enum_base? enum_body
  ;

enum_body
  : '{' enum_member_declarations? '}' ';'?
  | '{' enum_member_declarations ',' '}' ';'?
  | ';'
  ;

Poznámka: Tyto produkce nahrazují record_declaration v Records a record_struct_declaration ve záznamových strukturách , které jsou zastaralé.

Je chybou, jestliže class_baseargument_list a obalující class_declaration neobsahuje parameter_list. Nejméně jedna částečná deklarace typu částečné třídy nebo struktury může poskytnout parameter_list. Parametry v parameter_list deklarace record musí být všechny parametry hodnot.

Podle tohoto návrhu mohou class_body, struct_body, interface_body a enum_body se skládat pouze z ;.

Třída nebo struktura s parameter_list má implicitní veřejný konstruktor, jehož podpis odpovídá parametrům hodnoty deklarace typu. Toto se nazývá primární konstruktor pro typ a způsobí potlačení implicitně deklarovaného konstruktoru bez parametrů, pokud existuje. Je chybou mít primární konstruktor a konstruktor se stejným podpisem, který již existuje v deklaraci typu.

Vyhledat

Vyhledávání jednoduchých názvů je rozšířeno pro zpracování parametrů primárního konstruktoru. Změny jsou zvýrazněné tučným písmem v následujícím výňatku:

  • Jinak platí, že pro každý typ instance T (§15.3.2), počínaje typem instance bezprostředně uzavřené deklarace typu a pokračováním s typem instance každé nadřazené třídy nebo deklarace struktury (pokud existuje):
    • Pokud deklarace T obsahuje primární parametr konstruktoru I a odkaz se vyskytne v argument_list u class_baseTnebo v inicializátoru pole, vlastnosti či události T, potom je výsledkem primární parametr konstruktoru I
    • Jinak pokud je e nula a deklarace T obsahuje parametr typu s názvem I, pak simple_name odkazuje na tento parametr typu.
    • V opačném případě, pokud vyhledávání členů (§12.5) I v T s argumenty typu e vytvoří shodu:
      • Pokud T je typ instance bezprostředně ohraničující typ třídy nebo struktury a vyhledávání identifikuje jednu nebo více metod, je výsledkem skupina metod s přidruženým výrazem instance this. Pokud byl zadán seznam argumentů typu, používá se při volání obecné metody (§12.8.10.2).
      • Jinak pokud je T typem instance bezprostředně ohraničujícího typu třídy nebo struktury, pokud vyhledávání identifikuje člena instance a pokud se odkaz vyskytuje v rámci bloku konstruktoru instance, metody instance nebo objektu instance (§12.2.1), výsledek je stejný jako přístup člena (§12.8.7) formuláře this.I. K tomu může dojít pouze v případě, že e je nula.
      • V opačném případě je výsledek stejný jako přístup člena (§12.8.7) formuláře T.I nebo T.I<A₁, ..., Aₑ>.
    • Jinak pokud deklarace T obsahuje parametr primárního konstruktoru I, výsledek je primární konstruktor parametr I.

První sčítání odpovídá změně vzniklé primární konstruktory záznamůa zajišťuje, aby byly nalezeny parametry primárního konstruktoru před všemi odpovídajícími poli v rámci inicializátorů a argumentů základní třídy. Toto pravidlo rozšiřuje také na statické inicializátory. Vzhledem k tomu, že záznamy mají vždy člena instance se stejným názvem jako parametr, může rozšíření vést pouze ke změně v chybové zprávě. Neplatný přístup k parametru vs. neplatný přístup k členu instance.

Druhý doplněk umožňuje nalezení parametrů primárního konstruktoru jinde v těle typu, ale pouze v případě, že nejsou stínovány členy.

Jedná se o chybu odkazování na parametr primárního konstruktoru, pokud k odkazu nedojde v některé z následujících možností:

  • argument nameof
  • inicializátor pole instance, vlastnosti nebo události deklarujícího typu (typ deklarující primární konstruktor s parametrem).
  • argument_list class_base deklarujícího typu.
  • tělo metody instance (všimněte si, že konstruktory instance jsou vyloučeny) deklarujícího typu.
  • tělo přístupového prvku instance deklarujícího typu.

Jinými slovy, parametry primárního konstruktoru jsou viditelné během celého těla deklarujícího typu. Stínují členy deklarujícího typu v inicializátoru pole, vlastnosti nebo události deklarujícího typu nebo v rámci argument_listclass_base deklarujícího typu. Jsou překrývány ostatními členy deklarujícího typu všude jinde.

Proto v následující deklaraci:

class C(int i)
{
    protected int i = i; // references parameter
    public int I => i; // references field
}

Inicializátor pole i odkazuje na parametr i, zatímco text vlastnosti I odkazuje na pole i.

Upozornit na stínování od člena ze základny

Kompilátor vygeneruje upozornění na použití identifikátoru, když základní člen stínuje parametr primárního konstruktoru, pokud tento parametr primárního konstruktoru nebyl předán do základního typu prostřednictvím jeho konstruktoru.

Parametr primárního konstruktoru se považuje za předaný základnímu typu prostřednictvím jeho konstruktoru, pokud jsou pro argument v class_basesplněny všechny následující podmínky:

  • Argument představuje implicitní nebo explicitní převod identity parametru primárního konstruktoru.
  • Argument není součástí rozšířeného argumentu params;

Sémantika

Primární konstruktor vede k vytvoření konstruktoru instance u obklopujícího typu s danými parametry. Pokud má class_base seznam argumentů, vygenerovaný konstruktor instance bude mít base inicializátor se stejným seznamem argumentů.

Parametry primárního konstruktoru v deklarací třídy/struktury lze deklarovat ref, in nebo out. Deklarace parametrů ref nebo out zůstává v primárních konstruktorech deklarace záznamu neplatná.

Všechny inicializátory členů instance v těle třídy se stanou přiřazeními v generovaném konstruktoru.

Pokud se na parametr primárního konstruktoru odkazuje v rámci člena instance a odkaz není uvnitř argumentu nameof, zachytí se do stavu ohraničujícího typu, takže zůstane přístupný po ukončení konstruktoru. Pravděpodobná strategie implementace je prostřednictvím soukromého pole s použitím zakódovaného názvu. Ve struktuře jen pro čtení budou pole pro zachytávání jen pro čtení. Přístup k zachyceným parametrům struktury jen pro čtení proto bude mít podobná omezení jako přístup k polím jen pro čtení. Přístup k zachyceným parametrům v rámci člena jen pro čtení bude mít podobná omezení jako přístup k polím instance ve stejném kontextu.

Zachytávání není povoleno pro parametry, které mají typ podobný odkazu, a zachytávání není povoleno pro ref, in nebo out parametry. Podobá se omezení pro zachycení v lambdách.

Pokud se na parametr primárního konstruktoru odkazuje pouze z inicializátorů člena instance, mohou tyto parametry přímo odkazovat na parametr vygenerovaného konstruktoru, protože jsou prováděny jako součást.

Primární konstruktor provede následující posloupnost operací:

  1. Hodnoty parametrů jsou uloženy v polích zachycení, pokud existují.
  2. Spouští se inicializátory instancí.
  3. Inicializátor základního konstruktoru je volán.

Odkazy na parametry v libovolném uživatelském kódu se nahradí odpovídajícími odkazy na pole zachycení.

Například tato deklarace:

public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s) : this(true, 0, s) { } // must call this(...)
}

Vygeneruje kód podobný následujícímu:

public class C : B
{
    public int I { get; set; }
    public string S
    {
        get => __s;
        set => __s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s) : this(0, s) { ... } // must call this(...)
    
    // generated members
    private string __s; // for capture of s
    public C(bool b, int i, string s)
    {
        __s = s; // capture s
        I = i; // run I's initializer
        B(b) // run B's constructor
    }
}

Jedná se o chybu, že deklarace konstruktoru, která není primárním konstruktorem, má stejný seznam parametrů jako primární konstruktor. Všechny nehlavní deklarace konstruktoru musí používat inicializátor this tak, aby byl nakonec volán hlavní konstruktor.

Záznamy generují upozornění, pokud primární parametr konstruktoru není přečtený v rámci inicializátorů instancí (pravděpodobně vygenerovaných) nebo inicializátoru base. Podobná upozornění budou hlášena pro parametry primárního konstruktoru ve třídách a strukturách:

  • Pro parametr předaný hodnotou, není-li parametr zachycen a není-li čten v rámci žádného inicializátoru instance nebo inicializátoru základního.
  • pro parametr in, pokud parametr není přečten v rámci inicializátorů instancí nebo základního inicializátoru.
  • pro parametr ref, pokud parametr není přečtený nebo zapsáný v rámci inicializátorů instance nebo základních inicializátorů.

Identické jednoduché názvy a názvy typů

Existuje speciální pravidlo jazyka pro scénáře, které se často označují jako scénáře "Color Color" – Identické jednoduché názvy a názvy typů.

V přístupu člena formuláře E.I, pokud E je jeden identifikátor, a pokud význam E jako simple_name (§12.8.4) je konstanta, pole, vlastnost, místní proměnná nebo parametr se stejným typem jako E jako type_name (§7.8.1), pak jsou povoleny oba možné významy E. Vyhledávání členů E.I není nikdy nejednoznačné, protože I musí být nutně členem typu E v obou případech. Jinými slovy pravidlo jednoduše povoluje přístup ke statickým členům a vnořeným typům E, kdy by jinak došlo k chybě kompilace.

Pokud jde o primární konstruktory, pravidlo ovlivňuje, zda má být identifikátor v rámci členu instance považován za odkaz na typ, nebo jako odkaz na parametr primární konstruktoru, který následně zachycuje parametr do stavu ohraničujícího typu. I když "vyhledávání členů E.I není nikdy nejednoznačné", když vyhledávání vrátí skupinu členů, v některých případech není možné určit, zda přístup člena odkazuje na statického člena nebo na člena instance bez kompletního vyhodnocení (zavázání) přístupu člena. Zachycení primárního parametru konstruktoru současně mění vlastnosti ohraničujícího typu způsobem, který ovlivňuje sémantickou analýzu. Například typ může být nespravovaný a kvůli tomu může selhat určitá omezení. Existují i scénáře, pro které může být vazba úspěšná, a to v závislosti na tom, jestli je parametr považován za zachycený nebo ne. Například:

struct S1(Color Color)
{
    public void Test()
    {
        Color.M1(this); // Error: ambiguity between parameter and typename
    }
}

class Color
{
    public void M1<T>(T x, int y = 0)
    {
        System.Console.WriteLine("instance");
    }
    
    public static void M1<T>(T x) where T : unmanaged
    {
        System.Console.WriteLine("static");
    }
}

Pokud zacházíme s příjemcem Color jako s hodnotou, zachytáváme parametr a spravujeme S1. Statická metoda se pak stane nepoužitelnou z důvodu omezení a zavoláme metodu instance. Pokud ale s příjemcem zacházíme jako s typem, nezachytáváme parametr a S1 zůstává nespravovaný, platí obě metody, ale statická metoda je "lepší", protože nemá volitelný parametr. Žádná volba vede k chybě, ale každá z nich by způsobovala odlišné chování.

V takovém případě kompilátor vygeneruje chybu nejednoznačnosti pro přístup k členovi E.I, pokud jsou splněny všechny následující podmínky:

  • Vyhledávání členů E.I přináší současně skupinu členů obsahující instance a statické členy. Rozšiřující metody použitelné pro typ příjemce jsou považovány za metody instance pro účely této kontroly.
  • Pokud se E považuje za jednoduchý název, nikoli jako název typu, odkazuje na primární parametr konstruktoru a zachytá parametr do stavu ohraničujícího typu.

Varování o dvojnásobném úložišti

Pokud je primární parametr konstruktoru předán základu a také zachycen, existuje vysoké riziko, že je neúmyslně uložen dvakrát v objektu.

Kompilátor vytvoří upozornění pro in nebo argument hodnoty v class_baseargument_list, pokud jsou splněny všechny následující podmínky:

  • Argument představuje implicitní nebo explicitní identitní převod parametru primárního konstruktoru.
  • Argument není součástí rozšířeného argumentu params;
  • Primární parametr konstruktoru je zachycen do stavu ohraničujícího typu.

Kompilátor vygeneruje upozornění pro variable_initializer, pokud jsou splněny všechny následující podmínky:

  • Inicializátor proměnné představuje implicitní nebo explicitní převod identity parametru primárního konstruktoru.
  • Primární parametr konstruktoru je zachycen do stavu ohraničujícího typu.

Například:

public class Person(string name)
{
    public string Name { get; set; } = name;   // warning: initialization
    public override string ToString() => name; // capture
}

Atributy, které cílí na primární konstruktory

V https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md jsme se rozhodli přijmout návrh https://github.com/dotnet/csharplang/issues/7047.

Cíl atributu "method" je povolen na class_declaration/struct_declaration s parameter_list a výsledkem je odpovídající primární konstruktor s daným atributem. Atributy s cílem method na class_declaration/struct_declaration bez parameter_list jsou ignorovány s upozorněním.

[method: FooAttr] // Good
public partial record Rec(
    [property: Foo] int X,
    [field: NonSerialized] int Y
);
[method: BarAttr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public partial record Rec
{
    public void Frobnicate()
    {
        ...
    }
}
[method: Attr] // Good
public record MyUnit1();
[method: Attr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public record MyUnit2;

Primární konstruktory záznamů

V tomto návrhu již záznamy nemusí samostatně určovat primární mechanismus konstruktoru. Místo toho by deklarace (záznamy, třídy a struktury), které mají primární konstruktory, dodržovaly obecná pravidla s těmito jednoduchými doplněními:

  • Pro každý parametr primárního konstruktoru, pokud člen s týmž jménem již existuje, musí být vlastností instance nebo polem. Pokud ne, je automaticky vytvořena veřejná vlastnost určená pouze pro inicializaci stejného názvu s inicializátorem, který přiřazuje hodnotu z parametru.
  • Dekonstruktor je syntetizován s výstupními parametry tak, aby odpovídal parametrům primárního konstruktoru.
  • Pokud je explicitní deklarace konstruktoru "kopírovací konstruktor" – konstruktor, který přebírá jeden parametr daného typu, není potřeba volat inicializátor this a nespustí inicializátory členů v deklaraci záznamu.

Nevýhody

  • Přidělená velikost vytvořených objektů je méně zřejmá, protože kompilátor určuje, zda se má přidělit pole pro parametr primárního konstruktoru na základě úplného textu třídy. Toto riziko se podobá implicitnímu zachycení proměnných výrazy lambda.
  • Běžným pokušením (nebo náhodným vzorem) může být zpracovat "stejný" parametr na více úrovních dědičnosti, jak se předává vzhůru řetězem konstruktoru, místo aby mu bylo explicitně přiděleno chráněné pole v základní třídě, což vede k duplicitním přidělením stejných dat v objektech. Toto je velmi podobné dnešnímu riziku přepsání automatických vlastností pomocí automatických vlastností.
  • Jak je zde navrhováno, neexistuje místo pro další logiku, která by mohla být obvykle vyjádřena v tělech konstruktoru. Níže uvedené rozšíření "primárních konstruktorů" to řeší.
  • Jak je navrhováno, sémantika pořadí provádění se subtilně liší oproti běžným konstruktorům, kdy jsou inicializátory členů zpožděny až po základních voláních. Pravděpodobně by to šlo vyřešit, ale za cenu některých návrhů rozšíření (zejména "těl primárních konstruktorů").
  • Návrh funguje jenom pro scénáře, kdy může být jeden konstruktor určen jako primární.
  • Neexistuje způsob, jak odděleně vyjádřit přístupnost třídy a primárního konstruktoru. Příkladem je, když všechny veřejné konstruktory delegují na jeden soukromý konstruktor „build-it-all“. V případě potřeby je možné pro ni později navrhnout syntaxi.

Alternativy

Bez zachycení

Mnohem jednodušší verze funkce by zakázala výskyt parametrů primárního konstruktoru v členských orgánech. Odkazování na ně by bylo chybou. Pole musí být explicitně deklarována, pokud je úložiště žádoucí nad rámec inicializačního kódu.

public class C(string s)
{
    public string S1 => s; // Nope!
    public string S2 { get; } = s; // Still allowed
}

To by se mohlo později rozvinout do úplného návrhu a vyhnuli byste se řadě rozhodnutí a složitostí, a to za cenu, že by se méně odstranilo základní struktury a je to pravděpodobně i zdánlivě neintuitivní.

Explicitní vygenerovaná pole

Alternativním přístupem je, aby parametry primárního konstruktoru vždy a vizuálně vygenerovaly pole se stejným názvem. Místo uzavření parametrů stejným způsobem jako v místních a anonymních funkcích by zde byla explicitní deklarace člena, podobná veřejným vlastnostem generovaným pro primární parametry konstruktoru v záznamech. Stejně jako u záznamů, pokud již existuje vhodný člen, žádný další by se negeneroval.

Pokud je vygenerované pole soukromé, může být přesto vynecháno, pokud není použito jako pole v tělech členů. Ve třídách by však soukromé pole často nebylo správnou volbou, protože by to v odvozených třídách mohlo způsobit duplikaci stavu. Tady by byla možnost generovat chráněné pole ve třídách, což by podporovalo opakované použití úložiště napříč vrstvami dědičnosti. Pak však nebudeme moct deklaraci elidovat a byly by vynaloženy náklady na přidělení pro každý parametr primárního konstruktoru.

To by sladilo primární konstruktory, které nejsou záznamy, těsněji s těmi záznamovými, v tom smyslu, že členové jsou vždy (alespoň po koncepční stránce) generováni, ačkoli různé druhy členů s různou přístupností. Ale také by to vedlo k překvapivým rozdílům od toho, jak jsou parametry a místní hodnoty zachyceny jinde v jazyce C#. Pokud bychom například někdy povolili místní třídy, implicitně zachytávaly uzavřené parametry a lokální proměnné. Zdá se, že generování stínových polí pro ně není rozumné chování.

Dalším problémem často vyvolaným tímto přístupem je, že mnoho vývojářů má různé konvence pojmenování parametrů a polí. Které by se měly použít pro parametr primárního konstruktoru? Obě volby by vedly k nekonzistence se zbytkem kódu.

Nakonec, viditelné generování deklarací členů je skutečně zásadní pro záznamy, ale mnohem překvapivější a "mimo charakter" pro běžné třídy a struktury. To vše je důvod, proč se hlavní návrh rozhodne pro implicitní zachycení s rozumným chováním (v souladu se záznamy) pro explicitní deklarace členů, pokud je to žádoucí.

Odebrání členů instance z oboru inicializátoru

Výše uvedená vyhledávací pravidla jsou určena k tomu, aby umožňovala aktuální chování parametrů primárního konstruktoru v záznamech, pokud je odpovídající člen ručně deklarován, a vysvětlit chování vygenerovaného člena, pokud není. To vyžaduje rozlišit vyhledávání mezi "inicializačním oborem" (this/base inicializátory, inicializátory členů) a "oborem těla" (těla členů), což výše uvedený návrh dosahuje změnou v závislosti na hledání parametrů primárního konstruktoru, v závislosti na tom, kde se odkaz vyskytuje.

Pozorování spočívá v tom, že odkazování na člena instance s jednoduchým názvem v oboru inicializátoru vždy vede k chybě. Místo pouhého stínování členů instance na těchto místech bychom je mohli jednoduše vyřadit z dostupnosti? Tímto způsobem by nebylo toto divné podmíněné řazení oborů.

Tato alternativa je pravděpodobně možná, ale měla by určité důsledky, které jsou poněkud dalekosáhlé a potenciálně nežádoucí. Za prvé, pokud odebereme členy instance z oboru inicializátoru, pak jednoduché jméno, které odpovídá členu instance a není parametrem primárního konstruktoru, by se mohlo omylem navázat na něco mimo deklaraci typu! Zdá se, že by to bylo zřídka úmyslné a chyba by byla lepší.

Kromě toho je v inicializačním oboru v pořádku odkazovat na statické členy . Proto bychom museli rozlišovat mezi statickými členy a členy instance při vyhledávání, něco, co dnes neděláme. (Při rozlišování přetížení děláme rozdíly, ale to zde není relevantní). To by také muselo být změněno, což vede k ještě více situacím, kdy by například ve statických kontextech něco vytvořilo vazbu "dále ven" místo chyby, protože našla člena instance.

Celkově by toto "zjednodušení" vedlo ke komplikaci, kterou nikdo nepožádal.

Možná rozšíření

Jedná se o varianty nebo doplňky základního návrhu, které mohou být považovány za užitečné ve spojení s ním, nebo v pozdější fázi.

Přístup k parametrům primárního konstruktoru v rámci konstruktorů

Výše uvedená pravidla zakazují odkazování na parametr primárního konstruktoru v jiném konstruktoru. To může být povoleno v těla jiných konstruktorů, protože primární konstruktor běží jako první. V seznamu argumentů inicializátoru this však musí zůstat nepovolený.

public class C(bool b, int i, string s) : B(b)
{
    public C(string s) : this(b, s) // b still disallowed
    { 
        i++; // could be allowed
    }
}

Takový přístup by stále způsoboval zachycení, protože by to byl jediný způsob, jak by tělo konstruktoru mohlo mít přístup k proměnné poté, co již primární konstruktor proběhl.

Zákaz parametrů primárního konstruktoru v argumentech tohoto inicializátoru by mohl být oslaben tak, aby povolit jejich použití, ale zůstaly by potenciálně nepřiřazeny, ale to se nezdá být užitečné.

Povolit konstruktory bez inicializátoru „this

Konstruktory bez inicializátoru this (tj. s implicitním nebo explicitním inicializátorem base) lze povolit. Takový konstruktor by nespustil inicializátory polí instance, vlastností a událostí, protože ty by byly považovány pouze za součást primárního konstruktoru.

V přítomnosti takových konstruktorů základního volání existuje několik možností, jak se zpracovává zachytávání parametrů primárního konstruktoru. Nejjednodušší je zcela zakázat zachycení v této situaci. Parametry primárního konstruktoru by byly pro inicializaci pouze v případě, že takové konstruktory existují.

Alternativně, pokud je to spojeno s dříve popsanou možností povolit přístup k primárním parametrům konstruktoru v rámci konstruktorů, parametry mohou vstoupit do těla konstruktoru bez jednoznačného přiřazení a ty, které jsou zachyceny, musí být jednoznačně přiřazeny na konci těla konstruktoru. V podstatě by to byly implicitní výstupní parametry. Takto by zachycené parametry primárního konstruktoru vždy měly rozumnou (tj. explicitně přiřazenou) hodnotu v době, kdy jsou spotřebovány jinými členy funkce.

Zajímavostí tohoto rozšíření (v obou formách) je, že plně zobecňuje aktuální výjimku pro "kopírovací konstruktory" v záznamech, aniž by to vedlo k situacím, kdy jsou pozorovány neinicializované parametry primárního konstruktoru. Konstruktory, které inicializují objekt alternativními způsoby, jsou v podstatě v pořádku. Omezení související s zachycením by nebyla zásadní změnou stávajících ručně definovaných konstruktorů kopírování v záznamech, protože záznamy nikdy nezachytávají primární parametry konstruktoru (místo toho generují pole).

public class C(bool b, int i, string s) : B(b)
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s2) : base(true) // cannot use `string s` because it would shadow
    { 
        s = s2; // must initialize s because it is captured by S
    }
    protected C(C original) : base(original) // copy constructor
    {
        this.s = original.s; // assignment to b and i not required because not captured
    }
}

Těla hlavních konstruktorů

Konstruktory samy často obsahují logiku ověření parametru nebo jiný netriviální inicializační kód, který nelze vyjádřit jako inicializátory.

Primární konstruktory lze rozšířit tak, aby umožňovaly zobrazení bloků příkazů přímo v těle třídy. Tyto příkazy by byly vloženy do vygenerovaného konstruktoru v místě, kde se nacházejí mezi přiřazovacími inicializacemi, a proto by byly provedeny proloženy s inicializátory. Například:

public class C(int i, string s) : B(s)
{
    {
        if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
    }
	int[] a = new int[i];
    public int S => s;
}

Pokud bychom chtěli zavést "konečné inicializátory", které se spouštějí po dokončení všech konstruktorů a i všech inicializátorů objektů a kolekcí, mohla by být značná část tohoto scénáře pokryta. Ověření argumentu je ale jedna věc, která by se ideálně stala co nejdříve.

Těla primárního konstruktoru mohou také poskytnout možnost nastavení modifikátoru přístupu pro primární konstruktor, umožňující jeho odchýlení se od přístupnosti ohraničujícího typu.

Kombinované deklarace parametrů a členů

Možným a často zmíněným sčítáním může být umožnit, aby parametry primárního konstruktoru byly anotovány tak, aby také deklarovat člena typu. Nejčastěji se navrhuje povolit specifikátoru přístupu u parametrů, aby aktivoval generování členů:

public class C(bool b, protected int i, string s) : B(b) // i is a field as well as a parameter
{
    void M()
    {
        ... i ... // refers to the field i
        ... s ... // closes over the parameter s
    }
}

Existují některé problémy:

  • Co když je požadovaná vlastnost, ne pole? Mít syntaxi { get; set; } přímo v seznamu parametrů nevypadá příliš lákavě.
  • Co když se pro parametry a pole používají různé zásady vytváření názvů? Tato funkce by pak byla zbytečná.

Jedná se o potenciální budoucí přidání, které lze přijmout nebo ne. Aktuální návrh ponechává možnost otevřenou.

Otevřené otázky

Pořadí vyhledávání pro parametry typu

Oddíl https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup určuje, že parametry typu deklarovaného typu by měly být před parametry primárního konstruktoru typu v každém kontextu, kde jsou tyto parametry použitelné. U záznamů však již existuje chování – parametry primárního konstruktoru přicházejí před parametry typu v inicializátoru základní třídy a inicializátorech polí.

Co bychom měli dělat s touto nesrovnalostí?

  • Upravte pravidla tak, aby odpovídala chování.
  • Upravte chování (možná změna narušující kompatibilitu).
  • Zakázat parametr primárního konstruktoru, aby použil název parametru typu (možná změna, která může způsobit nekompatibilitu).
  • Nedělejte nic, přijměte nekonzistence mezi specifikací a implementací.

Závěr:

Upravte pravidla tak, aby odpovídala chování (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors).

Atributy cílení na pole pro zachycené parametry primárního konstruktoru

Měli bychom povolit atributy cílení na pole pro zachycené parametry primárního konstruktoru?

class C1([field: Test] int x) // Parameter is captured, the attribute goes to the capture field
{
    public int X => x;
}
class C2([field: Test] int x) // Parameter is not captured, the attribute is ignored with a warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
    public int X = x;
}

Právě teď jsou atributy ignorovány s upozorněním bez ohledu na to, jestli je parametr zachycen.

Všimněte si, že u záznamů jsou povolené atributy zaměřené na pole, když je pro ně syntetizována vlastnost. Atributy pak přejdou do backingového pole.

record R1([field: Test]int X); // Ok, the attribute goes on the backing field
record R2([field: Test]int X) // warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
    public int X = X;
}

Závěr:

Nepovoleno (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#attributes-on-captured-parameters).

Upozornit na stínování od člena ze základny

Měli bychom nahlásit upozornění, když člen ze základny stínuje parametr primárního konstruktoru uvnitř člena (viz https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621)?

Závěr:

Alternativní návrh je schválen – https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors

Zachycení instance uzavřeného typu v uzavření

Při zachycení parametru do kontextu stavu ohraničujícího typu, na který se také odkazuje v lambdě uvnitř inicializátoru instance nebo základního inicializátoru, by lambda a stav ohraničujícího typu měly odkazovat na stejné umístění pro tento parametr. Například:

partial class C1
{
    public System.Func<int> F1 = Execute1(() => p1++);
}

partial class C1 (int p1)
{
    public int M1() { return p1++; }
    static System.Func<int> Execute1(System.Func<int> f)
    {
        _ = f();
        return f;
    }
}

Vzhledem k tomu, že naïve implementace zachycení parametru do stavu typu jednoduše zachycuje parametr v poli privátní instance, lambda musí odkazovat na stejné pole. V důsledku toho musí mít přístup k instanci typu. To vyžaduje, aby byl this zachycen do uzávěru před vyvoláním základního konstruktoru. To zase vede k bezpečnému, ale neověřitelnému IL. Je to přijatelné?

Případně můžeme:

  • Nepovolte takové lambda výrazy;
  • Nebo místo toho zachyťte parametry jako v instanci samostatné třídy (ještě jiné uzavření) a sdílejte tuto instanci mezi uzavřením a instancí nadřazeného typu. Tímto se eliminuje nutnost zachytit this v uzávěru.

Závěr:

Jsme v pohodě s zachycením this do uzávěru před vyvoláním základního konstruktoru (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md). Tým runtime také nenalezl vzor IL problematický.

Přiřazení k this v rámci struktury

Jazyk C# umožňuje přiřadit this v rámci struktury. Pokud struktura zachytí primární parametr konstruktoru, přiřazení přepíše jeho hodnotu, což nemusí být pro uživatele zřejmé. Chceme hlásit upozornění kvůli přiřazení, jako je toto?

struct S(int x)
{
    int X => x;
    
    void M(S s)
    {
        this = s; // 'x' is overwritten
    }
}

Závěr:

Povoleno, bez upozornění (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).

Upozornění na dvojité úložiště při inicializaci a zachytávání dat

Máme upozornění, pokud je parametr primárního konstruktoru předán základnímu objektu a také a zachyceno, protože existuje vysoké riziko, že je neúmyslně dvakrát uložen v objektu.

Zdá se, že existuje podobné riziko, pokud se parametr používá k inicializaci člena a je zachycen také. Tady je malý příklad:

public class Person(string name)
{
    public string Name { get; set; } = name;   // initialization
    public override string ToString() => name; // capture
}

U dané instance Personby se změny Name neprojevily ve výstupu ToString, což je pravděpodobně nezamýšlené na straně vývojáře.

Měli bychom pro tuto situaci zavést upozornění na dvojité úložiště?

Takto by to fungovalo:

Kompilátor vygeneruje upozornění pro variable_initializer, pokud jsou splněny všechny následující podmínky:

  • Inicializátor proměnné představuje implicitní nebo explicitní převod identity parametru primárního konstruktoru.
  • Primární parametr konstruktoru je zachycen do stavu ohraničujícího typu.

Závěr:

Schváleno, viz https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors

Schůzky LDM