web-dev-qa-db-fra.com

Méthode C # remplacer la bizarrerie de la résolution

Considérez l'extrait de code suivant:

using System;

class Base
{
    public virtual void Foo(int x)
    {
        Console.WriteLine("Base.Foo(int)");
    }
}

class Derived : Base
{
    public override void Foo(int x)
    {
        Console.WriteLine("Derived.Foo(int)");
    }

    public void Foo(object o)
    {
        Console.WriteLine("Derived.Foo(object)");
    }
}

public class Program
{
    public static void Main()
    {
        Derived d = new Derived();
        int i = 10;
        d.Foo(i);
    }
}

Et la sortie surprenante est:

Derived.Foo(object)

Je m'attendrais à ce qu'il sélectionne la méthode Foo(int x) substituée, car elle est plus spécifique. Cependant, le compilateur C # sélectionne la version Foo(object o) non héritée. Cela provoque également une opération de boxe.

Quelle est la raison de ce comportement?

26
Impworks

C'est la règle, et vous ne l'aimerez peut-être pas ...

Citation de Eric Lippert

si une méthode sur une classe plus dérivée est un candidat applicable, elle est automatiquement meilleure que toute méthode sur une classe moins dérivée, même si la méthode moins dérivée a une meilleure correspondance de signature.

La raison en est que la méthode (c'est-à-dire une meilleure correspondance de signature) pourrait avoir été ajoutée dans une version ultérieure et introduire ainsi un échec " classe de base fragile "


Remarque: C'est une partie assez compliquée/approfondie des spécifications C # et elle saute partout. Cependant, les principales parties du problème que vous rencontrez sont écrites comme suit

Mise à jour

Et c'est pourquoi j'aime stackoverflow, c'est un excellent endroit pour apprendre.

Je citais la section sur le traitement au moment de l'exécution de l'appel de méthode . Où que la question concerne compiler la résolution de surcharge de temps , et devrait l'être.

7.6.5.1 Invocations de méthode

...

L'ensemble des méthodes candidates est réduit pour ne contenir que les méthodes des types les plus dérivés: pour chaque méthode CF de l'ensemble, où C est le type dans lequel la méthode F est déclarée, toutes les méthodes déclarées dans un type de base de C sont supprimés de l'ensemble . De plus, si C est un type de classe autre qu'un objet, toutes les méthodes déclarées dans un type d'interface sont supprimées de l'ensemble. (Cette dernière règle n'a d'effet que lorsque le groupe de méthodes est le résultat d'une recherche de membre sur un paramètre de type ayant une classe de base effective autre que l'objet et un ensemble d'interfaces efficace non vide.)

S'il vous plaît voir la réponse d'Eric post https://stackoverflow.com/a/52670391/1612975 pour un détail complet sur ce qui se passe ici et la partie appropriée des spécifications

Original

Spécification du langage C # version 5.0

7.5.5 Invocation d'un membre de fonction

...

Le traitement au moment de l'exécution d'un appel de membre de fonction se compose des étapes suivantes, où M est le membre de fonction et, si M est un membre d'instance, E est l'expression d'instance:

...

Si M est un membre de fonction d'instance déclaré dans un type de référence:

  • E est évalué. Si cette évaluation provoque une exception, aucune autre étape n'est exécutée.
  • La liste d'arguments est évaluée comme décrit au §7.5.1.
  • Si le type de E est un type de valeur, une conversion de boxe (§4.3.1) est effectuée pour convertir E en objet de type, et E est considéré comme étant de type objet dans les étapes suivantes. Dans ce cas, M ne peut être membre que de System.Object.
  • La valeur de E est vérifiée pour être valide. Si la valeur de E est nulle, une System.NullReferenceException est levée et aucune autre étape n'est exécutée.
  • L'implémentation du membre de fonction à invoquer est déterminée:
    • Si le type de temps de liaison de E est une interface, le membre de fonction à invoquer est l'implémentation de M fournie par le type de temps d'exécution de l'instance référencée par E . Ce membre de fonction est déterminé en appliquant les règles de mappage d'interface (§13.4.4) pour déterminer l'implémentation de M fournie par le type d'exécution de l'instance référencée par E.
    • Sinon, si M est un membre de fonction virtuel, le membre de fonction à invoquer est l'implémentation de M fournie par le type d'exécution de l'instance référencée par E. Ce membre de fonction est déterminé en appliquant les règles pour déterminer l'implémentation la plus dérivée (§10.6.3) de M par rapport au type d'exécution de l'instance référencée par E.
    • Sinon, M est un membre de fonction non virtuel et le membre de fonction à invoquer est M lui-même.

Après avoir lu les spécifications, ce qui est intéressant, c'est que si vous utilisez une interface qui décrit la méthode, le compilateur choisira la signature de surcharge, qui à son tour fonctionnera comme prévu

  public interface ITest
  {
     void Foo(int x);
  }

qui peut être montré ici

En ce qui concerne l'interface, cela a du sens lorsque l'on considère que le comportement de surcharge a été mis en œuvre pour se protéger contre la classe de base Brittle


Ressources supplémentaires

Eric Lippert, Closer is better

L'aspect de la résolution de surcharge en C # dont je veux parler aujourd'hui est vraiment la règle fondamentale selon laquelle une surcharge potentielle est jugée meilleure qu'une autre pour un site d'appel donné: plus proche est toujours meilleur que plus loin. Il existe plusieurs façons de caractériser la "proximité" en C #. Commençons par le plus proche et sortons:

  • Une méthode d'abord déclarée dans une classe dérivée est plus proche qu'une méthode d'abord déclarée dans une classe de base.
  • Une méthode dans une classe imbriquée est plus proche qu'une méthode dans une classe conteneur.
  • Toute méthode du type de réception est plus proche que toute méthode d'extension.
  • Une méthode d'extension trouvée dans une classe dans un espace de noms imbriqué est plus proche qu'une méthode d'extension trouvée dans une classe dans un espace de noms externe.
  • Une méthode d'extension trouvée dans une classe dans l'espace de noms actuel est plus proche qu'une méthode d'extension trouvée dans une classe dans un espace de noms mentionné par une directive using.
  • Une méthode d'extension trouvée dans une classe dans un espace de noms mentionné dans une directive using où la directive est dans un espace de noms imbriqué est plus proche qu'une méthode d'extension trouvée dans une classe dans un espace de noms mentionné dans une directive using où la directive est dans un espace de noms externe.
29
TheGeneral

La réponse acceptée est correcte (à l'exception du fait qu'elle cite la mauvaise section de la spécification) mais elle explique les choses du point de vue de la spécification plutôt que de donner une justification pourquoi la spécification est bonne.

Supposons que nous ayons la classe de base B et la classe dérivée D. B a une méthode M qui prend Giraffe. Maintenant, rappelez-vous, par hypothèse, l'auteur de D sait tout sur les membres publics et protégés de B. Autrement dit: l'auteur de D doit en savoir plus que l'auteur de B, car D a été écrit après B , et D a été écrit pour étendre B à un scénario qui n'est pas déjà géré par B . Il faut donc croire que l'auteur de D fait un meilleur travail d'implémentation de tous fonctionnalité de D que l'auteur de B.

Si l'auteur de D fait une surcharge de M qui prend un animal, ils disent je sais mieux que l'auteur de B comment gérer les animaux, et cela inclut les girafes. Nous devons nous attendre à une résolution de surcharge lors d'un appel à D.M (Giraffe) pour appeler D.M (Animal), et non B.M (Giraffe).

Mettons cela autrement: on nous donne deux justifications possibles:

  • Un appel à D.M (Giraffe) devrait aller à B.M (Giraffe) car Giraffe est plus spécifique qu'Animal
  • Un appel à D.M (Giraffe) devrait aller à D.M (Animal) car D est plus spécifique que B

Les deux justifications concernent la spécificité , alors quelle justification est la meilleure? Nous n'appelons aucune méthode sur Animal! Nous appelons la méthode sur D, donc cette spécificité devrait être celle qui gagne. La spécificité du récepteur est de loin, beaucoup plus importante que la spécificité de l'un de ses paramètres. Les types de paramètres sont là pour briser l'égalité. L'important est de s'assurer que nous choisissons le récepteur le plus spécifique parce que cette méthode a été écrite plus tard par quelqu'un avec plus de connaissances du scénario que D est destiné à gérer .

Maintenant, vous pourriez dire, que se passe-t-il si l'auteur de D a également remplacé B.M (Giraffe)? Il y a deux arguments pour lesquels un appel à D.M (Giraffe) devrait appeler D.M (Animal) dans ce cas.

Premièrement , l'auteur de D devrait savoir que DM (Animal) peut être appelé avec une girafe , et il doit être écrit faire la bonne chose . Du point de vue de l'utilisateur, peu importe que l'appel soit résolu en D.M (Animal) ou B.M (Giraffe), car D a été écrit correctement pour faire la bonne chose.

Deuxièmement , si l'auteur de D a remplacé une méthode de B ou non est un détail d'implémentation de D, et non une partie de la surface publique . Autrement dit: il serait très étrange que change si une méthode a été remplacée ou non change quelle méthode est choisie . Imaginez si vous appelez une méthode sur une classe de base dans une version, puis dans la version suivante, l'auteur de la classe de base modifie légèrement si une méthode est remplacée ou non; vous ne vous attendriez pas à ce que la résolution de surcharge dans la classe dérivée change. C # a été conçu avec soin pour éviter ce type d'échec.

13
Eric Lippert