Priorité de résolution de surcharge
Remarque
Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Il inclut les modifications de spécification proposées, ainsi que les informations nécessaires pendant la conception et le développement de la fonctionnalité. Ces articles sont publiés jusqu’à ce que les modifications de spécification proposées soient finalisées et incorporées dans la spécification ECMA actuelle.
Il peut y avoir des différences entre la spécification de la fonctionnalité et l’implémentation terminée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).
Vous pouvez en savoir plus sur le processus d’adoption des speclets de fonctionnalités dans la norme de langage C# dans l’article sur les spécifications .
Problème de champion : https://github.com/dotnet/csharplang/issues/7706
Résumé
Nous introduisons un nouvel attribut, System.Runtime.CompilerServices.OverloadResolutionPriority
, qui peut être utilisé par les auteurs d’API pour ajuster la priorité relative des surcharges au sein d’un type unique comme moyen de diriger les consommateurs d’API pour utiliser des API spécifiques, même si ces API seraient normalement considérées comme ambiguës ou non choisies par les règles de résolution de surcharge de C#.
Motivation
Les auteurs d’API rencontrent souvent le problème de ne pas savoir quoi faire d'un membre une fois qu'il est devenu obsolète. À des fins de rétrocompatibilité, beaucoup garderont le membre existant avec ObsoleteAttribute
défini sur une erreur de manière permanente, afin d’éviter de perturber les consommateurs qui mettent à niveau les binaires lors de l’exécution. Cela atteint particulièrement les systèmes de plug-in, où l’auteur d’un plug-in ne contrôle pas l’environnement dans lequel le plug-in s’exécute. Le créateur de l’environnement peut vouloir conserver une méthode plus ancienne présente, mais bloquer l’accès à celui-ci pour tout code nouvellement développé. Toutefois, ObsoleteAttribute
par lui-même n’est pas suffisant. Le type ou le membre est toujours visible dans la résolution de surcharge, ce qui peut provoquer des défaillances de résolution de surcharge indésirables lorsqu'il existe une alternative parfaitement correcte. Cependant, cette alternative peut être ambiguë avec un membre devenu obsolète, ou bien la présence de ce membre obsolète provoque la fin prématurée de la résolution de surcharge sans jamais examiner le bon membre. À cet effet, nous voulons avoir un moyen pour les auteurs d’API de guider la résolution de surcharge sur la résolution de l’ambiguïté, afin qu’ils puissent évoluer leurs zones de surface d’API et diriger les utilisateurs vers des API performantes sans avoir à compromettre l’expérience utilisateur.
L’équipe BCL (Base Class Libraries) a plusieurs exemples d’endroits où cela peut s’avérer utile. Voici quelques exemples (hypothétiques) :
- Création d’une surcharge de
Debug.Assert
qui utiliseCallerArgumentExpression
pour obtenir l’expression déclarée, afin qu’elle puisse être incluse dans le message et la rendre préférable à la surcharge existante. - Faire de
string.IndexOf(string, StringComparison = Ordinal)
le préféré par rapport àstring.IndexOf(string)
. Cela devrait être discuté comme un changement majeur potentiel, mais certains pensent que c'est la meilleure valeur par défaut et qu'elle est plus probablement ce que l'utilisateur avait en tête. - Une combinaison de cette proposition et de
CallerAssemblyAttribute
permettrait aux méthodes avec une identité d’appelant implicite d’éviter les parcours de pile onéreux.Assembly.Load(AssemblyName)
le fait aujourd’hui, et cela pourrait être beaucoup plus efficace. Microsoft.Extensions.Primitives.StringValues
expose une conversion implicite enstring
et enstring[]
. Cela signifie qu’il présente une ambiguïté lorsqu’il est transmis à une méthode avec les surchargesparams string[]
etparams ReadOnlySpan<string>
. Cet attribut peut être utilisé pour hiérarchiser l’une des surcharges pour empêcher l’ambiguïté.
Conception détaillée
Priorité de résolution de surcharge
Nous définissons un nouveau concept, overload_resolution_priority, qui est utilisé pendant le processus de résolution d’un groupe de méthodes. overload_resolution_priority est une valeur entière de 32 bits. Toutes les méthodes ont une priorité_résolution_surcharge de 0 par défaut, et cela peut être modifié en appliquant OverloadResolutionPriorityAttribute
à une méthode. Nous mettons à jour la section §12.6.4.1 de la spécification C# comme suit (changement dans gras) :
Une fois que les membres de la fonction candidate et la liste d’arguments ont été identifiés, la sélection du meilleur membre de fonction est la même dans tous les cas :
- Tout d’abord, l’ensemble de membres de la fonction candidate est réduit à ceux qui s’appliquent à la liste d’arguments donnée (§12.6.4.2). Si cet ensemble réduit est vide, une erreur compile-time se produit.
- Ensuite, l’ensemble réduit de membres candidats est regroupé en déclarant le type. Au sein de chaque groupe :
- Les membres de la fonction candidate sont classés par overload_resolution_priority. Si le membre est une substitution, alors overload_resolution_priority provient de la déclaration la moins dérivée de ce membre.
- Tous les membres ayant une overload_resolution_priority inférieure à celle la plus élevée trouvée dans son groupe de types déclarants sont supprimés.
- Les groupes réduits sont ensuite combinés dans l’ensemble final des membres de la fonction candidate applicable.
- Ensuite, le meilleur membre de fonction parmi l’ensemble des membres de fonctions candidats applicables est localisé. Si l'ensemble contient un seul membre de fonction, alors celui-ci est le meilleur. Sinon, le meilleur membre de fonction est le membre de fonction qui est meilleur que tous les autres membres de fonction en ce qui concerne la liste d’arguments donnée, à condition que chaque membre de fonction soit comparé à tous les autres membres de fonction à l’aide des règles dans §12.6.4.3. S'il n'y a pas exactement un membre de fonction qui est meilleur que tous les autres membres de fonction, alors l'invocation du membre de fonction est ambiguë et une erreur de liaison se produit.
Par exemple, cette fonctionnalité entraînerait l’impression de l’extrait de code suivant « Span » au lieu de « Array » :
using System.Runtime.CompilerServices;
var d = new C1();
int[] arr = [1, 2, 3];
d.M(arr); // Prints "Span"
class C1
{
[OverloadResolutionPriority(1)]
public void M(ReadOnlySpan<int> s) => Console.WriteLine("Span");
// Default overload resolution priority
public void M(int[] a) => Console.WriteLine("Array");
}
L’effet de cette modification est que, comme l’élagage pour les types les plus dérivés, nous ajoutons un élagage final pour la priorité de résolution de surcharge. Étant donné que cet élagage se produit à la toute fin du processus de résolution de surcharge, cela signifie qu’un type de base ne peut pas rendre ses membres plus prioritaires que n’importe quel type dérivé. Cela est intentionnel et empêche une course aux armes de se produire où un type de base peut toujours essayer d’être mieux qu’un type dérivé. Par exemple:
using System.Runtime.CompilerServices;
var d = new Derived();
d.M([1, 2, 3]); // Prints "Derived", because members from Base are not considered due to finding an applicable member in Derived
class Base
{
[OverloadResolutionPriority(1)]
public void M(ReadOnlySpan<int> s) => Console.WriteLine("Base");
}
class Derived : Base
{
public void M(int[] a) => Console.WriteLine("Derived");
}
Les nombres négatifs sont autorisés à être utilisés et peuvent être utilisés pour marquer une surcharge spécifique comme pire que toutes les autres surcharges par défaut.
La overload_resolution_priority d’un membre provient de la déclaration la moins dérivée de ce membre. La priorité de résolution de surcharge n’est pas héritée ni déduite des membres d’interface qu’un membre de type peut implémenter. De plus, pour un membre Mx
qui implémente un membre d’interface Mi
, aucun avertissement n’est émis si Mx
et Mi
ont des priorités de résolution de surcharge différentes.
NB : L’intention de cette règle est de répliquer le comportement du modificateur
params
.
System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute
Nous introduisons l'attribut suivant à la bibliothèque de classes de base (BCL) :
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class OverloadResolutionPriorityAttribute(int priority) : Attribute
{
public int Priority => priority;
}
Toutes les méthodes en C# ont une overload_resolution_priority par défaut de 0, sauf si elles sont attribuées avec OverloadResolutionPriorityAttribute
. S’ils sont attribués à cet attribut, leur overload_resolution_priority est la valeur entière fournie au premier argument de l’attribut.
C’est une erreur d’appliquer OverloadResolutionPriorityAttribute
aux emplacements suivants :
- Propriétés non indexées
- Accesseurs de propriété, indexeur ou événement
- Opérateurs de conversion
- Lambdas
- Fonctions locales
- Finaliseurs
- Constructeurs statiques
Les attributs rencontrés sur ces emplacements dans les métadonnées sont ignorés par C#.
C’est une erreur d’appliquer OverloadResolutionPriorityAttribute
à un emplacement où il serait ignoré, comme sur une substitution d’une méthode de base, car la priorité est lue à partir de la déclaration la moins dérivée d’un membre.
NB : Cela diffère intentionnellement du comportement du modificateur
params
, qui permet de redéfinir ou d’ajouter lorsqu’il est ignoré.
Capacité à être appelés des membres
Une mise en garde importante pour OverloadResolutionPriorityAttribute
est qu’il peut rendre certains membres effectivement inaccessibles à partir d’un appel depuis la source. Par exemple:
using System.Runtime.CompilerServices;
int i = 1;
var c = new C3();
c.M1(i); // Will call C3.M1(long), even though there's an identity conversion for M1(int)
c.M2(i); // Will call C3.M2(int, string), even though C3.M1(int) has less default parameters
class C3
{
public void M1(int i) {}
[OverloadResolutionPriority(1)]
public void M1(long l) {}
[Conditional("DEBUG")]
public void M2(int i) {}
[OverloadResolutionPriority(1), Conditional("DEBUG")]
public void M2(int i, [CallerArgumentExpression(nameof(i))] string s = "") {}
public void M3(string s) {}
[OverloadResolutionPriority(1)]
public void M3(object o) {}
}
Pour ces exemples, les surcharges de priorité par défaut deviennent en effet vestigales et peuvent uniquement être appelées à l’aide de quelques étapes qui effectuent un effort supplémentaire :
- Convertir la méthode en délégué, puis utiliser ce délégué.
- Pour certains scénarios de variance de type de référence, tels que
M3(object)
qui est hiérarchisé par rapport àM3(string)
, cette stratégie échoue. - Les méthodes conditionnelles, telles que
M2
, ne peuvent pas non plus être appelées avec cette stratégie, car les méthodes conditionnelles ne peuvent pas être converties en délégués.
- Pour certains scénarios de variance de type de référence, tels que
- Utilisation de la fonctionnalité d’exécution
UnsafeAccessor
pour l’appeler via une signature correspondante. - Utiliser manuellement la réflexion pour obtenir une référence à la méthode, puis l’appeler.
- Le code qui n’est pas recompilé continuera d’appeler les anciennes méthodes.
- Le code IL écrit manuellement peut spécifier ce qu’il veut.
Questions en suspens
Regroupement de méthodes d’extension (réponse)
Comme actuellement formulées, les méthodes d’extension sont classées par priorité uniquement au sein de leur propre type. Par exemple:
new C2().M([1, 2, 3]); // Will print Ext2 ReadOnlySpan
static class Ext1
{
[OverloadResolutionPriority(1)]
public static void M(this C2 c, Span<int> s) => Console.WriteLine("Ext1 Span");
[OverloadResolutionPriority(0)]
public static void M(this C2 c, ReadOnlySpan<int> s) => Console.WriteLine("Ext1 ReadOnlySpan");
}
static class Ext2
{
[OverloadResolutionPriority(0)]
public static void M(this C2 c, ReadOnlySpan<int> s) => Console.WriteLine("Ext2 ReadOnlySpan");
}
class C2 {}
Lorsque nous effectuons une résolution de surcharge pour les membres d’extension, ne devrions-nous pas trier par type déclarant et considérer plutôt toutes les extensions dans la même étendue ?
Répondre
Nous nous regrouperons toujours. L’exemple ci-dessus imprime Ext2 ReadOnlySpan
Héritage d’attribut sur les substitutions (répondu)
L’attribut doit-il être hérité ? Si ce n’est pas le cas, quelle est la priorité du membre dominant ?
Si l’attribut est spécifié sur un membre virtuel, une substitution de ce membre doit-elle répéter l’attribut ?
Répondre
L’attribut ne sera pas marqué comme hérité. Nous examinerons la déclaration la moins dérivée d’un membre pour déterminer sa priorité de résolution des surcharges.
Erreur ou avertissement de l’application lors de la substitution (répondu)
class Base
{
[OverloadResolutionPriority(1)] public virtual void M() {}
}
class Derived
{
[OverloadResolutionPriority(2)] public override void M() {} // Warn or error for the useless and ignored attribute?
}
Que devrions-nous faire concernant l’application d’un OverloadResolutionPriorityAttribute
dans un contexte où il est ignoré, comme une substitution :
- Ne rien faire, laissez-le ignorer silencieusement.
- Émettez un avertissement indiquant que l’attribut sera ignoré.
- Émettez une erreur indiquant que l’attribut n’est pas autorisé.
3 est l’approche la plus prudente, si nous pensons qu’il peut y avoir un espace à l’avenir où nous pourrions autoriser une substitution pour spécifier cet attribut.
Répondre
Nous allons opter pour 3, et bloquer l'application dans les emplacements où elle serait ignorée.
Implémentation d’interface implicite (réponse)
Quel doit être le comportement d’une implémentation d’interface implicite ? Faut-il spécifier OverloadResolutionPriority
? Quel doit être le comportement du compilateur lorsqu’il rencontre une implémentation implicite sans priorité ? Cela se produit presque certainement, car une bibliothèque d’interface peut être mise à jour, mais pas une implémentation. L’antériorité ici avec params
consiste à ne pas spécifier et à ne pas reporter la valeur.
using System;
var c = new C();
c.M(1, 2, 3); // error CS1501: No overload for method 'M' takes 3 arguments
((I)c).M(1, 2, 3);
interface I
{
void M(params int[] ints);
}
class C : I
{
public void M(int[] ints) { Console.WriteLine("params"); }
}
Nos options sont les suivantes :
- Suivez
params
.OverloadResolutionPriorityAttribute
ne sera pas implicitement reporté ou ne devra pas être spécifié. - Reportez implicitement l’attribut.
- N’effectuez pas implicitement le report de l’attribut, exigez qu’il soit spécifié sur le site d’appel.
- Cela apporte une question supplémentaire : quel doit être le comportement lorsque le compilateur rencontre ce scénario avec des références compilées ?
Répondre
Nous allons aller avec 1.
Autres erreurs d’application (réponse)
Il y a quelques autres emplacements comme celui-ci qui doivent être confirmés. Il s’agit notamment des éléments suivants :
- Opérateurs de conversion : la spécification ne dit jamais que les opérateurs de conversion passent par la résolution de surcharge, de sorte que l’implémentation bloque l’application sur ces membres. Devrait-on le confirmer ?
- Lambdas : de même, les lambda ne sont jamais soumis à une résolution de surcharge, de sorte que l’implémentation les bloque. Devrait-on le confirmer ?
- Destructeurs : encore une fois, actuellement bloqués.
- Constructeurs statiques : encore une fois, actuellement bloqués.
- Fonctions locales : celles-ci ne sont pas actuellement bloquées, car elles subissent une résolution de surcharge, mais vous ne pouvez tout simplement pas les surcharger. Cela est similaire à la façon dont nous n’avons pas d’erreur lorsque l’attribut est appliqué à un membre d’un type qui n’est pas surchargé. Ce comportement doit-il être confirmé ?
Répondre
Tous les emplacements répertoriés ci-dessus sont bloqués.
Comportement Langversion (répondu)
L’implémentation n'émet actuellement des erreurs de version de langage que lorsque OverloadResolutionPriorityAttribute
est appliqué, et non pas ni, lorsqu’elle a effectivement un impact. Cette décision a été prise parce qu’il existe des API que la bibliothèque de classes de base (BCL) ajoutera (à la fois maintenant et au fil du temps) qui commenceront à utiliser cet attribut ; si l’utilisateur définit manuellement sa version de langage sur C# 12 ou une version antérieure, il peut voir ces membres et, en fonction de notre comportement de version de langage, soit :
- Si nous ignorons l’attribut en C# <13, rencontrez une erreur d’ambiguïté, car l’API est vraiment ambiguë sans l’attribut ou ;
- Si nous avons une erreur lorsque l’attribut a affecté le résultat, rencontrez une erreur indiquant que l’API est inconsumable. Cela sera particulièrement mauvais, car
Debug.Assert(bool)
est dé-hiérarchisé dans .NET 9 ou ; - Si nous changeons silencieusement la résolution, rencontrez un comportement potentiellement différent entre différentes versions du compilateur si l’on comprend l’attribut et un autre ne le fait pas.
Le dernier comportement a été choisi, car il entraîne la compatibilité la plus avancée, mais le résultat de modification peut être surprenant pour certains utilisateurs. Devrions-nous confirmer cela ou choisir l’une des autres options ?
Répondre
Nous allons utiliser l’option 1, en ignorant silencieusement l’attribut dans les versions de langue précédentes.
Alternatives
Une proposition précédente a tenté de spécifier une approche avec BinaryCompatOnlyAttribute
, qui était très stricte pour masquer des éléments. Toutefois, cela a beaucoup de problèmes d’implémentation difficiles qui signifient que la proposition est trop forte pour être utile (empêcher le test d’anciennes API, par exemple) ou si faible qu’elle a manqué certaines des objectifs d’origine (comme être en mesure d’avoir une API qui serait autrement considérée comme ambiguë d’appeler une nouvelle API). Cette version est répliquée ci-dessous.
Proposition BinaryCompatOnlyAttribute Proposal (obsolète)
BinaryCompatOnlyAttribute
Conception détaillée
System.BinaryCompatOnlyAttribute
Nous présentons un nouvel attribut réservé :
namespace System;
// Excludes Assembly, GenericParameter, Module, Parameter, ReturnValue
[AttributeUsage(AttributeTargets.Class
| AttributeTargets.Constructor
| AttributeTargets.Delegate
| AttributeTargets.Enum
| AttributeTargets.Event
| AttributeTargets.Field
| AttributeTargets.Interface
| AttributeTargets.Method
| AttributeTargets.Property
| AttributeTargets.Struct,
AllowMultiple = false,
Inherited = false)]
public class BinaryCompatOnlyAttribute : Attribute {}
Lorsqu’il est appliqué à un membre de type, ce membre est traité comme inaccessible à chaque emplacement par le compilateur, ce qui signifie qu’il ne contribue pas à la recherche de membre, à la résolution de surcharge ou à tout autre processus similaire.
Domaines d’accessibilité
Nous mettons à jour §7.5.3 Domaines d’accessibilité comme suit :
Le domaine d’accessibilité d’un membre se compose des sections (éventuellement disjoint) du texte du programme dans lesquelles l’accès au membre est autorisé. Pour définir le domaine d’accessibilité d’un membre, un membre est considéré comme de niveau supérieur s’il n’est pas déclaré dans un type et qu’un membre est considéré comme imbriqué s’il est déclaré dans un autre type. En outre, le texte du programme d’un programme est défini comme tout le texte contenu dans toutes les unités de compilation du programme, et le texte du programme d’un type est défini comme tout le texte contenu dans les type_declarationde ce type (y compris, éventuellement, les types imbriqués dans le type).
Le domaine d’accessibilité d’un type prédéfini (tel que
object
,int
oudouble
) est illimité.Le domaine d’accessibilité d’un type indépendant de niveau supérieur
T
(§8.4.4) déclaré dans un programmeP
est défini comme suit :
- Si
T
est marqué avecBinaryCompatOnlyAttribute
, le domaine d’accessibilité deT
est totalement inaccessible au texte du programme deP
et à tout programme qui fait référenceP
.- Si l’accessibilité déclarée de
T
est publique, le domaine d’accessibilité deT
est le texte du programme deP
et tout programme qui fait référenceP
.- Si l’accessibilité déclarée de
T
est interne, le domaine d’accessibilité deT
est le texte du programme deP
.Remarque: à partir de ces définitions, il suit que le domaine d’accessibilité d’un type indépendant de niveau supérieur est toujours au moins le texte du programme dans lequel ce type est déclaré. fin de la remarque
Le domaine d’accessibilité d’un type construit
T<A₁, ..., Aₑ>
est l’intersection du domaine d’accessibilité du type générique indépendantT
et des domaines d’accessibilité des arguments de typeA₁, ..., Aₑ
.Le domaine d’accessibilité d’un membre imbriqué
M
déclaré dans un typeT
au sein d’un programmeP
, est défini comme suit (notant queM
lui-même peut être un type) :
- Si
M
est marqué avecBinaryCompatOnlyAttribute
, le domaine d’accessibilité deM
est totalement inaccessible au texte du programme deP
et à tout programme qui fait référenceP
.- Si l’accessibilité déclarée de
M
estpublic
, le domaine d’accessibilité deM
est le domaine d’accessibilité deT
.- Si l’accessibilité déclarée du code
M
estprotected internal
, laissezD
être l’union du texte du programme deP
et du texte du programme de tout type dérivé deT
, qui est déclaré en dehors deP
. Le domaine d’accessibilité deM
est l’intersection du domaine d’accessibilité deT
avecD
.- Si l’accessibilité déclarée de
M
estprivate protected
, laissezD
être l’intersection du texte du programme deP
et du texte du programme deT
ainsi que de tout type dérivé deT
. Le domaine d’accessibilité deM
est l’intersection du domaine d’accessibilité deT
avecD
.- Si l’accessibilité déclarée de
M
estprotected
, laissezD
être l’union du texte du programme deT
et du texte du programme de tout type dérivé deT
. Le domaine d’accessibilité deM
est l’intersection du domaine d’accessibilité deT
avecD
.- Si l’accessibilité déclarée de
M
estinternal
, le domaine d’accessibilité deM
est l’intersection du domaine d’accessibilité deT
avec le texte du programme deP
.- Si l’accessibilité déclarée de
M
estprivate
, le domaine d’accessibilité deM
est le texte du programme deT
.
L’objectif de ces ajouts est de faire en sorte que les membres marqués avec BinaryCompatOnlyAttribute
soient complètement inaccessibles à n’importe quel emplacement, ils ne participeront pas à la recherche des membres et ne peuvent pas affecter le reste du programme. Par conséquent, cela signifie qu’ils ne peuvent pas implémenter les membres d’interface, ne peuvent pas s’appeler les uns les autres et ne peuvent pas être substitués (méthodes virtuelles), cachés ou implémentés (membres d’interface). Si cela est trop strict est l’objet de plusieurs questions ouvertes ci-dessous.
Questions non résolues
Méthodes virtuelles et substitution
Que faisons-nous lorsqu’une méthode virtuelle est marquée comme BinaryCompatOnly
? Les substitutions dans une classe dérivée peuvent même ne pas se trouver dans l’assembly actuel, et il se peut que l’utilisateur cherche à introduire une nouvelle version d’une méthode qui, par exemple, diffère uniquement par le type de retour, ce que C# n’autorise pas normalement pour la surcharge. Que se passe-t-il pour les substitutions de cette méthode précédente lors de la recompilation ? Sont-ils autorisés à remplacer le membre BinaryCompatOnly
s’ils sont également marqués comme BinaryCompatOnly
?
Utiliser dans la même DLL
Cette proposition indique que les membres de BinaryCompatOnly
ne sont visibles nulle part, même pas dans l'assemblée en cours de compilation. Est-ce trop strict, ou les membres de BinaryCompatAttribute
doivent-ils éventuellement se chaîner les uns aux autres ?
Implémenter de manière implicite des membres d’interface
Les membres BinaryCompatOnly
doivent-ils pouvoir implémenter des membres d’interface ? Ou devraient-ils être empêchés de le faire. Cela exigerait que, lorsqu’un utilisateur souhaite transformer une implémentation d’interface implicite en BinaryCompatOnly
, il devra également fournir une implémentation d’interface explicite, en clonant probablement le même corps que le membre BinaryCompatOnly
, puisque l’implémentation d’interface explicite ne serait plus en mesure de voir le membre d’origine.
Implémentation des membres d’interface marqués BinaryCompatOnly
Que faisons-nous lorsqu’un membre d’interface a été marqué comme BinaryCompatOnly
? Le type doit toujours fournir une implémentation pour ce membre ; il se peut que nous devons simplement dire que les membres de l’interface ne peuvent pas être marqués comme BinaryCompatOnly
.
C# feature specifications