Zamknięte hierarchie

Kwestia dotycząca mistrza: https://github.com/dotnet/csharplang/issues/9499

Podsumowanie

Zezwalaj na deklarowanie closedklasy . Zapobiega to deklarowaniu bezpośrednio pochodnych klas w innym zestawie:

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

Ponieważ wszystkie klasy pochodne są deklarowane w zestawie zamkniętej klasy, wyrażenie zużywające switch , które obejmuje wszystkie z nich, można zakończyć do "wyczerpania" zamkniętej klasy - nie musi podawać domyślnego przypadku, aby uniknąć ostrzeżeń.

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

Motywacja

Wiele typów klas nie ma być rozszerzonych przez nikogo, ale ich autorów, ale język nie zapewnia możliwości wyrażenia tej intencji, nie mówiąc już o tym, aby się temu sprzeciwiać. Dla konsumentów klasy oznacza to, że żaden zestaw klas pochodnych nie zostanie uznany za "wyczerpany" klasę bazową, a wyrażenie przełącznika musi zawierać przypadek catch-all, aby uniknąć ostrzeżeń.

Zamknięte klasy zapewniają sposób wskazywania, że zestaw klas pochodnych jest kompletny i umożliwia korzystanie z kodu, aby polegać na tym w celu wyczerpującości w wyrażeniach przełącznika.

Szczegółowy projekt

Składnia

Zezwalaj closed jako modyfikator klas. Klasa closed jest niejawnie abstrakcyjna. W związku z tym nie może również mieć sealed modyfikatora ani static .

Jest to błąd jawnego użycia abstract modyfikatora w closed klasie.

Klasa pochodząca z zamkniętej klasy nie jest zamknięta, chyba że jawnie zadeklarowana jest.

Ograniczenie dotyczące tego samego zestawu

Jeśli klasa w jednym zestawie jest zadeklarowana closed , jest to błąd, aby bezpośrednio z niego pochodzić w innym zestawie:

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

To samo ograniczenie dotyczy modułów. Podtyp closed typu musi znajdować się w tym samym module co typ podstawowy.

Ograniczenie parametru typu

Jeśli klasa ogólna pochodzi bezpośrednio z zamkniętej klasy, wszystkie jej parametry typu muszą być używane w specyfikacji klasy bazowej:

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

Ta reguła polega na upewnieniu się, że istnieje pojedyncze ogólne wystąpienie typu pochodnego, który "wyczerpuje" daną ogólną wystąpienia typu bazy zamkniętej.

Uwaga: Ta reguła może nie być wystarczająca, jeśli w pewnym momencie zezwalamy na zamknięte interfejsy, ponieważ a) klasy mogą implementować wiele ogólnych wystąpień tego samego interfejsu, a b) parametry typu interfejsu mogą być współ- lub kontrawariantne. W takim momencie musimy uściślić regułę, aby nadal mieć pewność, że istnieje tylko jedno wystąpienie ogólne danego typu pochodnego na wystąpienie typu zamkniętego podstawowego.

Wyczerpującość przełączników

Wyrażenie switch , które obsługuje wszystkie bezpośrednie elementy potomne klasy zamkniętej, zostanie uznane za wyczerpane tej klasy. Oznacza to, że niektóre ostrzeżenia o braku wyczerpującości nie będą już wyświetlane:

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

Z drugiej strony oznacza to również, że może to być błąd, aby zamknięta klasa bazowa występowała jako przypadek po wszystkich jej bezpośrednich elementach potomnych:

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

Uwaga: Dla niektórych wystąpień ogólnych zamkniętej klasy bazowej może nie istnieć prawidłowe klasy pochodne. Wyczerpujący przełącznik musi określać tylko przypadki dla typów pochodnych, które są rzeczywiście możliwe.

Przykład:

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

Na C<string>przykład w przełączniku nie ma odpowiedniego D2<...>wystąpienia elementu , a w przełączniku nie ma żadnego przypadku D2<...> :

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

Wyczerpującość, gdy nie można użyć podtypu

Jeśli podtyp nie jest prawidłowy w określonej witrynie użycia, ze względu na naruszenia ograniczeń, naruszenia ułatwień dostępu lub inne przyczyny, nie można wyczerpać przełącznika za pośrednictwem podtypów.

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

Dotyczy to również sytuacji, gdy podtyp ogólny nie jest czytelny, a jego zastosowanie może zależeć od podstawienia argumentu końcowego typu.

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

Ograniczenia podtypu nie mają wpływu na wyczerpującość

Język nie uściśli określenia, czy podtyp jest możliwy na podstawie ograniczeń dotyczących parametrów typu w definicji typu podstawowego i podtypu.

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

Na przykład powyższe wyrażenia przełącznika nie analizują D2<X>konstrukcji wystarczająco dokładnie, aby zdać sobie sprawę, że wszystkie możliwe X naruszenia ograniczeń .U2 W związku z tym zakłada się, że niektóre z nich D2<X> są możliwe i prosi użytkownika o jego obsługę przez wyczerpanie typu podstawowego.

Wyczerpującość, gdy nie istnieją żadne podtypy

Gdy zamknięta klasa nie ma podtypów, pusty przełącznik nie jest traktowany jako wyczerpujący.

Uwagi: Przyjmuje się, że jest to "stan pośredni" w normalnym kodzie. Autor najprawdopodobniej wprowadzi zmianę w celu zadeklarowania podtypu w tym scenariuszu. To zachowanie jest "dziwaczne"- pomimo "wszystkich obsługiwanych podtypów 0", język nadal prosi użytkownika o obsługę typu podstawowego.

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

Wyczerpującość parametrów typu ograniczonych do typu zamkniętego

Parametr typu ograniczony do zamkniętej klasy jest traktowany podobnie jak zamknięta klasa do celów wyczerpujących kontroli.

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

Określanie podtypów zamkniętej klasy

Wyczerpującość przełączników dla zamkniętych typów klas jest określana przez sprawdzenie, czy przełącznik jest wyczerpujący w zestawie podtypów typu zamkniętej klasy wejściowej.

Zestaw podtypów S zamkniętej klasy jest określany w następujący sposób:

  1. Dla danego zamkniętego typu Cniech C₀ będzie oryginalną definicją.
  2. Dla każdej deklaracji podtypu S₀ , której typ podstawowy ma oryginalną definicję C₀, ustal, czy konstrukcja S istnieje, która ma typ Cpodstawowy .
  3. Jeśli taki element S istnieje, znajduje się w zestawie podtypów.

Konwertowanie interfejsu zamkniętych klas

Mówi się, że zamknięta klasa ma zapieczętowaną hierarchię, jeśli wszystkie jego podtypy są zapieczętowane lub mają zapieczętowaną hierarchię. Oznacza to, że wszystkie klasy w rozszerzonej hierarchii są zapieczętowane lub zamknięte.

Gdy zamknięta klasa ma zapieczętowaną hierarchię, zostanie wprowadzone ograniczenie konwersji interfejsu . Zapobiega to próbie konwersji na typ interfejsu, co nigdy nie mogło się powieść.

To ograniczenie jest podobne do jawnej konwersji odwołania z zapieczętowanego typu klasy do typu interfejsu. Zobacz §10.3.5 Jawne konwersje odwołań.

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

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

Określamy, czy jawna konwersja odwołania z C do I istnieje, rekursywnie zbierając zestaw interfejsów implementowanych przez C i jego podtypy. Jeśli zestaw interfejsów zawiera Ielement i C nie implementuje Imetody , jawna konwersja odwołania istnieje z C do I. (W przypadku, gdy C implementuje Ielement , zamiast tego dostępna jest niejawna konwersja odwołania).

Obniżenie

Zamknięte klasy są generowane za pomocą atrybutu IsClosedType , aby umożliwić ich rozpoznawanie przez kompilator zużywające.

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

Blokowanie podtypowania z innych języków/kompilatorów

Zamknięte klasy nie są dziedziczone z języków, które nie obsługują zamkniętych klas. Jest to realizowane przez dodanie [CompilerFeatureRequired("ClosedClasses")] do wszystkich konstruktorów zamkniętych klas.

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

Metadane "widok" elementu C1:

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

Należy pamiętać, że w przeciwieństwie do funkcji "wymaganych elementów członkowskich" element ObsoleteAttribute nie jest emitowany oprócz atrybutu CompilerFeatureRequiredAttribute. Emitowane są tylko te ostatnie.

Wiele atrybutów CompilerFeatureRequiredAttributes

W scenariuszu takim jak poniżej kompilator emituje oddzielną CompilerFeatureRequiredfunkcję , dla każdej wymaganej funkcji, która jest odpowiednia dla symbolu:

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

Złe strony

  • Może to być zmiana powodująca niezgodność, aby dodać closed modyfikator do istniejącej klasy lub dodać dodatkową klasę pochodną z zamkniętej klasy. Przed opublikowaniem zamkniętej klasy autor musi wziąć pod uwagę długoterminową umowę, która implikuje ze swoimi konsumentami.

Alternatives

  • Zamiast nowego closed modyfikatora można wyznaczyć zamkniętą klasę za pomocą atrybutu [Closed] .
  • Zakres, w którym obiekty potomne są dozwolone, można zawęzić dalej do pliku (chociaż nie miałoby to wielu precedensów w języku C#) lub wewnątrz treści zamkniętej klasy jako klas zagnieżdżonych.
  • Zamknięty zestaw dozwolonych elementów potomnych można podać jako listę zamiast sugerować, gdzie występują deklaracje. Pozwoliłoby to na włączenie klas w innych zestawach.

Funkcje opcjonalne

  • Interfejsy mogą być również zamykane. Reguły byłyby bardzo podobne.

Otwórz pytania

N/A