Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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:
- Dla danego zamkniętego typu
CniechC₀będzie oryginalną definicją. - Dla każdej deklaracji podtypu
S₀, której typ podstawowy ma oryginalną definicjęC₀, ustal, czy konstrukcjaSistnieje, która ma typCpodstawowy .- Zobacz też §19.6.3 Unikatowość implementowanych interfejsów w standardzie.
- Jeśli taki element
Sistnieje, 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ć
closedmodyfikator 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
closedmodyfikatora 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
C# feature specifications