Ce programme C # complet illustre le problème:
public abstract class Executor<T>
{
public abstract void Execute(T item);
}
class StringExecutor : Executor<string>
{
public void Execute(object item)
{
// why does this method call back into itself instead of binding
// to the more specific "string" overload.
this.Execute((string)item);
}
public override void Execute(string item) { }
}
class Program
{
static void Main(string[] args)
{
object item = "value";
new StringExecutor()
// stack overflow
.Execute(item);
}
}
Je suis tombé sur une StackOverlowException que j'ai retracée à ce modèle d'appel où j'essayais de transférer des appels vers une surcharge plus spécifique. À ma grande surprise, l'invocation ne sélectionnait pas cependant la surcharge plus spécifique, mais rappelait en elle-même. Cela a clairement quelque chose à voir avec le type de base étant générique, mais je ne comprends pas pourquoi il ne sélectionnerait pas la surcharge d'exécution (chaîne).
Quelqu'un a-t-il une idée de cela?
Le code ci-dessus a été simplifié pour montrer le modèle, la structure réelle est un peu plus compliquée, mais le problème est le même.
Il semble que cela soit mentionné dans la spécification C # 5.0, 7.5.3 Résolution de surcharge:
La résolution de surcharge sélectionne le membre de la fonction à invoquer dans les contextes distincts suivants au sein de C #:
- Invocation d'une méthode nommée dans une expression d'invocation (§7.6.5.1).
- Appel d'un constructeur d'instance nommé dans une expression de création d'objet (§7.6.10.1).
- Invocation d'un accesseur indexeur via un accès aux éléments (§7.6.6).
- Invocation d'un opérateur prédéfini ou défini par l'utilisateur référencé dans une expression (§7.3.3 et §7.3.4).
Chacun de ces contextes définit l'ensemble des membres de fonction candidats et la liste des arguments à sa manière unique, comme décrit en détail dans les sections répertoriées ci-dessus. Par exemple, l'ensemble des candidats pour une invocation de méthode n'inclut pas les méthodes marquées override (§7.4), et les méthodes d'une classe de base ne sont pas candidates si une méthode d'une classe dérivée est applicable (§7.6 .5.1).
Quand on regarde 7.4:
Une recherche de membre d'un nom N avec des paramètres de type K dans un type T est traitée comme suit:
• Premièrement, un ensemble de membres accessibles nommé N est déterminé:
Si T est un paramètre de type, alors l'ensemble est l'union des ensembles de
membres accessibles nommés N dans chacun des types spécifiés en tant que contrainte principale ou contrainte secondaire (§10.1.5) pour T, ainsi que l'ensemble des membres accessibles nommés N dans object.Sinon, l'ensemble se compose de tous les membres accessibles (§3.5) nommés N dans T, y compris les membres hérités et les membres accessibles nommés N dans object. Si T est un type construit, l'ensemble des membres est obtenu en substituant les arguments de type comme décrit au §10.3.2. Les membres qui incluent un modificateur de remplacement sont exclus de l'ensemble.
Si vous supprimez override
, le compilateur sélectionne la surcharge Execute(string)
lorsque vous transtypez l'élément.
Comme mentionné dans Jon Skeet article sur la surcharge , lors de l'appel d'une méthode dans une classe qui remplace également une méthode avec le même nom d'une classe de base, le compilateur prendra toujours la méthode en classe au lieu de la méthode remplacer, quelle que soit la "spécificité" du type, à condition que la signature soit "compatible".
Jon poursuit en soulignant qu'il s'agit d'un excellent argument pour éviter la surcharge au-delà des limites d'héritage, car c'est exactement le genre de comportement inattendu qui peut se produire.
Comme d'autres réponses l'ont noté, c'est par conception.
Prenons un exemple moins compliqué:
class Animal
{
public virtual void Eat(Apple a) { ... }
}
class Giraffe : Animal
{
public void Eat(Food f) { ... }
public override void Eat(Apple a) { ... }
}
La question est de savoir pourquoi giraffe.Eat(Apple)
se résout en Giraffe.Eat(Food)
et non le virtuel Animal.Eat(Apple)
.
Ceci est la conséquence de deux règles:
(1) Le type du récepteur est plus important que le type de tout argument lors de la résolution des surcharges.
J'espère qu'il est clair pourquoi cela doit être le cas. La personne qui écrit la classe dérivée a strictement plus de connaissances que la personne qui écrit la classe de base, car la personne qui écrit la classe dérivée a utilisé la classe de base, et non l'inverse.
La personne qui a écrit Giraffe
a dit "J'ai un moyen pour un Giraffe
de manger n'importe quelle nourriture " et cela nécessite connaissance particulière des composants internes de la digestion des girafes. Cette information n'est pas présente dans l'implémentation de la classe de base, qui ne sait que manger des pommes.
Ainsi, la résolution de surcharge doit toujours donner la priorité au choix d'une méthode applicable d'une classe dérivée par rapport au choix d'une méthode d'une classe de base, indépendamment de l'amertume des conversions de type d'argument.
(2) Le choix de remplacer ou de ne pas remplacer une méthode virtuelle ne fait pas partie de la surface publique d'une classe. C'est un détail d'implémentation privé. Par conséquent, aucune décision ne doit être prise lors de la résolution de surcharge qui changerait selon qu'une méthode est ou non remplacée.
La résolution de surcharge ne doit jamais dire "je vais choisir le virtuel Animal.Eat(Apple)
car il a été remplacé ".
Maintenant, vous pourriez bien dire "OK, supposons que je suis à l'intérieur de la girafe quand je passe l'appel." Code à l'intérieur Giraffe a toutes les connaissances des détails d'implémentation privés, non? Il pourrait donc prendre la décision d'appeler le virtuel Animal.Eat(Apple)
au lieu de Giraffe.Eat(Food)
face à giraffe.Eat(Apple)
, non? Parce qu'il sait qu'il existe une implémentation qui comprend les besoins des girafes qui mangent des pommes.
C'est un remède pire que la maladie. Nous avons maintenant une situation où le code identique a un comportement différent selon l'endroit où c'est couru! Vous pouvez imaginer avoir un appel à giraffe.Eat(Apple)
en dehors de la classe, le refactoriser pour qu'il soit à l'intérieur de la classe, et des changements de comportement soudainement observables!
Ou, vous pourriez dire, hé, je me rends compte que ma logique Giraffe est en fait suffisamment générale pour passer à une classe de base, mais pas à Animal, donc je vais refactoriser mon code Giraffe
pour:
class Mammal : Animal
{
public void Eat(Food f) { ... }
public override void Eat(Apple a) { ... }
}
class Giraffe : Mammal
{
...
}
Et maintenant, tous les appels à giraffe.Eat(Apple)
à l'intérieur Giraffe
ont soudainement différents comportement de résolution de surcharge après le refactoring? Ce serait très inattendu!
C # est un langage de succès; nous voulons vraiment nous assurer que de simples refactorings comme changer où dans une hiérarchie une méthode est surchargée ne provoquent pas de changements subtils de comportement.
Résumant:
Des réflexions supplémentaires sur des questions connexes peuvent être trouvées ici: https://ericlippert.com/2013/12/23/closer-is-better/ et ici https: //blogs.msdn .Microsoft.com/ericlippert/2007/09/04/future-breaking-changes-part-three /