Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Problème de champion : https://github.com/dotnet/csharplang/issues/9499
Résumé
Autoriser la déclaration closedd’une classe . Cela empêche les classes dérivées directement d’être déclarées dans un autre assembly :
// 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
Étant donné que toutes les classes dérivées sont déclarées dans l’assembly de la classe fermée, une expression consommatrice switch qui les couvre peut être conclue à « épuiser » la classe fermée . Il n’est pas nécessaire de fournir un cas par défaut pour éviter les avertissements.
// Assembly 3
GateState state = ...;
string description = state switch
{
Closed => "closed",
Open(var percent) => $"{percent}% open"
// No warning about missing cases
};
Motivation
De nombreux types de classes ne sont pas destinés à être étendus par n’importe qui, mais leurs auteurs, mais la langue ne permet pas d’exprimer cette intention, et encore moins de se protéger contre ce qui se passe. Pour les consommateurs de la classe, cela signifie qu’aucun ensemble de classes dérivées ne sera considéré comme « épuiser » la classe de base, et qu’une expression switch doit inclure un cas catch-all pour éviter les avertissements.
Les classes fermées fournissent un moyen d’indiquer qu’un ensemble de classes dérivées est complet et permettre à l’utilisation du code de s’appuyer sur celui-ci pour l’exhaustivité dans les expressions de commutateur.
Conception détaillée
Syntaxe
Autoriser closed en tant que modificateur sur les classes. Une closed classe est implicitement abstraite. Ainsi, il ne peut pas également avoir un modificateur ou static un sealed modificateur.
Il s’agit d’une erreur d’utiliser explicitement un abstract modificateur sur une closed classe.
Une classe dérivant d’une classe fermée n’est pas elle-même fermée, sauf si elle est déclarée explicitement.
Restriction du même assembly
Si une classe d’un assembly est déclarée closed , il s’agit d’une erreur de dériver directement de celle-ci dans un autre 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
La même restriction s’applique aux modules. Un sous-type d’un closed type doit se trouver dans le même module que le type de base.
Restriction de paramètre de type
Si une classe générique dérive directement d’une classe fermée, tous ses paramètres de type doivent être utilisés dans la spécification de classe de base :
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
Cette règle consiste à s’assurer qu’il existe une instanciation générique unique du type dérivé qui « épuise » une instanciation générique donnée du type de base fermé.
Note: Cette règle peut ne pas suffire si nous autoriseons des interfaces fermées à un moment donné, car a) les classes peuvent implémenter plusieurs instanciations génériques de la même interface et b) les paramètres de type d’interface peuvent être covariants ou contravariants. À ce stade, nous devons affiner la règle pour continuer à garantir qu’il n’y a jamais une instanciation générique d’un type dérivé donné par instanciation générique d’un type de base fermé.
Exhaustive dans les commutateurs
Une switch expression qui gère tous les descendants directs d’une classe fermée est considérée comme ayant épuisé cette classe. Cela signifie que certains avertissements non exhaustifs ne seront plus donnés :
CC cc = ...;
_ = cc switch
{
CO co => ...,
// No warning about non-exhaustive switch
};
En revanche, cela signifie également qu’il peut s’agir d’une erreur pour que la classe de base fermée se produise en tant que cas après tous ses descendants directs :
_ = cc switch
{
CO co => ...,
CC cc => ..., // Error, case cannot be reached
};
Note: Il se peut qu’il n’existe pas de classes dérivées valides pour certaines instanciations génériques d’une classe de base fermée. Un commutateur exhaustif doit uniquement spécifier des cas pour les types dérivés qui sont réellement possibles.
Par exemple:
closed class C<T> { ... }
class D1<U> : C<U> { ... }
class D2<V> : C<V[]> { ... }
Par C<string>exemple, il n’existe aucune instanciation correspondante, D2<...>et aucun cas ne D2<...> doit être donné dans un commutateur :
C<string> cs = ...;
_ = cs switch
{
D1<string> d1 => ...,
// No need for a 'D2<...>' case - no instantiation corresponds to 'C<string>'
}
Exhaustive lorsqu’un sous-type ne peut pas être utilisé
Si un sous-type n’est pas valide sur un site d’utilisation particulier, en raison de violations de contraintes, de violations d’accessibilité ou d’autres raisons, il n’est pas possible d’épuiser le commutateur via des sous-types.
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.
};
}
Cela s’applique également lorsqu’un sous-type générique n’est pas parlant et que son applicabilité peut dépendre de la substitution de l’argument de type final.
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.
};
}
Les contraintes de sous-type n’affectent pas l’exhaustivité
Le langage n’affine pas la détermination de savoir si un sous-type est possible en fonction de contraintes sur les paramètres de type dans le type de base et la définition de sous-type.
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
};
}
}
Par exemple, les expressions de commutateur ci-dessus, n’analysent pas précisément la construction D2<X>, pour réaliser que toutes les contraintes possibles X violent les contraintes de U2. Par conséquent, il part du principe que certains D2<X> sont possibles et demande à l’utilisateur de le gérer en épuisant le type de base.
Exhaustive lorsqu’aucun sous-type n’existe
Lorsqu’une classe fermée n’a aucun sous-type, un basculement vide n’est pas considéré comme exhaustif.
Remarques : il s’agit d’un « état intermédiaire » dans le code normal. L’auteur apporte probablement une modification pour déclarer un sous-type dans ce scénario. Ce comportement équivaut à un « quirk » (malgré « tous les 0 sous-types gérés ») , la langue demande toujours à l’utilisateur de gérer le type de base.
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
};
}
Exhaustive des paramètres de type contraints au type fermé
Un paramètre de type limité à une classe fermée est traité de la même façon qu’une classe fermée à des fins de vérifications d’exhaustivité.
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
};
}
Détermination des sous-types d’une classe fermée
L’exhaustivité des commutateurs sur les types de classes fermées est déterminée en vérifiant si le commutateur est exhaustif sur l’ensemble de sous-types du type de classe fermée d’entrée.
L’ensemble de sous-types S d’une classe fermée est déterminé de la manière suivante :
- Pour un type
Cfermé donné, supposonsC₀sa définition d’origine. - Pour chaque déclaration
S₀de sous-type dont le type de base a une définitionC₀d’origine, déterminez si une constructionSexiste qui a le typeCde base .- Consultez également le §19.6.3 Unicité des interfaces implémentées dans la norme.
- S’il en existe un
S, il est inclus dans l’ensemble de sous-types.
Convertibilité de l’interface des classes fermées
Une classe fermée est dite avoir une hiérarchie scellée, si tous ses sous-types sont scellés ou ont une hiérarchie scellée. Autrement dit, toutes les classes de la hiérarchie développée sont scellées ou fermées.
Lorsqu’une classe fermée a une hiérarchie scellée, une restriction de convertibilité d’interface est introduite. Cela empêche toute tentative de conversion en type d’interface, qui ne peut jamais réussir.
Cette restriction est similaire dans la nature à la conversion de référence explicite d’un type de classe scellé en type d’interface. Consultez les conversions de référence explicites de §10.3.5.
var c = new C();
var i = (I)c; // error
closed class C { }
sealed class D1 : C { }
sealed class D2 : C { }
interface I { }
Nous déterminons si la conversion de référence explicite en CI existe, en collectant de manière récursive l’ensemble d’interfaces implémentées par C et ses sous-types. Si l’ensemble d’interfaces inclut , et C n’implémente Ipas , la conversion de référence explicite existe à Ipartir de C .I (Dans le cas qui C implémente I, une conversion de référence implicite est disponible à la place.)
Abaissement
Les classes fermées sont générées avec un IsClosedType attribut pour les permettre d’être reconnues par un compilateur consommateur.
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class IsClosedTypeAttribute : Attribute { }
}
Blocage des sous-saisies à partir d’autres langages/compilateurs
Les classes fermées ne doivent pas être héritées des langues qui ne prennent pas en charge les classes fermées. Pour ce faire, ajoutez [CompilerFeatureRequired("ClosedClasses")] à tous les constructeurs de classes fermées.
// 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"
}
Métadonnées « affichage » de C1:
[IsClosedType]
class C1
{
[CompilerFeatureRequired("ClosedClasses")]
public C1() { }
[CompilerFeatureRequired("ClosedClasses")]
public C1(int param) { }
}
Notez que contrairement à la fonctionnalité « membres obligatoires », un ObsolèteAttribute n’est pas émis en plus du CompilateurFeatureRequiredAttribute. Seul ce dernier est émis.
Multiple CompilerFeatureRequiredAttributes
Dans un scénario comme suit, le compilateur émet un élément distinct CompilerFeatureRequiredpour chaque fonctionnalité requise qui est pertinente pour le symbole :
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() { }
}
Inconvénients
- Il peut s’agir d’une modification cassant pour ajouter un
closedmodificateur à une classe existante ou pour ajouter une classe dérivée supplémentaire d’une classe fermée. Avant de publier une classe fermée, l’auteur doit prendre en compte le contrat à long terme qu’il implique avec ses consommateurs.
Autres solutions
- Au lieu d’un nouveau
closedmodificateur, une classe fermée peut être désignée avec un[Closed]attribut. - L’étendue de l’endroit où les descendants sont autorisés pourrait être réduite plus loin dans un fichier (bien que cela n’aurait pas beaucoup de précédent en C#) ou à l’intérieur du corps de la classe fermée en tant que classes imbriquées.
- L’ensemble fermé de descendants autorisés peut être donné sous la forme d’une liste au lieu d’être implicite par l’endroit où des déclarations se produisent. Cela permettrait l’inclusion de classes dans d’autres assemblys.
Fonctionnalités facultatives
- Les interfaces peuvent également être fermées. Les règles seraient très similaires.
Questions ouvertes
N/A
C# feature specifications