Udostępnij za pośrednictwem


Wymagani członkowie

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

Ta propozycja wprowadza sposób określania, że właściwość lub pole musi być ustawione podczas inicjalizacji obiektu, wymuszając na twórcy instancji podanie początkowej wartości dla członka w inicjatorze obiektu w miejscu tworzenia.

Motywacja

Obecnie hierarchie obiektów wymagają wielu elementów szablonowych do przenoszenia danych na wszystkich poziomach hierarchii. Przyjrzyjmy się prostej hierarchii obejmującej Person, jak można zdefiniować w języku C# 8:

class Person
{
    public string FirstName { get; }
    public string MiddleName { get; }
    public string LastName { get; }

    public Person(string firstName, string lastName, string? middleName = null)
    {
        FirstName = firstName;
        LastName = lastName;
        MiddleName = middleName ?? string.Empty;
    }
}

class Student : Person
{
    public int ID { get; }
    public Student(int id, string firstName, string lastName, string? middleName = null)
        : base(firstName, lastName, middleName)
    {
        ID = id;
    }
}

Tutaj dzieje się wiele powtórzeń:

  1. Na szczycie hierarchii typ każdej właściwości musiał być powtórzony dwa razy, a nazwa cztery razy.
  2. Na poziomie pochodnym typ każdej dziedziczonej właściwości musiał być powtarzany raz, a nazwa musiała być powtarzana dwa razy.

Jest to prosta hierarchia z 3 właściwościami i 1 poziomem dziedziczenia, ale wiele rzeczywistych przykładów takich hierarchii osiąga wiele głębszych poziomów, gromadząc coraz większą liczbę właściwości do przekazania w miarę pogłębiania się. Roslyn jest jedną z takich baz kodowych, na przykład w różnych typach drzew, które tworzą nasze CST i AST. To zagnieżdżanie jest na tyle żmudne, że mamy generatory kodu do generowania konstruktorów i definicji tych typów, a wielu klientów stosuje podobne podejścia do tego problemu. W języku C# 9 wprowadzono rekordy, które w niektórych scenariuszach mogą uczynić to lepszym rozwiązaniem:

record Person(string FirstName, string LastName, string MiddleName = "");
record Student(int ID, string FirstName, string LastName, string MiddleName = "") : Person(FirstName, LastName, MiddleName);

recordwyeliminować pierwsze źródło duplikacji, ale drugie źródło duplikacji pozostaje niezmienione: niestety jest to źródło duplikacji, które rośnie w miarę wzrostu hierarchii i jest najbardziej bolesną częścią duplikacji w celu naprawienia po wprowadzeniu zmiany w hierarchii, ponieważ wymaga ona śledzenia hierarchii przez wszystkie jej lokalizacje, potencjalnie nawet w projektach i potencjalnie łamiących konsumentów.

Aby uniknąć tego powielania jako obejścia, od dawna zauważyliśmy, że użytkownicy stosują inicjatory obiektów jako sposób unikania pisania konstruktorów. Przed C# 9 miało to jednak 2 główne wady:

  1. Hierarchia obiektów musi być w pełni modyfikowalna, z akcesorami set dla każdej właściwości.
  2. Nie ma możliwości zapewnienia, że każde wystąpienie obiektu z grafu ustawia każdy element członkowski.

W języku C# 9 ponownie rozwiązano pierwszy problem, wprowadzając metodę dostępu init: za jej pomocą te właściwości można ustawić podczas tworzenia/inicjowania obiektu, ale nie później. Jednak nadal mamy drugi problem: właściwości w języku C# zostały opcjonalne od wersji C# 1.0. Typy referencyjne dopuszczalne do wartości null, wprowadzone w języku C# 8.0, poradziły sobie z częścią tego problemu: jeśli konstruktor nie inicjuje właściwości typu referencyjnego bez wartości null, użytkownik zostanie o tym ostrzeżony. Jednak nie jest to rozwiązanie problemu: użytkownik chce uniknąć powtarzania dużych fragmentów swojego typu w konstruktorze i zamierza przekazać wymaganie , aby ustawić właściwości dla swoich konsumentów. Nie udostępnia również żadnych ostrzeżeń dotyczących ID z Student, ponieważ jest to typ wartości. Te scenariusze są bardzo powszechne w modelach baz danych ORM, takich jak EF Core, które muszą mieć publiczny konstruktor bez parametrów, ale następnie określają nulowalność wierszy na podstawie nulowalności właściwości.

Wniosek ten ma na celu rozwiązanie tych problemów, wprowadzając nową funkcję w języku C#: wymaganych członków. Wymagani członkowie będą musieli być inicjowani przez użytkowników, zamiast przez autora typu, z możliwością dostosowania, aby zapewnić elastyczność dla wielu konstruktorów i innych scenariuszy.

Szczegółowy projekt

typy class, structi record uzyskują możliwość deklarowania required_member_list. Na tej liście znajdują się wszystkie właściwości i pola typu, które są uznawane za wymaganei muszą zostać zainicjowane w trakcie tworzenia i inicjalizacji instancji tego typu. Typy dziedziczą te listy automatycznie z ich typów bazowych, zapewniając płynne doświadczenie, które usuwa zbędny i powtarzalny kod.

modyfikator required

Dodajemy 'required' do listy modyfikatorów w field_modifier i property_modifier. required_member_list typu obejmuje wszystkie elementy członkowskie, do których required zostało zastosowane. W związku z tym typ Person z wcześniejszej wersji wygląda następująco:

public class Person
{
    // The default constructor requires that FirstName and LastName be set at construction time
    public required string FirstName { get; init; }
    public string MiddleName { get; init; } = "";
    public required string LastName { get; init; }
}

Wszystkie konstruktory typu z required_member_list automatycznie wymagają kontraktu , co oznacza, że odbiorcy typu muszą zainicjować wszystkie właściwości znajdujące się na liście. Błędem jest, gdy konstruktor anonsuje kontrakt, który wymaga elementu, który nie jest co najmniej tak dostępny jak sam konstruktor. Na przykład:

public class C
{
    public required int Prop { get; protected init; }

    // Advertises that Prop is required. This is fine, because the constructor is just as accessible as the property initer.
    protected C() {}

    // Error: ctor C(object) is more accessible than required property Prop.init.
    public C(object otherArg) {}
}

required jest prawidłowy tylko w typach class, structi record. To nie jest prawidłowe dla typów interface. required nie można połączyć z następującymi modyfikatorami:

  • fixed
  • ref readonly
  • ref
  • const
  • static

required nie można zastosować do indeksatorów.

Kompilator wyświetli ostrzeżenie po zastosowaniu Obsolete do wymaganego elementu typu:

  1. Typ jest nieoznaczony jako Obsoletelub
  2. Żaden konstruktor, który nie został przypisany do SetsRequiredMembersAttribute, nie jest oznaczony jako Obsolete.

SetsRequiredMembersAttribute

Wszystkie konstruktory w typie z wymaganymi elementami członkowskimi lub których typ podstawowy określa wymagane elementy członkowskie, muszą mieć te elementy członkowskie ustawione przez konsumenta, gdy ten konstruktor jest wywoływany. Aby zwolnić konstruktorów z tego wymagania, konstruktorowi można nadać atrybut SetsRequiredMembersAttribute, co eliminuje te wymagania. Treść konstruktora nie jest weryfikowana pod kątem zapewnienia, że na pewno ustawia wymagane składowe typu.

SetsRequiredMembersAttribute usuwa wszystkie wymagania z konstruktora, a te wymagania nie są w żaden sposób sprawdzane pod względem poprawności. NB: jest to wyjście awaryjne, jeśli dziedziczenie z typu z nieprawidłową wymaganą listą elementów członkowskich jest konieczne: oznacz konstruktor tego typu za pomocą SetsRequiredMembersAttribute, a nie będą zgłaszane żadne błędy.

Jeśli konstruktor C jest połączony z konstruktorem base lub this, któremu przypisano SetsRequiredMembersAttribute, C musi być również przypisany SetsRequiredMembersAttribute.

W przypadku typów rekordów emitujemy SetsRequiredMembersAttribute w syntetyzowanym konstruktorze kopii rekordu, jeśli typ rekordu lub którykolwiek z jego typów bazowych ma wymagane pola.

NB: Wcześniejsza wersja tej propozycji miała większy metajęzyk wokół inicjowania, umożliwiając dodawanie i usuwanie poszczególnych wymaganych elementów z konstruktora, a także weryfikację, czy konstruktor ustawia wszystkie wymagane elementy. Zostało to uznane za zbyt złożone dla wersji początkowej i usunięte. Możemy przyjrzeć się dodawaniu bardziej złożonych kontraktów i modyfikacji jako późniejszej funkcji.

Egzekwowanie

Dla każdego konstruktora Ci w typie T z wymaganymi składowymi R, użytkownicy, którzy wywołują Ci, muszą wykonać jedną z poniższych czynności:

  • Ustaw wszystkich członków R w object_initializer na object_creation_expression,
  • Możesz też ustawić wszystkich członków R za pośrednictwem sekcji named_argument_listattribute_target.

chyba że Ci jest przypisywany do SetsRequiredMembers.

Jeśli bieżący kontekst nie pozwala na object_initializer lub nie jest attribute_target, i Ci nie jest przypisywany do SetsRequiredMembers, to błędem jest wywołanie Ci.

ograniczenie new()

Typ z konstruktorem bez parametrów, który anonsuje kontraktu nie może zostać zastąpiony przez parametr typu ograniczony do new(), ponieważ nie ma możliwości ogólnego utworzenia wystąpienia, aby upewnić się, że wymagania są spełnione.

struct defaultS

Wymagane elementy członkowskie nie są wymuszane na wystąpieniach typów struct utworzonych za pomocą default lub default(StructType). Są one wymuszane dla wystąpień struct utworzonych za pomocą new StructType(), nawet jeśli StructType nie ma konstruktora bez parametrów i jest używany domyślny konstruktor struktury.

Dostępność

Jest to błąd oznaczania elementu członkowskiego wymaganego, jeśli nie można ustawić elementu członkowskiego w żadnym kontekście, w którym jest widoczny typ zawierający.

  • Jeśli element członkowski jest polem, nie można go readonly.
  • Jeśli element członkowski jest właściwością, musi mieć element ustawiający lub initer co najmniej tak dostępny, jak typ zawierający element członkowski.

Oznacza to, że następujące przypadki nie są dozwolone:

interface I
{
    int Prop1 { get; }
}
public class Base
{
    public virtual int Prop2 { get; set; }

    protected required int _field; // Error: _field is not at least as visible as Base. Open question below about the protected constructor scenario

    public required readonly int _field2; // Error: required fields cannot be readonly
    protected Base() { }

    protected class Inner
    {
        protected required int PropInner { get; set; } // Error: PropInner cannot be set inside Base or Derived
    }
}
public class Derived : Base, I
{
    required int I.Prop1 { get; } // Error: explicit interface implementions cannot be required as they cannot be set in an object initializer

    public required override int Prop2 { get; set; } // Error: this property is hidden by Derived.Prop2 and cannot be set in an object initializer
    public new int Prop2 { get; }

    public required int Prop3 { get; } // Error: Required member must have a setter or initer

    public required int Prop4 { get; internal set; } // Error: Required member setter must be at least as visible as the constructor of Derived
}

Jest to błąd ukrywania elementu członkowskiego required, ponieważ użytkownik nie może go już ustawić.

Podczas zastępowania elementu członkowskiego required słowo kluczowe required musi zostać uwzględnione w podpisie metody. Dzieje się tak, abyśmy w przyszłości mogli umożliwić rezygnację z wymogu właściwości poprzez nadpisanie i mieli na to możliwość projektową.

Przesłonięcia mogą oznaczać członka required tam, gdzie nie był on required w typie bazowym. Członek oznaczony w ten sposób jest dodawany do wymaganej listy członków typu pochodnego.

Typy mogą zastępować wymagane właściwości wirtualne. Oznacza to, że jeśli podstawowa właściwość wirtualna ma pamięć, a typ pochodny próbuje odwołać się do implementacji podstawowej tej właściwości, mogą zauważyć niezainicjowaną pamięć. NB: Jest to ogólny antywzór języka C# i nie uważamy, że ta propozycja powinna podjąć próbę rozwiązania tego problemu.

Wpływ na analizę dopuszczaną do wartości null

Elementy członkowskie oznaczone required nie muszą być inicjowane do prawidłowego stanu dopuszczalnego do wartości null na końcu konstruktora. Wszystkie elementy required tego typu i wszystkie typy podstawowe są uznawane przez analizę dopuszczającą wartość null za domyślne na początku dowolnego konstruktora w tym typie, chyba że łączą się z konstruktorem this lub base, który jest oznaczony atrybutem SetsRequiredMembersAttribute.

Analiza dopuszczana do wartości null wyświetli ostrzeżenie o wszystkich elementach członkowskich required z bieżących i podstawowych typów, które nie mają prawidłowego stanu dopuszczalnego do wartości null na końcu konstruktora przypisanego SetsRequiredMembersAttribute.

#nullable enable
public class Base
{
    public required string Prop1 { get; set; }

    public Base() {}

    [SetsRequiredMembers]
    public Base(int unused) { Prop1 = ""; }
}
public class Derived : Base
{
    public required string Prop2 { get; set; }

    [SetsRequiredMembers]
    public Derived() : base()
    {
    } // Warning: Prop1 and Prop2 are possibly null.

    [SetsRequiredMembers]
    public Derived(int unused) : base()
    {
        Prop1.ToString(); // Warning: possibly null dereference
        Prop2.ToString(); // Warning: possibly null dereference
    }

    [SetsRequiredMembers]
    public Derived(int unused, int unused2) : this()
    {
        Prop1.ToString(); // Ok
        Prop2.ToString(); // Ok
    }

    [SetsRequiredMembers]
    public Derived(int unused1, int unused2, int unused3) : base(unused1)
    {
        Prop1.ToString(); // Ok
        Prop2.ToString(); // Warning: possibly null dereference
    }
}

Reprezentacja metadanych

Następujące 2 atrybuty są znane kompilatorowi języka C# i są wymagane do działania tej funkcji:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
    public sealed class RequiredMemberAttribute : Attribute
    {
        public RequiredMemberAttribute() {}
    }
}

namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
    public sealed class SetsRequiredMembersAttribute : Attribute
    {
        public SetsRequiredMembersAttribute() {}
    }
}

Jest to błąd ręcznego stosowania RequiredMemberAttribute do typu.

Każdy członek oznaczony required ma zastosowany RequiredMemberAttribute. Ponadto każdy typ definiujący takich członków jest oznaczony RequiredMemberAttributejako znacznik wskazujący, że w tym typie są wymagani członkowie. Należy pamiętać, że jeśli typ B pochodzi z A, a A definiuje członków required, ale B nie dodaje żadnych nowych ani nie przesłania żadnych istniejących członków required, B nie zostaną oznaczone RequiredMemberAttribute. Aby w pełni określić, czy w Bistnieją wymagani członkowie, należy sprawdzić pełną hierarchię dziedziczenia.

Każdy konstruktor w typie zawierającym elementy required, do których nie zastosowano SetsRequiredMembersAttribute, jest oznaczony dwoma atrybutami.

  1. System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute z nazwą funkcji "RequiredMembers".
  2. System.ObsoleteAttribute z ciągiem "Types with required members are not supported in this version of your compiler", a atrybut jest oznaczony jako błąd, aby zapobiec używaniu tych konstruktorów przez starsze kompilatory.

Nie używamy tutaj modreq, ponieważ jest to cel utrzymania zgodności binarnej: jeśli ostatnia właściwość required została usunięta z typu, kompilator nie będzie już syntetyzować tej modreq, która jest zmianą powodującą niezgodność binarną, a wszyscy konsumenci musieliby zostać ponownie skompilowani. Kompilator, który rozpoznaje członków required, zignoruje ten przestarzały atrybut. Należy pamiętać, że składowe mogą również pochodzić z typów podstawowych: nawet jeśli w bieżącym typie nie ma nowych elementów członkowskich required, jeśli jakikolwiek typ podstawowy ma required składowych, ten atrybut Obsolete zostanie wygenerowany. Jeśli konstruktor ma już atrybut Obsolete, nie zostanie wygenerowany żaden dodatkowy atrybut Obsolete.

Używamy zarówno ObsoleteAttribute, jak i CompilerFeatureRequiredAttribute, ponieważ ta ostatnia jest nową wersją, a starsze kompilatory tego nie rozumieją. W przyszłości możemy usunąć ObsoleteAttribute i/lub nie używać jej do ochrony nowych funkcji, ale na razie potrzebujemy obu tych funkcji w celu zapewnienia pełnej ochrony.

Aby utworzyć pełną listę elementów członkowskich requiredR dla danego typu T, w tym wszystkich typów podstawowych, jest uruchamiany następujący algorytm:

  1. Dla każdego Tb, zaczynając od T i przechodząc przez łańcuch typów podstawowych, aż do osiągnięcia object.
  2. Jeśli Tb jest oznaczony RequiredMemberAttribute, wszyscy członkowie Tb oznaczeni RequiredMemberAttribute są zbierani do Rb
    1. Dla każdego Ri w Rb, jeśli Ri jest zastępowany przez dowolnego członka R, jest pomijany.
    2. W przeciwnym razie jeśli jakikolwiek Ri jest ukryty przez członka R, wyszukiwanie wymaganych członków kończy się niepowodzeniem i nie zostaną podjęte żadne dalsze kroki. Wywołanie dowolnego konstruktora T bez atrybutu SetsRequiredMembers powoduje błąd.
    3. W przeciwnym razie Ri zostanie dodany do R.

Otwórz pytania

Inicjalizatory zagnieżdżonych członków

Jakie będą mechanizmy egzekucji dla zagnieżdżonych inicjatorów składowych? Czy będą one całkowicie niedozwolone?

class Range
{
    public required Location Start { get; init; }
    public required Location End { get; init; }
}

class Location
{
    public required int Column { get; init; }
    public required int Line { get; init; }
}

_ = new Range { Start = { Column = 0, Line = 0 }, End = { Column = 1, Line = 0 } } // Would this be allowed if Location is a struct type?
_ = new Range { Start = new Location { Column = 0, Line = 0 }, End = new Location { Column = 1, Line = 0 } } // Or would this form be necessary instead?

Omówione pytania

Poziom wymuszania dla klauzul init

Funkcja klauzuli init nie została zaimplementowana w języku C# 11. Pozostaje to aktywna propozycja.

Czy staramy się ściśle egzekwować, że członkowie określeni w klauzuli init bez inicjatora muszą zainicjować wszystkich członków? Wydaje się prawdopodobne, że tak, w przeciwnym razie tworzymy łatwy błąd, który prowadzi do porażki. Jednak ryzykujemy ponowne wprowadzenie tych samych problemów, które rozwiązaliśmy za pomocą MemberNotNull w języku C# 9. Jeśli chcemy ściśle to wymusić, prawdopodobnie będziemy potrzebować sposobu, aby metoda pomocnicza informowała, że ustawia członka. W tym celu omówiliśmy niektóre możliwe składnie:

  • Zezwalaj na metody init. Te metody mogą być wywoływane tylko z konstruktora lub innej metody init i mogą uzyskiwać dostęp do this tak, jakby był w konstruktorze (tj. ustawić readonly i init pola/właściwości). Można to połączyć z klauzulami init dla takich metod. Klauzula init byłaby uznawana za spełnioną, jeśli człon w klauzuli jest niewątpliwie przypisany w treści metody/konstruktora. Wywoływanie metody z klauzulą init, która zawiera element członkowski zlicza się jako przypisanie do tego elementu członkowskiego. Jeśli zdecydujemy, że jest to kierunek, który chcemy podjąć, teraz lub w przyszłości, prawdopodobnie nie powinniśmy używać init jako słowa kluczowego klauzuli inicjalizacji w konstruktorze, ponieważ byłoby to mylące.
  • Zezwól operatorowi ! na jawne pomijanie ostrzeżenia/błędu. Jeśli inicjujesz członka w skomplikowany sposób (na przykład we współdzielonej metodzie), użytkownik może dodać ! do sekcji init, aby wskazać, że kompilator nie powinien sprawdzać inicjalizacji.

Wniosek: Po przeprowadzonej dyskusji lubimy ideę operatora !. Pozwala to użytkownikowi być świadomym w bardziej skomplikowanych scenariuszach, a jednocześnie nie tworzy dużych braków projektowych wokół metod inicjowania i oznaczania każdej metody jako ustawiającej elementy X lub Y. ! został wybrany, ponieważ już używamy go do pomijania ostrzeżeń dopuszczających wartość null, a użycie go w celu przekazania kompilatorowi "Jestem mądrzejszy od ciebie" w innym miejscu jest naturalnym rozszerzeniem formy składniowej.

Wymagani członkowie interfejsu

Ta propozycja nie zezwala interfejsom na oznaczanie członków jako wymaganych. Chroni to nas przed koniecznością wypracowania złożonych scenariuszy dotyczących new() i ograniczeń interfejsu w generykach, jest to bezpośrednio związane z fabrykami oraz konstrukcją generyczną. Aby zapewnić, że mamy przestrzeń projektową w tym obszarze, zabrania się używania required w interfejsach oraz zabrania się zastępowania parametrów typu ograniczonych do typami z new(). Gdy chcemy przyjrzeć się szerszym scenariuszom budowy z fabrykami, możemy ponownie zapoznać się z tym problemem.

Pytania składniowe

Funkcja klauzuli init nie została zaimplementowana w języku C# 11. Pozostaje to aktywna propozycja.

  • Czy init odpowiednie słowo? init jako modyfikator postfiksowy w konstruktorze może przeszkadzać, gdybyśmy kiedykolwiek chcieli ponownie wykorzystać go dla fabryk, a także umożliwić metody init z modyfikatorem prefiksowym. Inne możliwości:
    • set
  • Czy required to właściwy modyfikator do określenia, że wszyscy członkowie są inicjowani? Inni zasugerowali:
    • default
    • all
    • Z ! aby wskazać złożoną logikę
  • Czy należy wymagać separatora między base/this a init?
    • : separator
    • Separator przecinek
  • Czy required właściwy modyfikator? Inne sugerowane alternatywy:
    • req
    • require
    • mustinit
    • must
    • explicit

Wniosek: Na razie usunęliśmy klauzulę konstruktora init i kontynuujemy z required jako modyfikatorem właściwości.

Ograniczenia klauzuli init

Funkcja klauzuli init nie została zaimplementowana w języku C# 11. Pozostaje to aktywna propozycja.

Czy należy zezwolić na dostęp do this w klauzuli inicjalizacyjnej? Jeśli chcemy, aby przypisanie w init było skrótem do przypisania członka w samym konstruktorze, wydaje się, że tak powinniśmy postąpić.

Ponadto czy tworzy nowy zakres, taki jak base(), czy ma taki sam zakres jak treść metody? Jest to szczególnie ważne w przypadku takich elementów jak funkcje lokalne, do których klauzula init może chcieć uzyskać dostęp lub w przypadku cieniowania nazw, jeśli wyrażenie init wprowadza zmienną za pośrednictwem parametru out.

wniosku : klauzula init została usunięta.

Wymagania dotyczące dostępności i init

Funkcja klauzuli init nie została zaimplementowana w języku C# 11. Pozostaje to aktywna propozycja.

W wersjach tej propozycji z klauzulą init omówiliśmy możliwość wystąpienia następującego scenariusza:

public class Base
{
    protected required int _field;

    protected Base() {} // Contract required that _field is set
}
public class Derived : Base
{
    public Derived() : init(_field = 1) // Contract is fulfilled and _field is removed from the required members list
    {
    }
}

Jednak w tym momencie usunęliśmy klauzulę init z wniosku, dlatego musimy zdecydować, czy zezwolić na ten scenariusz w ograniczony sposób. Dostępne opcje to:

  1. Nie zezwalaj na scenariusz. Jest to najbardziej konserwatywne podejście, a zasady w Accessibility są obecnie napisane z tym założeniem. Reguła polega na tym, że każdy wymagany człon składowy musi być co najmniej tak widoczny, jak jego typ zawierający.
  2. Wymagaj, aby wszystkie konstruktory były albo:
    1. Nie jest bardziej widoczny niż najmniej widoczny wymagany element.
    2. Należy zastosować SetsRequiredMembersAttribute do konstruktora. Zapewni to, że każdy, kto może zobaczyć konstruktor, może ustawić wszystkie eksportowane elementy lub nie ma nic do ustawienia. Może to być przydatne w przypadku typów, które są tworzone tylko za pośrednictwem metod statycznych Create lub podobnych konstruktorów, ale jego przydatność wydaje się być ogólnie ograniczona.
  3. Proszę ponownie dodać sposób usunięcia określonych części umowy do propozycji, zgodnie z wcześniejszymi ustaleniami w LDM.

Wniosek: Opcja 1, wszystkie wymagane elementy członkowskie muszą być co najmniej tak widoczne, jak ich typ zawierający.

Nadpisywanie reguł

Bieżąca specyfikacja mówi, że słowo kluczowe required musi zostać skopiowane i że przesłonięcia mogą sprawić, że członek stanie się bardziej wymagany, ale nie mniej. Czy to, co chcemy zrobić? Zezwolenie na usunięcie wymagań wymaga większej liczby możliwości modyfikacji kontraktu, niż obecnie proponujemy.

Wniosek: dodawanie required przy przesłonięciu jest dozwolone. Jeśli zastępowany element członkowski jest requiredto zastępujący element członkowski musi również być required.

Reprezentacja alternatywnych metadanych

Możemy również podejść inaczej do reprezentacji metadanych, inspirować się metodami rozszerzeń. Możemy użyć RequiredMemberAttribute na typie, aby wskazać, że typ zawiera wymagane elementy, a następnie użyć RequiredMemberAttribute na każdym wymaganym elemencie. Uprościłoby to sekwencję wyszukiwania (nie trzeba wykonywać wyszukiwania składowego, po prostu szukać członków, które mają atrybut).

Wniosek: Zatwierdzona alternatywa.

Reprezentacja metadanych

Należy zatwierdzić reprezentację metadanych. Ponadto musimy zdecydować, czy te atrybuty powinny być uwzględnione na liście BCL.

  1. W przypadku RequiredMemberAttributeten atrybut jest bardziej zbliżony do ogólnych osadzonych atrybutów, których używamy dla nazw elementów nullable/nint/krotek, i nie będzie stosowany ręcznie przez użytkownika w C#. Istnieje możliwość, że inne języki mogą chcieć ręcznie zastosować ten atrybut.
  2. SetsRequiredMembersAttribute, z drugiej strony, jest bezpośrednio używany przez konsumentów, a tym samym powinien znajdować się na liście BCL.

Jeśli przejdziemy na alternatywną reprezentację w poprzedniej sekcji, może to spowodować zmianę obliczeń dla RequiredMemberAttribute: zamiast przypominać ogólne wbudowane atrybuty dla nintnazwy opcjonalnych / nullowalnych składowych krotki, jest bliżej System.Runtime.CompilerServices.ExtensionAttribute, która jest obecna w platformie od wprowadzenia metod rozszerzeń.

: Wniosek: oba atrybuty zostaną umieszczone w liście BCL.

Ostrzeżenie a błąd

Czy brak ustawienia wymaganego elementu powinien być ostrzeżeniem czy błędem? Z pewnością można oszukać system za pośrednictwem Activator.CreateInstance(typeof(C)) lub podobnych, co oznacza, że możemy nie być w stanie w pełni zagwarantować, że wszystkie właściwości są zawsze ustawione. Pozwalamy również na wyłączanie diagnostyki w miejscu konstruktora przy użyciu !, co zwykle nie jest dozwolone w przypadku błędów. Jednak ta funkcja jest podobna do pól niezmiennych lub właściwości inicjalizacyjnych, ponieważ powodujemy błąd krytyczny, jeśli użytkownicy próbują ustawić taki członek po zainicjowaniu, ale można je obejść przez odbicie.

Wnioski: Błędy.