Udostępnij za pośrednictwem


Konstruktory podstawowe

Notatka

Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.

Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są przechwytywane w odpowiednich spotkania projektowego języka (LDM).

Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .

Streszczenie

Klasy i struktury mogą mieć listę parametrów, a ich specyfikacja klasy bazowej może mieć listę argumentów. Podstawowe parametry konstruktora są dostępne w całej deklaracji klasy lub struktury, a jeśli są przechwytywane przez składową funkcji lub funkcję anonimową, są one odpowiednio przechowywane (np. jako tajne pola prywatne zadeklarowanej klasy lub struktury).

Propozycja wprowadza "retrospektywną ciągłość," stosując istniejące konstruktory podstawowe na rekordach do tej bardziej ogólnej funkcji, z dodanymi niektórymi syntezowanymi elementami.

Motywacja

Zdolność klasy lub struktury w języku C# do posiadania więcej niż jednego konstruktora zapewnia większą elastyczność, ale kosztem nieco bardziej złożonej składni deklaracji, ponieważ dane wejściowe konstruktora i stan obiektu muszą być czysto oddzielone.

Konstruktory podstawowe umieszczają parametry jednego konstruktora, czyniąc je dostępne w całej klasie lub strukturze, do użycia podczas inicjalizacji lub bezpośrednio jako stan obiektu. Kompromis polega na tym, że każdy inny konstruktor musi wywoływać za pośrednictwem konstruktora podstawowego.

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

Szczegółowy projekt

W artykule opisano uogólniony projekt dotyczący rekordów i nierejestrowych struktur, a następnie szczegółowo przedstawiono, w jaki sposób istniejące konstruktory podstawowe dla rekordów są określone poprzez dodanie zestawu syntetyzowanych członków w obecności konstruktora podstawowego.

Składnia

Deklaracje klas i struktur są rozszerzone, aby umożliwić listę parametrów w nazwie typu, listę argumentów w klasie bazowej i treść składającą się tylko 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 ',' '}' ';'?
  | ';'
  ;

Uwaga: Te produkcje zastępują record_declaration w Records i record_struct_declaration w strukturach rekordów , obydwie stają się przestarzałe.

Jest błędem, gdy class_base ma argument_list, jeśli otaczające class_declaration nie zawiera parameter_list. Co najwyżej jedna deklaracja częściowego typu klasy lub struktury może dostarczyć parameter_list. Parametry w parameter_list deklaracji record muszą być parametrami wartości.

Należy pamiętać, że zgodnie z niniejszym wnioskiem class_body, struct_body, interface_body i enum_body mogą składać się tylko z ;.

Klasa lub struktura z parameter_list ma niejawny publiczny konstruktor, którego podpis odpowiada parametrom wartości deklaracji typu. Jest to nazywane konstruktorem podstawowym dla typu i powoduje pominięcie niejawnie zadeklarowanego konstruktora bez parametrów, jeśli istnieje. Jest błędem posiadanie zarówno konstruktora podstawowego, jak i konstruktora z tym samym podpisem w deklaracji typu.

Wyszukiwanie

Wyszukiwanie prostych nazw jest rozbudowane, aby obsługiwać główne parametry konstruktora. Zmiany są wyróżnione pogrubionym w następującym fragmencie:

  • W przeciwnym razie, dla każdego typu wystąpienia T (§15.3.2), zaczynając od typu wystąpienia bezpośrednio otaczającej deklaracji typu, a następnie kontynuując przez typ wystąpienia każdej otaczającej klasy lub struktury (jeśli istnieją):
    • Jeśli deklaracja T zawiera podstawowy parametr konstruktora I, a odwołanie występuje w argument_listclass_baseTlub w inicjatorze pola, właściwości lub zdarzenia T, wynik jest podstawowym parametrem konstruktora I
    • w przeciwnym razie, jeśli e ma wartość zero, a deklaracja T zawiera parametr typu o nazwie I, simple_name odnosi się do tego parametru typu.
    • W przeciwnym razie, jeśli wyszukiwanie elementu członkowskiego (§12.5) I w T z argumentami typu e prowadzi do znalezienia dopasowania:
      • Jeśli T jest typem wystąpienia natychmiast otaczającej klasy lub typu struktury, a wyszukiwanie identyfikuje jedną lub więcej metod, wynikiem jest grupa metod ze skojarzonym wyrażeniem wystąpienia this. Jeśli określono listę argumentów typu, jest ona używana w wywoływaniu metody ogólnej (§12.8.10.2).
      • W przeciwnym razie, jeśli T jest typem instancji bezpośrednio otaczającej klasy lub typu struktury, jeśli odnośnik identyfikuje element instancji, a odwołanie występuje w bloku konstruktora instancji, metody instancji lub akcesora instancji (§12.2.1), wynik jest taki sam jak dostęp do elementu (§12.8.7) w postaci this.I. Może się to zdarzyć tylko wtedy, gdy e wynosi zero.
      • W przeciwnym razie wynik jest taki sam jak dostęp do członka (§12.8.7) postaci T.I lub T.I<A₁, ..., Aₑ>.
    • W przeciwnym razie, jeśli deklaracja T zawiera podstawowy parametr konstruktora I, wynik jest podstawowym parametrem konstruktora I.

Pierwszy dodatek odpowiada zmianie poniesionej przez konstruktorów podstawowych na rekordachi zapewnia, że podstawowe parametry konstruktora zostaną znalezione przed odpowiednimi polami w inicjatorach i argumentach klasy bazowej. Rozszerza tę regułę również na statyczne inicjatory. Jednak ponieważ rekordy zawsze mają członka instancji o tej samej nazwie co parametr, rozszerzenie może prowadzić tylko do zmiany komunikatu o błędzie. Niedozwolony dostęp do parametru a niedozwolony dostęp do elementu członkowskiego obiektu.

Drugi dodatek umożliwia znajdowanie parametrów konstruktora głównego w innym miejscu w treści typu, ale tylko wtedy, gdy nie zostaną zacienione przez członków.

Jest to błąd podczas odwołowania się do podstawowego parametru konstruktora, jeśli odwołanie nie występuje w ramach jednego z następujących elementów:

  • argument nameof
  • inicjalizator pola instancji, właściwości lub zdarzenia typu deklarującego (typ deklarujący konstruktor podstawowy z parametrem).
  • argument_list class_base typu deklarowanego.
  • treść metody wystąpienia (należy pamiętać, że konstruktory wystąpień są wykluczone) typu deklaratywnego.
  • treść wystąpienia metody dostępu typu deklaratora.

Innymi słowy, podstawowe parametry konstruktora są dostępne w całym ciele zadeklarowanego typu. Ukrywają składowe typu deklarowanego w inicjatorze pola, właściwości lub zdarzenia tego typu albo w argument_listclass_base dla tego typu deklarowanego. Są one przesłaniane przez członków typu deklarującego w innych miejscach.

W związku z tym w następującej deklaracji:

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

Inicjator pola i odwołuje się do parametru i, natomiast treść właściwości I odwołuje się do pola i.

Ostrzegaj przed cieniowaniem przez element członkowski z bazy

Kompilator wygeneruje ostrzeżenie dotyczące użycia identyfikatora, gdy element członkowski bazowy przysłania podstawowy parametr konstruktora, jeśli ten podstawowy parametr konstruktora nie został przekazany do typu bazowego za pośrednictwem jego konstruktora.

Podstawowy parametr konstruktora jest uważany za przekazywany do typu podstawowego za pośrednictwem konstruktora, gdy wszystkie następujące warunki są spełnione dla argumentu w class_base:

  • Argument reprezentuje niejawną lub jawną konwersję tożsamości podstawowego parametru konstruktora;
  • Argument nie jest częścią rozwiniętego argumentu params;

Semantyka

Podstawowy konstruktor prowadzi do generowania konstruktora instancji dla otaczającego typu z podanymi parametrami. Jeśli class_base ma listę argumentów, wygenerowany konstruktor wystąpienia będzie miał inicjator base z tą samą listą argumentów.

Podstawowe parametry konstruktora w deklaracjach klas/struktur można zadeklarować ref, in lub out. Deklarowanie parametrów ref lub out jest nadal niedozwolone w podstawowych konstruktorach deklaracji rekordu.

Wszystkie inicjatory składowych wystąpień w ciele klasy staną się przypisaniami w wygenerowanym konstruktorze.

Jeśli parametr konstruktora głównego jest przywoływany wewnątrz elementu instancji członkowskiej, a odwołanie nie znajduje się wewnątrz argumentu nameof, jest przechwytywany do stanu otaczającego typu, co gwarantuje jego dostępność po zakończeniu konstruktora. Prawdopodobną strategią implementacji jest użycie pola prywatnego przy użyciu zdeformowanej nazwy. W strukturze tylko do odczytu pola będą tylko do odczytu. W związku z tym dostęp do przechwyconych parametrów struktury tylko do odczytu będzie miał podobne ograniczenia, jak dostęp do pól tylko do odczytu. Dostęp do przechwyconych parametrów w ramach elementu członkowskiego tylko do odczytu będzie miał podobne ograniczenia jak dostęp do pól wystąpień w tym samym kontekście.

Przechwytywanie nie jest dozwolone dla parametrów typu ref oraz parametrów ref, in lub out. Jest to podobne do ograniczenia przechwytywania w lambdach.

Jeśli podstawowy parametr konstruktora jest przywoływany tylko z inicjatorów członków instancji, mogą one bezpośrednio odwoływać się do parametru wygenerowanego konstruktora, ponieważ są one wykonywane jako część tej operacji.

Podstawowy konstruktor wykona następującą sekwencję operacji:

  1. Wartości parametrów są przechowywane w polach przechwytywania, jeśli istnieją.
  2. Inicjatory wystąpień są wykonywane
  3. Inicjalizator konstruktora bazowego jest wywoływany

Odwołania do parametrów w dowolnym kodzie użytkownika są zastępowane odpowiednimi odwołaniami do pól przechwytywania.

Na przykład ta deklaracja:

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

Generuje kod podobny do następującego:

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

Pojawia się błąd, gdy deklaracja konstruktora innego niż podstawowy ma taką samą listę parametrów jak konstruktor podstawowy. Wszystkie deklaracje konstruktorów innych niż podstawowe muszą używać inicjalizatora this, aby konstruktor podstawowy był ostatecznie wywoływany.

Rekordy generują ostrzeżenie, jeśli podstawowy parametr konstruktora nie jest odczytywany w (prawdopodobnie wygenerowanych) inicjatorach wystąpienia lub inicjatorze bazowym. Podobne ostrzeżenia będą zgłaszane dla podstawowych parametrów konstruktora w klasach i strukturach:

  • dla parametru by-value, jeśli parametr nie jest przechwytywany i nie jest odczytywany w żadnych inicjatorach wystąpień lub inicjatorze podstawowym.
  • dla parametru in, jeśli parametr nie jest odczytywany w żadnych inicjalizatorach wystąpień lub inicjalizatorze bazowym.
  • dla parametru ref, jeśli parametr nie jest odczytywany ani zapisywany w żadnych inicjatorach wystąpień lub inicjatorze podstawowym.

Identyczne proste nazwy i nazwy typów

Istnieje specjalna reguła językowa dla scenariuszy często określanych jako scenariusze „Kolor Kolor” - Identyczne proste nazwy i nazwy typów.

W przypadku dostępu do elementu członkowskiego formy E.I, jeśli E jest pojedynczym identyfikatorem, a znaczenie E jako nazwa_prosta (§12.8.4) jest stałą, polem, właściwością, zmienną lokalną lub parametrem o takim samym typie jak znaczenie E jako nazwa_typu (§7.8.1), wówczas dozwolone są oba możliwe znaczenia E. Wyszukiwanie członka E.I nigdy nie jest niejednoznaczne, ponieważ I jest koniecznie członkiem typu E w obu przypadkach. Innymi słowy, reguła po prostu zezwala na dostęp do statycznych elementów członkowskich i zagnieżdżonych typów E, gdzie inaczej wystąpiłby błąd czasu kompilacji.

W odniesieniu do konstruktorów podstawowych reguła wpływa na to, czy identyfikator w członku instancji powinien być traktowany jako odwołanie do typu, czy jako odwołanie do parametru konstruktora podstawowego, który z kolei przechwytuje parametr do stanu otaczającego typu. Mimo że "wyszukiwanie elementów członkowskich E.I nigdy nie jest niejednoznaczne", gdy wyszukiwanie zwraca grupę elementów, w niektórych przypadkach nie można określić, czy dostęp do elementu odnosi się do elementu statycznego czy elementu instancji bez pełnego rozwiązania (powiązania) tego dostępu. Jednocześnie przechwytywanie podstawowego parametru konstruktora zmienia właściwości otaczającego typu w sposób, który wpływa na analizę semantyczną. Na przykład typ może stać się niezarządzany i nie spełniać pewnych ograniczeń z tego powodu. Istnieją nawet scenariusze, w których powiązanie może zakończyć się powodzeniem w zależności od tego, czy parametr jest uznawany za przechwycony, czy nie. Na przykład:

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

Jeśli traktujemy odbiornik Color jako wartość, przechwycimy parametr i "S1" stanie się zarządzany. Następnie metoda statyczna staje się niewłaściwa ze względu na ograniczenie i wywołujemy metodę instancji. Jeśli jednak traktujemy odbiornik jako typ, nie przechwycimy parametru, a parametr "S1" pozostaje niezarządzany, wówczas obie metody mają zastosowanie, ale metoda statyczna jest "lepsza", ponieważ nie ma opcjonalnego parametru. Żaden wybór nie prowadzi do błędu, ale każdy z nich skutkuje odmiennym zachowaniem.

W związku z tym, kompilator wygeneruje błąd niejednoznaczności dla dostępu do składowej E.I, gdy zostaną spełnione wszystkie następujące warunki:

  • Wyszukiwanie elementów członkowskich E.I daje grupę członkowską zawierającą zarówno elementy instancji, jak i statyczne. Metody rozszerzeń stosowane do typu odbiorcy są traktowane jako metody wystąpienia na potrzeby tego sprawdzenia.
  • Jeśli E jest traktowana jako prosta nazwa, a nie nazwa typu, odwołuje się do podstawowego parametru konstruktora i przechwytuje parametr w stanie otaczającego typu.

Ostrzeżenia dotyczące podwójnego wykorzystania pamięci

Jeśli podstawowy parametr konstruktora jest przekazywany do klasy bazowej, a oraz są przechwytywane, istnieje duże ryzyko, że zostanie przypadkowo przechowany dwa razy w obiekcie.

Kompilator utworzy ostrzeżenie dla in lub według argumentu wartości w class_baseargument_list, gdy spełnione są wszystkie następujące warunki:

  • Argument reprezentuje niejawną lub jawną konwersję tożsamości podstawowego parametru konstruktora;
  • Argument nie jest częścią rozwiniętego argumentu params;
  • Podstawowy parametr konstruktora jest zapisywany w stanie otaczającej klasy.

Kompilator wyświetli ostrzeżenie dla variable_initializer, gdy spełnione są wszystkie następujące warunki:

  • Inicjator zmiennej reprezentuje niejawną lub jawną konwersję tożsamości podstawowego parametru konstruktora;
  • Podstawowy parametr konstruktora jest przechwytywany w stanie otaczającego typu.

Na przykład:

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

Atrybuty przeznaczone dla konstruktorów podstawowych

Na https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md postanowiliśmy przyjąć propozycję https://github.com/dotnet/csharplang/issues/7047.

Obiekt docelowy atrybutu "method" jest dozwolony na class_declaration/struct_declaration z parameter_list i powoduje, że odpowiedni konstruktor podstawowy ma ten atrybut. Atrybuty z obiektem docelowym method na class_declaration/struct_declaration bez parameter_list są ignorowane z ostrzeżeniem.

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

Konstruktory podstawowe w rekordach

W przypadku tej propozycji rekordy nie muszą już oddzielnie określać podstawowego mechanizmu konstruktora. Zamiast tego należy rejestrować deklaracje (klasy i struktury), które mają konstruktory podstawowe, przestrzegałyby ogólnych reguł, z następującymi prostymi dodatkami:

  • Dla każdego podstawowego parametru konstruktora, jeśli człon klasy o tej samej nazwie już istnieje, musi być właściwością wystąpienia lub polem. Jeśli nie, publiczna automatyczna właściwość z ograniczeniem inicjalizacji o tej samej nazwie jest automatycznie tworzona i inicjalizowana z przypisaniem wartości z parametru.
  • Dekonstruktor jest syntetyzowany z parametrami out w celu dopasowania do podstawowych parametrów konstruktora.
  • Jeśli jawna deklaracja konstruktora jest "konstruktorem kopii" — konstruktorem, który przyjmuje pojedynczy parametr otaczającego typu — nie jest wymagane wywołanie inicjatora this i nie wykona inicjatorów składowych znajdujących się w deklaracji rekordu.

Wady i niedogodności

  • Rozmiar alokacji skonstruowanych obiektów jest mniej oczywisty, ponieważ kompilator określa, czy przydzielić pole dla podstawowego parametru konstruktora na podstawie pełnego tekstu klasy. To ryzyko jest podobne do niejawnego przechwytywania zmiennych przez wyrażenia lambda.
  • Typową pokusą (lub przypadkowym wzorcem) może być przechwycenie "tego samego" parametru na wielu poziomach dziedziczenia, gdy jest on przekazywany w górę łańcucha konstruktorów, zamiast jawnie przypisać mu pole chronione w klasie bazowej. To prowadzi do zduplikowanych alokacji dla tych samych danych w obiektach. Jest to bardzo podobne do dzisiejszego ryzyka zastąpienia właściwości automatycznych za pomocą właściwości automatycznych.
  • Jak zaproponowano tutaj, nie ma miejsca na dodatkową logikę, która zwykle może być wyrażona w ciałach konstruktorów. Poniższe rozszerzenie "podstawowe jednostki konstruktora" rozwiązuje ten problem.
  • Zgodnie z propozycją semantyka kolejności wykonywania różni się subtelnie od zwykłych konstruktorów, opóźniając inicjatory składowych do po wywołaniach bazowych. Prawdopodobnie można by to naprawić, ale kosztem niektórych propozycji rozszerzeń (zwłaszcza "ciał głównych konstruktorów").
  • Propozycja działa tylko w scenariuszach, w których można uznać jednego konstruktora za podstawowego.
  • Nie ma możliwości wyrażenia oddzielnej dostępności klasy i konstruktora podstawowego. Przykładem sytuacji jest, gdy wszystkie konstruktory publiczne delegują do jednego prywatnego konstruktora odpowiedzialnego za całą budowę. W razie potrzeby można zaproponować składnię później.

Alternatywy

Brak przechwytywania

Znacznie prostsza wersja funkcji uniemożliwiałaby występowanie podstawowych parametrów konstruktora w ciałach członkowskich. Odwoływanie się do nich byłoby błędem. Pola muszą być jawnie zadeklarowane, jeśli przechowywanie jest wymagane poza kodem inicjalizacyjnym.

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

To nadal może ewoluować w pełną propozycję w późniejszym czasie, co pozwoliłoby uniknąć wielu decyzji i złożoności, kosztem usunięcia mniej szablonowych elementów początkowo, i prawdopodobnie początkowo wydające się nieintuicyjne.

Pola wygenerowane jawnie

Alternatywne podejście polega na tym, że podstawowe parametry konstruktora zawsze i wyraźnie generują pole o tej samej nazwie. Zamiast zamykać parametry w taki sam sposób, jak funkcje lokalne i anonimowe, jawnie będzie wygenerowana deklaracja składowa, podobna do właściwości publicznych wygenerowanych dla podstawowych parametrów uwierzytelniania w rekordach. Podobnie jak w przypadku rekordów, jeśli już istnieje odpowiedni element, nie zostanie wygenerowany.

Jeśli wygenerowane pole jest prywatne, może być nadal pominięte, gdy nie jest używane jako pole w strukturach klas. Jednak w klasach pole prywatne często nie byłoby właściwym wyborem, ponieważ mogłoby prowadzić do duplikacji stanu w klasach pochodnych. Jedną z opcji w tym miejscu byłoby wygenerowanie chronionego pola w klasach, co zachęcałoby do ponownego użycia pamięci przez warstwy dziedziczenia. Jednak wówczas nie będziemy w stanie pominąć deklaracji i poniesiemy koszt alokacji dla każdego parametru konstruktora podstawowego.

Byłoby to bardziej zbliżone do tego, jak konstruktorzy podstawowi bez rekordów są uporządkowani w stosunku do tych z rekordami, w tym sensie, że elementy składowe są zawsze (przynajmniej koncepcyjnie) generowane, chociaż różnią się rodzajem i dostępami. Ale doprowadziłoby to również do zaskakujących różnic w sposobie przechwytywania parametrów i ustawień lokalnych w innym miejscu w języku C#. Gdybyśmy kiedykolwiek zezwolili na klasy lokalne, na przykład przechwyciliby niejawnie otaczające parametry i ustawienia lokalne. Wyraźnie generowanie pól cieniowania dla nich nie wydaje się być rozsądnym zachowaniem.

Innym problemem często zgłaszany przy użyciu tego podejścia jest to, że wielu deweloperów ma różne konwencje nazewnictwa parametrów i pól. Które należy użyć dla podstawowego parametru konstruktora? Wybór może prowadzić do niespójności z resztą kodu.

Wreszcie, wyraźne generowanie deklaracji składowych jest naprawdę kluczowym aspektem dla rekordów, ale znacznie bardziej zaskakujące i nietypowe dla klas i struktur innych niż rekordy. W sumie są to powody, dla których główna propozycja decyduje się na niejawne przechwytywanie, z rozsądnym zachowaniem (zgodnym z rekordami) dla jawnych deklaracji składowych, gdy są one wymagane.

Usuwanie elementów członkowskich instancji z zakresu konstruktora

Powyższe reguły wyszukiwania mają na celu umożliwienie bieżącego zachowania podstawowych parametrów konstruktora podstawowego w rekordach, gdy odpowiedni członek jest zadeklarowany ręcznie, oraz wyjaśnienie zachowania wygenerowanego członka, gdy nie jest. Wymaga to różnic między "zakresem inicjowania" (to/podstawowe inicjatory, inicjatory składowych) i "zakresem treści" (jednostki członkowskie), które powyższe propozycje osiąga, zmieniając podczas wyszukiwania podstawowych parametrów konstruktora, w zależności od tego, gdzie występuje odwołanie.

Należy zauważyć, że odwoływanie się do elementu członkowskiego wystąpienia, używając prostej nazwy w kontekście inicjalizatora, zawsze prowadzi do błędu. Zamiast jedynie ukrywać członków instancji w tych miejscach, czy możemy po prostu usunąć je z zasięgu? W ten sposób nie byłoby tego dziwnego warunkowego uporządkowania obszarów.

Ta alternatywa jest prawdopodobnie możliwa, ale miałoby to pewne konsekwencje, które są nieco daleko idące i potencjalnie niepożądane. Przede wszystkim, jeśli usuniemy elementy członkowskie wystąpienia z zakresu inicjatora, prosta nazwa, która odpowiada członkowi wystąpienia i nie z podstawowym parametrem konstruktora może przypadkowo powiązać coś poza deklaracją typu! Wydaje się, że rzadko byłoby to zamierzone, a błąd byłby lepszy.

Ponadto statyczne elementy członkowskie są dobrze obsługiwane w odniesieniu do zakresu inicjalizacji. Dlatego musielibyśmy w wyszukiwaniu odróżnić statycznych członków od członków instancji, czego obecnie nie robimy. (Rozróżniamy rozdzielczość przeciążenia, ale nie jest to w grze tutaj). Dlatego konieczna byłaby również zmiana, co prowadzi do jeszcze większej liczby sytuacji, w których np. w kontekstach statycznych coś wiązałoby się z częścią bardziej oddaloną, zamiast powodować błąd, ponieważ znalazło członkowską część instancji.

Podsumowując, to "uproszczenie" w rzeczywistości doprowadziłoby do dość poważnych komplikacji, o które nikt się nie prosił.

Możliwe rozszerzenia

Są to odmiany lub dodatki do podstawowej propozycji, które mogą być brane pod uwagę w połączeniu z nim lub na późniejszym etapie, jeśli zostaną uznane za przydatne.

Dostęp do podstawowego parametru konstruktora w konstruktorach

Powyższe reguły powodują błąd, gdy odwołujemy się do podstawowego parametru konstruktora wewnątrz innego konstruktora. Może to być dozwolone w treści innych konstruktorów, ponieważ podstawowy konstruktor uruchamia się jako pierwszy. Jednak musiałoby pozostać niedozwolone w liście argumentów inicjalizatora this.

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

Taki dostęp nadal powoduje przechwycenie, ponieważ byłby to jedyny sposób, w jaki treść konstruktora może uzyskać w zmiennej po uruchomieniu podstawowego konstruktora.

Zakaz podstawowych parametrów konstruktora w argumentach tego inicjatora może zostać osłabiony, aby je umożliwić, ale sprawić, że nie zostaną one zdecydowanie przypisane, ale to nie wydaje się przydatne.

Zezwalaj konstruktorom bez inicjatora this

Konstruktory bez inicjatora this (tj. z niejawnym lub jawnym inicjatorem base) mogą być dozwolone. Taki konstruktor nie uruchomić pola wystąpienia, właściwości i inicjatorów zdarzeń, ponieważ byłyby one uważane za część tylko konstruktora podstawowego.

W obecności konstruktorów wywołujących bazę dostępnych jest kilka opcji obsługi przechwytywania parametrów podstawowego konstruktora. Najprostszym rozwiązaniem jest całkowite uniemożliwienie przechwytywania w tej sytuacji. Podstawowe parametry konstruktora byłyby do inicjowania tylko wtedy, gdy takie konstruktory istnieją.

Alternatywnie, jeśli połączy się to z wcześniej opisaną opcją, która pozwala na dostęp do podstawowych parametrów konstruktora w konstruktorach, parametry mogą wchodzić do treści konstruktora jako niepewne przypisania, a te, które są przechwytywane, muszą być jednoznacznie przypisane do końca ciała konstruktora. Zasadniczo byłyby to parametry niejawne. W ten sposób przechwycone podstawowe parametry konstruktora zawsze miałyby rozsądną (tj. jawnie przypisaną) wartość, zanim zostaną użyte przez inne członki funkcji.

Zaletą tego rozszerzenia (w obu formach) jest to, że w pełni uogólnia aktualne zwolnienie dla "konstruktorów kopiujących" w rekordach, nie prowadząc do sytuacji, w których są obserwowane niezainicjowane podstawowe parametry konstruktora. Zasadniczo konstruktory, które inicjują obiekt w alternatywny sposób, są w porządku. Ograniczenia związane z przechwytywaniem nie byłyby zmianą powodującą niezgodność dla istniejących ręcznie zdefiniowanych konstruktorów kopii w rekordach, ponieważ rekordy nigdy nie przechwytują swoich podstawowych parametrów konstruktora (zamiast tego generują pola).

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

Podstawowe jednostki konstruktora

Konstruktory same w sobie często zawierają logikę walidacji parametrów lub inny nietriwialny kod inicjowania, którego nie można wyrazić jako inicjatorów.

Konstruktory podstawowe można rozszerzyć w celu umożliwienia wyświetlania bloków instrukcji bezpośrednio w treści klasy. Te oświadczenia zostaną wstawione do wygenerowanego konstruktora w miejscu, w którym pojawiają się między inicjującymi przypisaniami, i będą zatem wykonywane przeplatane z inicjatorami. Na przykład:

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

Wiele aspektów tego scenariusza może być odpowiednio pokrytych, jeśli wprowadzimy "końcowe inicjatory", które działają po zakończeniu wszystkich inicjatorów obiektów/kolekcji oraz konstruktorów i. Jednak weryfikacja argumentów jest jedną rzeczą, która najlepiej się stanie tak wcześnie, jak to możliwe.

Podstawowe jednostki konstruktorów mogą również zapewnić miejsce umożliwiające modyfikator dostępu dla konstruktora podstawowego, dzięki czemu może odbiegać od dostępności otaczającego typu.

Połączone deklaracje parametrów i składowych

Możliwe i często wymienione dodanie może pozwolić na dodawanie podstawowych parametrów konstruktora do adnotacji, tak aby również zadeklarować element członkowski w typie. Najczęściej zaleca się zezwolenie specyfikatorowi dostępu na parametrach na wyzwalanie generowania składowych:

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

Istnieją pewne problemy:

  • Co jeśli właściwość jest pożądana, a nie pole? Posiadanie składni { get; set; } wbudowanej na liście parametrów nie wydaje się atrakcyjne.
  • Co zrobić, jeśli dla parametrów i pól są używane różne konwencje nazewnictwa? Wtedy ta funkcja byłaby bezużyteczna.

Jest to potencjalny przyszły dodatek, który można przyjąć lub nie. Obecny wniosek pozostawia otwartą możliwość.

Otwórz pytania

Kolejność wyszukiwania dla parametrów typu

Sekcja https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup określa, że parametry typu deklarowanego typu powinny poprzedzać parametry głównego konstruktora typu w każdym kontekście, w którym te parametry są dostępne. Jednak mamy już istniejące zachowanie z rekordami — podstawowe parametry konstruktora pochodzą przed parametrami typu w inicjatorze podstawowym i inicjatorach pól.

Co należy zrobić w przypadku tej rozbieżności?

  • Dostosuj reguły, aby odpowiadały zachowaniu.
  • Dostosuj zachowanie (potencjalna zmiana niekompatybilna).
  • Nie zezwalaj na parametr konstruktora primiry do używania nazwy parametru typu (możliwa zmiana powodująca niezgodność).
  • Nic nie rób, zaakceptuj niespójność między specyfikacją a implementacją.

Konkluzja:

Dostosuj reguły, aby odpowiadały zachowaniu (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors).

Atrybuty określania wartości docelowej pola dla przechwyconych parametrów konstruktora podstawowego

Czy należy zezwolić na atrybuty określania wartości docelowej pola dla przechwyconych parametrów konstruktora podstawowego?

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

W tej chwili atrybuty są ignorowane z ostrzeżeniem niezależnie od tego, czy parametr jest przechwytywany.

Należy pamiętać, że w przypadku rekordów atrybuty docelowe pól są dozwolone, gdy właściwość jest dla niej syntetyzowana. Następnie atrybuty przechodzą na pole wspierające.

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

Konkluzja:

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

Ostrzegaj przed cieniowaniem przez element członkowski z bazy

Czy należy zgłosić ostrzeżenie, gdy element członkowski z bazy zastępuje podstawowy parametr konstruktora wewnątrz innego elementu członkowskiego (patrz https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621)?

Konkluzja:

Zatwierdzony jest alternatywny projekt — https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors

Przechwytywanie wystąpienia typu otaczającego w zamknięciu

Gdy parametr przechwycony w stanie otaczającego typu jest również przywoływany w lambda wewnątrz inicjatora wystąpienia lub inicjatora podstawowego, lambda i stan otaczającego typu powinny odwoływać się do tej samej lokalizacji dla parametru. Na przykład:

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

Ponieważ naiwna implementacja przechwytywania parametru w stanie typu po prostu przechwytuje parametr w polu wystąpienia prywatnego, lambda musi odwoływać się do tego samego pola. W związku z tym musi mieć możliwość uzyskania dostępu do egzemplarza typu. Aby przechwycić this do zamknięcia, należy to zrobić przed wywołaniem konstruktora podstawowego. To z kolei skutkuje bezpiecznym, ale nieweryfikowalnym IL. Czy jest to akceptowalne?

Alternatywnie możemy:

  • Nie zezwalaj na lambdy takie;
  • Zamiast tego przechwyć parametry w wystąpieniu oddzielnej klasy (co stanowi kolejnego zamknięcie), i podziel to wystąpienie między zamknięciem a instancją otaczającego typu. W ten sposób eliminując konieczność przechwytywania this w zamknięciu.

Konkluzja:

Dobrze jest przechwytywać this do zamknięcia przed wywołaniem konstruktora podstawowego (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md). Zespół środowiska uruchomieniowego również nie znalazł problematycznego wzorca IL.

Przypisywanie do this w ramach struktury

Język C# umożliwia przypisanie this w ramach struktury. Jeśli struktura przechwytuje podstawowy parametr konstruktora, przypisanie zastąpi jego wartość, co może nie być oczywiste dla użytkownika. Czy chcemy zgłosić ostrzeżenie dotyczące takich przypisań?

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

Konkluzja:

Dozwolone, bez ostrzeżenia (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).

Ostrzeżenie o podwójnym zapisie dla inicjalizacji i przechwytywania

Mamy ostrzeżenie, jeśli podstawowy parametr konstruktora jest przekazywany do bazy i także oraz są przechwytywane, ponieważ istnieje duże ryzyko, że są przypadkowo przechowywane dwukrotnie w obiekcie.

Wydaje się, że istnieje podobne ryzyko, jeśli parametr jest używany do inicjowania członka i jest również przechwytywany. Oto mały przykład:

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

W przypadku danego wystąpienia Person, zmiany Name nie zostaną odzwierciedlone w danych wyjściowych ToString, co prawdopodobnie jest niezamierzone ze strony twórcy.

Czy powinniśmy wprowadzić ostrzeżenie o podwójnym przechowywaniu w tej sytuacji?

W ten sposób będzie działać:

Kompilator wyświetli ostrzeżenie dla variable_initializer, gdy spełnione są wszystkie następujące warunki:

  • Inicjator zmiennej reprezentuje niejawną lub jawną konwersję tożsamości podstawowego parametru konstruktora;
  • Podstawowy parametr konstruktora jest zapisywany w stanie otaczającego typu.

Konkluzja:

Zatwierdzone, zobacz https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors

Spotkania LDM