Dela via


Obligatoriska medlemmar

Note

Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.

Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader fångas i de relevanta anteckningarna från -språkkonstruktionsmötet (Language Design Meeting).

Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Champion-fråga: https://github.com/dotnet/csharplang/issues/3630

Sammanfattning

Det här förslaget lägger till ett sätt att ange att en egenskap eller ett fält måste anges under objektinitiering, vilket tvingar instansens skapare att ange ett initialt värde för medlemmen i en objektinitierare på skapandeplatsen.

Motivation

Objekthierarkier kräver idag mycket standardkod för att kunna överföra data över alla nivåer i hierarkin. Nu ska vi titta på en enkel hierarki med en Person som kan definieras i 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;
    }
}

Det pågår massor av upprepningar här:

  1. Vid roten av hierarkin måste typen av varje egenskap upprepas två gånger och namnet måste upprepas fyra gånger.
  2. På den härledda nivån måste typen av varje ärvd egenskap upprepas en gång och namnet måste upprepas två gånger.

Detta är en enkel hierarki med 3 egenskaper och 1 arvsnivå, men många verkliga exempel på dessa typer av hierarkier går många nivåer djupare och ackumulerar större och större antal egenskaper för att gå vidare när de gör det. Roslyn är en sådan kodbas, till exempel i de olika trädtyperna som gör våra CST och AST. Den här kapslingen är så omständlig att vi har kodgeneratorer för att generera konstruktorer och definitioner av dessa typer, och många kunder använder liknande metoder för problemet. C# 9 introducerar rekord, vilket för vissa scenarier kan förbättra detta:

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

recordeliminerar den första dupliceringskällan, men den andra dupliceringskällan förblir oförändrad: tyvärr är detta källan till duplicering som växer när hierarkin växer och är den mest smärtsamma delen av dupliceringen att åtgärda efter att ha gjort en ändring i hierarkin eftersom den krävde att jaga hierarkin genom alla dess platser, till och med mellan projekt och potentiellt icke-bakåtkompatibla konsumenter.

Som en lösning för att undvika den här dupliceringen har vi länge sett konsumenter som använder objektinitierare som ett sätt att undvika att skriva konstruktorer. Före C# 9 hade detta dock 2 stora nackdelar:

  1. Objekthierarkin måste vara helt föränderlig, med set-åtkomst på varje egenskap.
  2. Det finns inget sätt att se till att varje instansiering av ett objekt från grafen anger varje medlem.

C# 9 tog återigen upp det första problemet här genom att introducera init-accessorn: med det kan dessa egenskaper anges vid skapande/initiering av objekt, men inte senare. Men vi har fortfarande det andra problemet: egenskaperna i C# har varit valfria sedan C# 1.0. Nullbara referenstyper, som introducerades i C# 8.0, åtgärdade en del av det här problemet: om en konstruktor inte initierar en icke-nullbar referenstypsegenskap varnas användaren om det. Detta löser dock inte problemet: här vill användaren inte upprepa stora delar av sin typ i konstruktorn, de vill skicka vidare -krav för att ställa in egenskaper hos sina konsumenter. Det ger inte heller några varningar om ID från Student, eftersom det är en värdetyp. Dessa scenarier är mycket vanliga i databasmodell-ORM:er, till exempel EF Core, som måste ha en offentlig parameterlös konstruktor men sedan driva nullabiliteten för raderna baserat på egenskapernas nullbarhet.

Det här förslaget syftar till att åtgärda dessa problem genom att införa en ny funktion för C#: obligatoriska medlemmar. Obligatoriska medlemmar måste initieras av konsumenter, snarare än av typförfattaren, med olika anpassningar för att möjliggöra flexibilitet för flera konstruktorer och andra scenarier.

Detaljerad design

class, structoch record typer får möjlighet att deklarera en required_member_list. Den här listan innehåller alla egenskaper och fält av en typ som anses nödvändigaoch som måste initieras under konstruktionen och initialiseringen av en instans av typen. Typer ärver dessa listor från sina basklasser automatiskt, vilket ger en sömlös upplevelse som eliminerar standardkod och repetitiv kod.

required modifierare

Vi lägger till 'required' i listan över modifierare i field_modifier och property_modifier. required_member_list av en typ består av alla medlemmar som har required tillämpat på dem. Därför ser den Person typen från tidigare nu ut så här:

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

Alla konstruktorer av en typ som har en required_member_list uttrycker automatiskt ett kontrakt som innebär att användare av typen måste initiera alla egenskaper i listan. Det är ett fel för en konstruktor att annonsera ett kontrakt som kräver en medlem som inte är minst lika tillgänglig som konstruktorn själv. Till exempel:

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 är endast giltigt i typerna class, structoch record. Det är inte giltigt för interface-typer. required kan inte kombineras med följande modifierare:

  • fixed
  • ref readonly
  • ref
  • const
  • static

required tillåts inte tillämpas på indexerare.

Kompilatorn utfärdar en varning när Obsolete tillämpas på en obligatorisk medlem av en typ och:

  1. Typen är inte markerad som Obsoleteeller
  2. Konstruktörer som inte har attributet SetsRequiredMembersAttribute är inte markerade med Obsolete.

SetsRequiredMembersAttribute

Alla konstruktorer i en typ med nödvändiga medlemmar, eller vars bastyp specificerar nödvändiga medlemmar, måste ha dessa medlemmar inställda av användaren när konstruktorn anropas. För att undanta konstruktorer från detta krav kan en konstruktor hänföras till SetsRequiredMembersAttribute, vilket tar bort dessa krav. Konstruktorns brödtext verifieras inte för att säkerställa att den definitivt anger de nödvändiga medlemmarna av typen.

SetsRequiredMembersAttribute tar bort alla krav från en konstruktor, och dessa krav kontrolleras inte överhuvudtaget för giltighet. Obs! Detta är utvägen om det är nödvändigt att ärva från en typ med en ogiltig obligatorisk medlemslista: markera konstruktorn för den typen med SetsRequiredMembersAttribute, och inga fel kommer att rapporteras.

Om en konstruktor C kedjar vidare till en base- eller this-konstruktor som är markerad med SetsRequiredMembersAttribute, måste C också markeras med SetsRequiredMembersAttribute.

För posttyper kommer vi att generera SetsRequiredMembersAttribute på den syntetiserade kopieringskonstruktorn för en post om posttypen eller någon av dess bastyper har obligatoriska medlemmar.

Obs! En tidigare version av det här förslaget hade en större metalanguage kring initiering, vilket gjorde det möjligt att lägga till och ta bort enskilda obligatoriska medlemmar från en konstruktor, samt validering av att konstruktorn angav alla nödvändiga medlemmar. Detta ansågs vara för komplext för den första versionen och togs bort. Vi kan titta på hur du lägger till mer komplexa kontrakt och ändringar som en senare funktion.

Tillämpning

För varje konstruktor Ci i typen T med nödvändiga medlemmar Rmåste användare som anropar Ci göra ett av följande:

  • Ange alla medlemmar i R i en object_initializerobject_creation_expression,
  • Eller ange alla medlemmar i R via avsnittet named_argument_list i en attribute_target.

såvida inte Ci tillskrivs SetsRequiredMembers.

Om den aktuella kontexten inte tillåter en object_initializer eller inte är en attribute_target, och Ci inte tillskrivs SetsRequiredMembers, är det ett fel att anropa Ci.

new() begränsning

En typ med en parameterlös konstruktor som annonserar ett kontrakt inte får ersättas med en typparameter som är begränsad till new()eftersom det inte finns något sätt för den allmänna instansieringen att säkerställa att kraven uppfylls.

struct defaults

Obligatoriska medlemmar gäller inte för instanser av struct-typer som är skapade med default eller default(StructType). De tillämpas för struct instanser som skapats med new StructType(), även om StructType inte har någon parameterlös konstruktor och standardkonstruktionskonstruktorn används.

Tillgänglighet

Det är ett fel att göra en medlem obligatorisk om medlemen inte kan anges i någon kontext där den innehållande typen synliggörs.

  • Om medlemmen är ett fält kan det inte vara readonly.
  • Om medlemmen är en egenskap måste den ha en setter eller initer som är minst lika tillgänglig som medlemmens innehållande typ.

Det innebär att följande fall inte tillåts:

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
}

Det är ett fel att dölja en required medlem, eftersom den medlemmen inte längre kan anges av en konsument.

När du åsidosätter en required medlem måste nyckelordet required inkluderas i metodsignaturen. Detta görs så att vi i framtiden, om vi någonsin vill tillåta att en egenskap blir frivillig med en åsidosättning, har vi designutrymme för att göra det.

Överskridningar tillåts markera en medlem required om det inte var required i bastypen. En medlem som är så markerad läggs till i listan med obligatoriska medlemmar av den härledda typen.

Typer kan åsidosätta nödvändiga virtuella egenskaper. Det innebär att om den virtuella basegenskapen har lagring och den härledda typen försöker komma åt den grundläggande implementeringen av den egenskapen kan de observera eninitierad lagring. Obs! Detta är ett allmänt C#-antimönster och vi anser inte att det här förslaget bör försöka åtgärda det.

Effekt på nullbar analys

Medlemmar som är markerade required behöver inte initieras till ett giltigt null-tillstånd i slutet av en konstruktor. Alla required-medlemmar från den här typen och alla bastyper anses av nullbarhetsanalys vara förvalda i början av vilken konstruktor som helst i den typen, om det inte länkas till en this- eller base-konstruktor som tillskrivs SetsRequiredMembersAttribute.

Nullbar analys varnar för alla required medlemmar från de aktuella och grundläggande typerna som inte har ett giltigt null-tillstånd i slutet av en konstruktor som tillskrivs 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
    }
}

Metadata representation

Följande två attribut är kända för C#-kompilatorn och krävs för att funktionen ska fungera:

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

Det är ett fel att manuellt tillämpa RequiredMemberAttribute på en typ.

Varje medlem som är markerad med required har en RequiredMemberAttribute tillämpad på sig. Dessutom markeras alla typer som definierar sådana medlemmar med RequiredMemberAttribute, som en markör som anger att det finns nödvändiga medlemmar i den här typen. Observera att om typen B härleds från Aoch A definierar required medlemmar, men B inte lägger till några nya eller åsidosätter befintliga required medlemmar, markeras B inte med en RequiredMemberAttribute. Om du vill ta reda på om det finns några obligatoriska medlemmar i Bmåste du kontrollera den fullständiga arvshierarkin.

Någon konstruktor i en typ med required medlemmar som inte har SetsRequiredMembersAttribute tillämpat på sig markeras med två attribut:

  1. System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute med funktionsnamnet "RequiredMembers".
  2. System.ObsoleteAttribute med strängen "Types with required members are not supported in this version of your compiler"och attributet markeras som ett fel för att förhindra att äldre kompilatorer använder dessa konstruktorer.

Vi använder inte en modreq här eftersom det är ett mål att upprätthålla binär kompatibilitet: om den sista required egenskapen togs bort från en typ skulle kompilatorn inte längre syntetisera den här modreq, vilket är en binärbrytande ändring och alla konsumenter skulle behöva kompileras om. En kompilator som förstår required medlemmar ignorerar det här föråldrade attributet. Observera att medlemmar också kan komma från bastyper: även om det inte finns några nya required medlemmar i den aktuella typen genereras det här required attributet om någon bastyp har Obsolete medlemmar. Om konstruktorn redan har ett Obsolete-attribut genereras inga ytterligare Obsolete attribut.

Vi använder både ObsoleteAttribute och CompilerFeatureRequiredAttribute eftersom den senare är ny den här versionen och äldre kompilatorer inte förstår den. I framtiden kanske vi kan släppa ObsoleteAttribute och/eller inte använda den för att skydda nya funktioner, men för tillfället behöver vi båda för fullständigt skydd.

Om du vill skapa en fullständig lista över required medlemmar R för en viss typ T, inklusive alla bastyper, körs följande algoritm:

  1. För varje Tbbörjar du med T och arbetar genom bastypskedjan tills object har nåtts.
  2. Om Tb har markerats med RequiredMemberAttributesamlas alla medlemmar i Tb markerade med RequiredMemberAttribute in i Rb
    1. För varje Ri i Rb, hoppas Ri över om den åsidosätts av någon medlem i R.
    2. Annars, om någon Ri döljs av en medlem i R, misslyckas sökningen av nödvändiga medlemmar och inga ytterligare åtgärder vidtas. Att anropa någon konstruktor av T utan attributet SetsRequiredMembers orsakar ett fel.
    3. Annars läggs Ri till i R.

Öppna frågor

Kapslade medlemsinitierare

Vilka kommer tillsynsmekanismerna för kapslade medlemsinitialiseringar att vara? Kommer de inte att tillåtas helt och hållet?

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?

Diskuterade frågor

Tillämpningsnivå för init-satser

Funktionen init-satsen implementerades inte i C# 11. Det är fortfarande ett aktivt förslag.

Framtvingar vi strikt att medlemmar som anges i en init-sats utan initierare måste initiera alla medlemmar? Det verkar troligt att vi gör det, annars skapar vi en enkel felgrop. Men vi riskerar också att återinföra samma problem som vi löst med MemberNotNull i C# 9. Om vi vill tillämpa detta strikt behöver vi förmodligen ett sätt för en hjälpmetod att visa att den sätter en komponent. Några möjliga syntaxer som vi har diskuterat för detta:

  • Tillåt init metoder. Dessa metoder kan bara anropas från en konstruktor eller från en annan init-metod och kan komma åt this som om den finns i konstruktorn (dvs. ange readonly och init fält/egenskaper). Detta kan kombineras med init-satser på sådana metoder. En init-klausul skulle anses vara uppfylld om medlemmen i satsen definitivt tilldelas i brödtexten i metoden/konstruktorn. Att anropa en metod med en init-klass som inkluderar en medlem räknas som en tilldelning till den medlemmen. Om vi bestämmer oss för att detta är en väg vi vill följa, nu eller i framtiden, verkar det rimligt att vi inte bör använda init som nyckelord för init-klausulen i en konstruktor, eftersom det kan skapa förvirring.
  • Tillåt att !-operatorn uttryckligen utelämnar varningen/felet. Om du initierar en medlem på ett komplicerat sätt (till exempel i en delad metod) kan användaren lägga till en ! i init-satsen för att indikera att kompilatorn inte bör söka efter initiering.

Slutsats: Efter diskussion gillar vi idén om ! operatören. Det gör att användaren kan agera medvetet i mer komplicerade scenarier utan att skapa ett stort designhål runt init-metoder och att märka varje metod som att ange medlemmar X eller Y. ! valdes eftersom vi redan använder det för att undertrycka nullbara varningar och att använda det för att berätta för kompilatorn "Jag är smartare än du" på en annan plats är en naturlig förlängning av syntaxformen.

Nödvändiga gränssnittsmedlemmar

Det här förslaget tillåter inte att gränssnitt markerar medlemmar efter behov. Detta skyddar oss från att behöva ta reda på komplexa scenarier kring new() och gränssnittsbegränsningar i generiska objekt just nu och är direkt relaterade till både fabriker och allmän konstruktion. För att säkerställa att vi har designutrymme i det här området förbjuder vi required i gränssnitt och förbjuder typer med required_member_lists från att ersättas med typparametrar som är begränsade till new(). När vi vill ta en bredare titt på allmänna byggscenarier med fabriker kan vi gå tillbaka till det här problemet.

Syntaxfrågor

Funktionen init-satsen implementerades inte i C# 11. Det är fortfarande ett aktivt förslag.

  • Är init rätt ord? init som en postfixmodifierare på konstruktorn kan störa om vi vill återanvända den för fabriker i framtiden och även aktivera init-metoder med en prefixmodifierare. Andra möjligheter:
    • set
  • Är required korrekt modifierare för att ange att alla element initieras? Andra föreslog:
    • default
    • all
    • Med ett utropstecken! för att ange komplex logik
  • Ska vi kräva en avgränsare mellan base/this och init?
    • : avgränsare
    • '', avgränsare
  • Är required rätt modifierare? Andra alternativ som har föreslagits:
    • req
    • require
    • mustinit
    • must
    • explicit

Slutsats: Vi har tagit bort init konstruktorsatsen temporärt och vi fortsätter att använda required som egenskapsmodifierare.

Begränsningar för Init-satser

Funktionen init-satsen implementerades inte i C# 11. Det är fortfarande ett aktivt förslag.

Ska vi tillåta åtkomst till this i init-satsen? Om vi vill att tilldelningen i init ska vara en förkortning för att tilldela medlemmen i själva konstruktorn verkar det som om vi borde göra det.

Skapar den dessutom ett nytt omfång, som base() gör, eller delar det samma omfång som metodtexten? Detta är särskilt viktigt för saker som lokala funktioner, som init-satsen kanske vill komma åt, eller för namnskuggning, om ett init-uttryck introducerar en variabel via en out-parameter.

Slutsats: init-satsen har tagits bort.

Tillgänglighetskrav och init

Funktionen init-satsen implementerades inte i C# 11. Det är fortfarande ett aktivt förslag.

I versioner av det här förslaget med init-satsen talade vi om att kunna ha följande scenario:

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

Vi har dock tagit bort init-klausulen från förslaget just nu, så vi måste besluta om vi ska tillåta detta scenario på ett begränsat sätt. Alternativen vi har är:

  1. Tillåt inte scenariot. Detta är den mest konservativa metoden, och reglerna i Accessibility är för närvarande skrivna med detta antagande i åtanke. Regeln är att en medlem som krävs måste vara minst lika synlig som sin innehållande typ.
  2. Kräv att alla konstruktorer antingen är:
    1. Inte mer synlig än den minst synliga obligatoriska medlemmen.
    2. Låt SetsRequiredMembersAttribute tillämpas på konstruktorn. Dessa skulle se till att alla som kan se en konstruktor antingen kan ange alla de saker som exporteras eller att det inte finns något att ange. Detta kan vara användbart för typer som bara skapas via statiska Create metoder eller liknande byggare, men verktyget verkar totalt sett begränsat.
  3. Läste ett sätt att ta bort specifika delar av kontraktet till förslaget, enligt beskrivningen i LDM- tidigare.

Slutsats: Alternativ 1, alla nödvändiga medlemmar måste vara minst lika synliga som deras innehållande typ.

Åsidosätt regler

Den nuvarande specifikationen säger att nyckelordet required måste kopieras över och att åsidosättningar kan göra att en medlem krävs mer, men inte mindre. Är det vad vi vill göra? Att tillåta borttagning av krav kräver mer kapacitet för kontraktsändring än vad vi för närvarande föreslår.

Slutsats: Att lägga till required vid åsidosättning är tillåtet. Om den åsidosatta medlemmen är requiredmåste den överdrivande medlemmen också vara required.

Alternativ metadatarepresentation

Vi kan också använda en annan metod för att representera metadata och inspireras av koncept från tilläggsmetoder. Vi kan placera en RequiredMemberAttribute på typen för att ange att typen innehåller nödvändiga medlemmar och sedan lägga en RequiredMemberAttribute på varje medlem som krävs. Detta skulle förenkla uppslagssekvensen (du behöver inte göra medlemssökning, leta bara efter medlemmar med attributet).

Slutsats: Alternativ godkänd.

Metadata representation

Den metadata-representationen måste godkännas. Vi måste också bestämma om dessa attribut ska ingå i BCL:n.

  1. För RequiredMemberAttributeär det här attributet mer likt de allmänna inbäddade attribut som vi använder för nullable/nint/tuppelns medlemsnamn och tillämpas inte manuellt av användaren i C#. Det är dock möjligt att andra språk vill tillämpa det här attributet manuellt.
  2. SetsRequiredMembersAttribute, å andra sidan, används direkt av konsumenterna och bör därför sannolikt finnas i BCL.

Om vi använder den alternativa representationen i föregående avsnitt kan det ändra kalkylen för RequiredMemberAttribute: i stället för att likna de allmänna inbäddade attributen för nint/nullable/tuppelns medlemsnamn är det närmare System.Runtime.CompilerServices.ExtensionAttribute, som har funnits i ramverket sedan tilläggsmetoderna levererades.

Slutsats: Vi placerar båda attributen i BCL:n.

Varning kontra fel

Ska det inte vara en varning eller ett fel att inte ange en obligatorisk medlem? Det är säkert möjligt att lura systemet, via Activator.CreateInstance(typeof(C)) eller liknande, vilket innebär att vi kanske inte helt kan garantera att alla egenskaper alltid är inställda. Vi tillåter även undertryckning av diagnostik på konstruktorplatsen med hjälp av !, som vi vanligtvis inte tillåter för fel. Funktionen liknar dock skrivskyddade fält eller init-egenskaper, eftersom vi ger ett hårt felmeddelande om användarna försöker ange en sådan medlem efter initieringen, men de kan kringgås genom reflection.

slutsats: Fel.