Gesloten hiërarchieën

Kampioensprobleem: https://github.com/dotnet/csharplang/issues/9499

Overzicht

Toestaan dat een klasse wordt gedeclareerd closed. Hiermee voorkomt u dat rechtstreeks afgeleide klassen in een andere assembly worden gedeclareerd:

// Assembly 1
public closed record class GateState;
public record class Closed : GateState;
public record class Open(float Percent) : GateState;

// Assembly 2
public record class Locked : GateState; // ERROR - 'GateState' is a closed class

Aangezien alle afgeleide klassen worden gedeclareerd in de assembly van de gesloten klasse, kan een verbruiksexpressie switch die alle klassen omvat worden afgesloten om de gesloten klasse uit te putten. Het hoeft geen standaardcase te bieden om waarschuwingen te voorkomen.

// Assembly 3
GateState state = ...;
string description = state switch
{
    Closed => "closed",
    Open(var percent) => $"{percent}% open"
    // No warning about missing cases
}; 

Motivatie

Veel klassetypen zijn niet bedoeld om door iedereen maar hun auteurs te worden uitgebreid, maar de taal biedt geen manier om die intentie uit te drukken, laat staan om te voorkomen dat dit gebeurt. Voor consumenten van de klasse betekent dit dat er geen set afgeleide klassen wordt beschouwd als 'uitlaat' van de basisklasse en dat een switchexpressie een catch-all case moet bevatten om waarschuwingen te voorkomen.

Gesloten klassen bieden een manier om aan te geven dat een set afgeleide klassen is voltooid en dat het gebruik van code afhankelijk is van die voor volledigheid in switchexpressies.

Gedetailleerd ontwerp

Syntaxis

Toestaan closed als wijzigingsfunctie voor klassen. Een closed klasse is impliciet abstract. Het kan dus niet ook een sealed of static modifier hebben.

Het is een fout om expliciet een abstract wijzigingsfunctie voor een closed klasse te gebruiken.

Een klasse die is afgeleid van een gesloten klasse is niet zelf gesloten, tenzij deze expliciet is gedeclareerd.

Beperking voor dezelfde assembly

Als een klasse in de ene assembly wordt gedeclareerd closed , is het een fout om er rechtstreeks van af te leiden in een andere assembly:

// Assembly 1
public closed class CC { ... } 
public class CO : CC { ... }     // Ok, same assembly

// Assembly 2
public class C1 : CC { ... }     // Error, 'CC' is closed and in a different assembly
public class C2 : CO { ... }     // Ok, 'CO' is not closed

Dezelfde beperking geldt voor modules. Een subtype van een closed type moet zich in dezelfde module bevinden als het basistype.

Parameterbeperking typen

Als een algemene klasse rechtstreeks is afgeleid van een gesloten klasse, moeten alle typeparameters worden gebruikt in de specificatie van de basisklasse:

closed class C<T> { ... }
class D1<U> : C<U> { ... }   // Ok, 'U' is used in base class
class D2<V> : C<V[]> { ... } // Ok, 'V' is used in base class
class D3<W> : C<int> { ... } // Error, 'W' is not used in base class

Deze regel is om ervoor te zorgen dat er één algemene instantiëring van het afgeleide type is dat een bepaalde algemene instantiëring van het gesloten basistype 'uitputt'.

Opmerking: Deze regel is mogelijk niet voldoende als we op een bepaald moment gesloten interfaces toestaan, omdat a) klassen meerdere algemene instantiëringen van dezelfde interface kunnen implementeren en b) parameters van het interfacetype co- of contravariant kunnen zijn. Op dat moment moeten we de regel verfijnen om ervoor te zorgen dat er maar één algemene instantiering van een bepaald afgeleide type per algemene instantiëring van een gesloten basistype is.

Uitputtendheid in switches

Een switch expressie die alle directe afstammelingen van een gesloten klasse verwerkt, wordt beschouwd als uitgeput die klasse. Dit betekent dat er niet-volledigheidswaarschuwingen meer worden gegeven:

CC cc = ...;
_ = cc switch
{
    CO co => ...,
    // No warning about non-exhaustive switch
};

Aan de andere kant betekent dit ook dat het een fout kan zijn dat de gesloten basisklasse zich voordoet als een geval na al zijn directe afstammelingen:

_ = cc switch
{
    CO co => ...,
    CC cc => ..., // Error, case cannot be reached
};

Opmerking: Er bestaan mogelijk geen geldige afgeleide klassen voor bepaalde algemene instantiëringen van een gesloten basisklasse. Een volledige switch hoeft alleen gevallen op te geven voor afgeleide typen die daadwerkelijk mogelijk zijn.

Voorbeeld:

closed class C<T> { ... }
class D1<U> : C<U> { ... }
class D2<V> : C<V[]> { ... }

Er C<string>is bijvoorbeeld geen overeenkomstige instantiëring van D2<...>en hoeft niet D2<...> te worden gegeven in een switch:

C<string> cs = ...;
_ = cs switch
{
    D1<string> d1 => ...,
    // No need for a 'D2<...>' case - no instantiation corresponds to 'C<string>'
}

Volledigheid wanneer een subtype niet kan worden gebruikt

Als een subtype niet geldig is op een bepaalde gebruikssite, vanwege schendingen van beperkingen, toegankelijkheidsschendingen of andere redenen, is het niet mogelijk om de switch via subtypen uit te voeren.

closed class C;
class D1 : C;
class Container
{
    protected class D2 : C;
}

class Program
{
    int M(C c)
        => c switch
        {
            D1 => 1,
            // warning: switch is non-exhaustive. Pattern 'C' is not handled.
        };
}

Dit geldt ook wanneer een algemeen subtype niet kan worden uitgesproken en de toepasbaarheid ervan kan afhankelijk zijn van de vervanging van het uiteindelijke typeargument.

closed class C<T> { ... }
class D1<U> : C<U> { ... }
class D2<V> : C<V[]> { ... }

class Program
{
    int M<X>(C<X> c)
        => c switch
        {
            D1<X> => 1,
            // warning: switch is non-exhaustive. Pattern 'C' is not handled.
        };
}

Subtypebeperkingen hebben geen invloed op de volledigheid

De taal verfijnt niet de bepaling of een subtype mogelijk is op basis van beperkingen voor typeparameters in het basistype en de subtypedefinitie.

closed class C<T>;
class D1<U1> : C<U1>;
class D2<U2> : C<U2> where U2 : struct;

class Program
{
    int M1<X>(C<X> c) where X : class
    {
        // warning: switch is not exhaustive. Pattern 'C<X>' is not handled.
        return c switch
        {
            D1<X> => 1,
        };
    }

    int M2<X>(C<X> c) where X : class
    {
        return c switch
        {
            D1<X> => 1,
            C<X> => 2, // ok
        };
    }
}

De bovenstaande switchexpressies analyseren de constructie D2<X> bijvoorbeeld niet precies genoeg om te realiseren dat alle mogelijke X beperkingen U2van . Daarom wordt ervan uitgegaan dat sommige D2<X> mogelijk zijn en vraagt de gebruiker om het te verwerken door het basistype uit te putten.

Uitputtendheid wanneer er geen subtypen bestaan

Wanneer een gesloten klasse geen subtypen heeft, wordt een lege schakeloptie niet als volledig beschouwd.

Opmerkingen: Dit wordt ervan uitgegaan dat deze een "tussenliggende toestand" is in normale code. De auteur brengt waarschijnlijk een wijziging aan om een subtype in dit scenario te declareren. Dit gedrag is een 'quirk'- ondanks 'alle 0 subtypen die worden verwerkt', vraagt de taal de gebruiker nog steeds om het basistype te verwerken.

closed class C;

class Program
{
    int M1(C c)
        // warning: switch is not exhaustive.
        => c switch
        {
        };

    int M2(C c)
        => c switch
        {
            C => 1, // ok
        };
}

Volledigheid van typeparameters beperkt tot gesloten type

Een typeparameter die is beperkt tot een gesloten klasse, wordt op dezelfde manier behandeld als een gesloten klasse voor het uitvoeren van volledigheidscontroles.

closed class C;
class D1 : C;
class D2 : C;

class Program
{
    int M1<X>(X x) where X : C
        => x switch
        {
            D1 => 1,
            D2 => 2,
        };

    int M2<X>(X x) where X : C
        => x switch
        {
            D1 => 1,
            D2 => 2,
            C => 3, // error: 'C' is subsumed by the previous cases
        };
}

Subtypen van een gesloten klasse bepalen

De volledigheid van switches ten opzichte van gesloten klassetypen wordt bepaald door te controleren of de switch volledig is voor de set subtypen van het gesloten klassetype voor invoer.

De set subtypen S van een gesloten klasse wordt op de volgende manier bepaald:

  1. Voor een bepaald gesloten type Ckunt C₀ u de oorspronkelijke definitie zijn.
  2. Bepaal voor elke subtypedeclaratie S₀ waarvan het basistype de oorspronkelijke definitie C₀heeft, of er een constructie S bestaat die het basistype Cheeft.
  3. Als een dergelijke S bestaat, wordt deze opgenomen in de set subtypen.

Interface-conversie van gesloten klassen

Een gesloten klasse wordt gezegd een verzegelde hiërarchie te hebben, als alle subtypen zijn verzegeld of een verzegelde hiërarchie hebben. Dat wil gezegd, alle klassen in de uitgebreide hiërarchie zijn verzegeld of gesloten.

Wanneer een gesloten klasse een verzegelde hiërarchie heeft, wordt er een beperking voor de conversie van interface geïntroduceerd. Dit voorkomt dat een conversie naar interfacetype wordt uitgevoerd, wat nooit kan slagen.

Deze beperking is vergelijkbaar met expliciete verwijzingsconversie van een verzegeld klassetype naar interfacetype. Zie §10.3.5 Expliciete referentieconversies.

var c = new C();
var i = (I)c; // error

closed class C { }
sealed class D1 : C { }
sealed class D2 : C { }
interface I { }

We bepalen of de expliciete verwijzingsconversie van C naar I bestaat, door recursief de set interfaces te verzamelen die zijn geïmplementeerd door C en de bijbehorende subtypen. Als de set interfaces en IC deze niet implementeertI, bestaat de expliciete verwijzingsconversie van C naar I. (In het geval dat C wordt geïmplementeerd I, is in plaats daarvan een impliciete verwijzingsconversie beschikbaar.)

Verlaging

Gesloten klassen worden gegenereerd met een IsClosedType kenmerk, zodat ze kunnen worden herkend door een verbruikende compiler.

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

Subtyping van andere talen/compilers blokkeren

Gesloten klassen worden niet overgenomen van talen die geen ondersteuning bieden voor gesloten klassen. Dit wordt bereikt door alle constructors van gesloten klassen toe te voegen [CompilerFeatureRequired("ClosedClasses")] .

// Authoring assembly, built with .NET 10 SDK
closed class C1
{
    public C1() { }
    public C1(int param) { }
}

// Consuming assembly, built with .NET 8 SDK
class C2 : C1
{
    public C2() { } // error: 'C1.C1()' requires compiler feature "ClosedClasses"
    public C2() : base(42) { } // error: 'C1.C1(int)' requires compiler feature "ClosedClasses"
}

Metagegevensweergave van C1:

[IsClosedType]
class C1
{
    [CompilerFeatureRequired("ClosedClasses")]
    public C1() { }
    [CompilerFeatureRequired("ClosedClasses")]
    public C1(int param) { }
}

Houd er rekening mee dat in tegenstelling tot de functie 'vereiste leden' een ObsoleteAttribute niet wordt verzonden naast de CompilerFeatureRequiredAttribute. Alleen de laatste wordt verzonden.

Meerdere CompilerFeatureRequiredAttributes

In een scenario als het volgende verzendt de compiler een afzonderlijke, voor CompilerFeatureRequiredelke vereiste functie die relevant is voor het symbool:

closed class C1
{
    public C() { }
    public required string P { get; set; }
}

// Metadata:
class C1
{
    [Obsolete("Types with required members are not supported in this version of your compiler")]
    [CompilerFeatureRequired("RequiredMembers")]
    [CompilerFeatureRequired("ClosedClasses")]
    public C1() { }
}

Nadelen

  • Het kan een belangrijke wijziging zijn om een closed wijziging toe te voegen aan een bestaande klasse of om een extra afgeleide klasse toe te voegen van een gesloten klasse. Voordat een gesloten klasse wordt gepubliceerd, moet de auteur rekening houden met het langetermijncontract dat het met de consumenten impliceert.

Alternatives

  • In plaats van een nieuwe closed wijzigingsfunctie kan een gesloten klasse worden aangewezen met een [Closed] kenmerk.
  • Het bereik van waar afstammelingen zijn toegestaan, kan verder worden beperkt tot een bestand (hoewel dat niet veel broncellen in C#zou hebben) of naar binnen de hoofdtekst van de gesloten klasse als geneste klassen.
  • De gesloten set toegestane afstammelingen kan worden opgegeven als een lijst in plaats van geïmpliceerd door waar declaraties plaatsvinden. Hierdoor kunnen klassen in andere assembly's worden opgenomen.

Optionele onderdelen

  • Interfaces kunnen ook worden gesloten. De regels zouden vergelijkbaar zijn.

Open vragen

N/A