En Java , il n'y a pas virtual
, new
, override
mots - clés pour la définition de la méthode. Ainsi, le fonctionnement d'une méthode est facile à comprendre. Si DerivedClass étend BaseClass et a une méthode portant le même nom et la même signature de BaseClass, le remplacement aura lieu lors du polymorphisme au moment de l’exécution (à condition que la méthode ne le soit pas static
).
BaseClass bcdc = new DerivedClass();
bcdc.doSomething() // will invoke DerivedClass's doSomething method.
Maintenant , venez à C # il peut y avoir tant de confusion et difficile à comprendre comment le new
ou virtual+derive
ou nouveau + override virtuel fonctionne.
Je ne suis pas en mesure de comprendre pourquoi, dans le monde, je vais ajouter une méthode dans le DerivedClass
même nom et la même signature BaseClass
et définir un nouveau comportement, mais au polymorphisme d'exécution, la BaseClass
méthode sera invoquée! (ce qui n'est pas primordial mais cela devrait être logique).
Dans le cas où virtual + override
l'implémentation logique est correcte, mais le programmeur doit penser à la méthode à donner à l'utilisateur pour autoriser le remplacement au moment du codage. Ce qui a des avantages (on n'y va pas maintenant).
Alors pourquoi en C # il y a tant d’espace pour le raisonnement non logique et la confusion. Donc, puis-je reformuler ma question comme dans quel contexte du monde réel devrais-je penser à utiliser virtual + override
au lieu de new
et aussi à utiliser new
au lieu de virtual + override
?
Après de très bonnes réponses, en particulier de la part d’ Omar , j’entends que les concepteurs de C # ont insisté sur le fait que les programmeurs devraient réfléchir avant de créer une méthode, ce qui est bien et corrige certaines erreurs de débutant en Java.
Maintenant, j'ai une question en tête. Comme en Java si j'avais un code comme
Vehicle vehicle = new Car();
vehicle.accelerate();
et plus tard je fais une nouvelle classe SpaceShip
dérivée de Vehicle
. Ensuite, je veux tout changer car
en un SpaceShip
objet je dois juste changer une seule ligne de code
Vehicle vehicle = new SpaceShip();
vehicle.accelerate();
Cela ne rompra aucune de ma logique à aucun moment du code.
Mais dans le cas de C #, si SpaceShip
ne remplace pas la Vehicle
classe accelerate
et son utilisation, new
la logique de mon code sera brisée. N'est-ce pas un inconvénient?
la source
final
; c'est juste le contraire).@Override
.Réponses:
Puisque vous avez demandé pourquoi C # l'avait fait de cette façon, il est préférable de demander aux créateurs de C #. Anders Hejlsberg, l'architecte principal de C #, a expliqué pourquoi ils avaient choisi de ne pas utiliser le mode virtuel par défaut (comme en Java) dans une interview . Vous trouverez ci-dessous des extraits pertinents.
Gardez à l'esprit que Java utilise virtuel par défaut avec le mot-clé final pour marquer une méthode comme non virtuelle. Encore deux concepts à apprendre, mais beaucoup de gens ne connaissent pas le mot-clé final ou ne l'utilisent pas de manière proactive. C # oblige l'utilisateur à utiliser virtuel et new / override pour prendre ces décisions consciemment.
L'interview aborde plus en détail la façon dont les développeurs envisagent la conception de l'héritage de classe et comment cela a conduit à leur décision.
Passons maintenant à la question suivante:
Cela se produit lorsqu'une classe dérivée veut déclarer qu'elle ne respecte pas le contrat de la classe de base, mais utilise une méthode portant le même nom. (Pour ceux qui ne connaissent pas la différence entre
new
etoverride
en C #, voir cette page MSDN ).Un scénario très pratique est le suivant:
Vehicle
.Vehicle
.Vehicle
classe n'avait aucune méthodePerformEngineCheck()
.Car
classe, j'ajoute une méthodePerformEngineCheck()
.PerformEngineCheck()
.Ainsi, lorsque je recompile avec votre nouvelle API, C # me met en garde contre ce problème, par exemple
Si la base
PerformEngineCheck()
n'était pasvirtual
:Et si la base
PerformEngineCheck()
étaitvirtual
:Maintenant, je dois explicitement décider si ma classe étend réellement le contrat de la classe de base ou s'il s'agit d'un contrat différent mais qui se trouve être le même nom.
new
, je ne casse pas mes clients si la fonctionnalité de la méthode de base était différente de la méthode dérivée. Tout code référencéVehicle
ne sera pasCar.PerformEngineCheck()
appelé, mais le code contenant une référenceCar
continuera de voir la même fonctionnalité que celle que j'avais proposéePerformEngineCheck()
.Un exemple similaire est lorsqu'une autre méthode de la classe de base peut appeler
PerformEngineCheck()
(en particulier dans la version la plus récente), comment l'empêche-t-elle d'appeler lePerformEngineCheck()
de la classe dérivée? En Java, cette décision incomberait à la classe de base, mais elle ne sait rien de la classe dérivée. En C #, cette décision repose à la fois sur la classe de base (via levirtual
mot - clé) et sur la classe dérivée (via les motsnew
-override
clés et ).Bien sûr, les erreurs que le compilateur génère constituent également un outil utile pour les programmeurs afin de ne pas commettre d'erreur inopinément (c'est-à-dire, remplacer ou fournir de nouvelles fonctionnalités sans s'en rendre compte.)
Comme Anders l’a dit, le monde réel nous force à nous attaquer à de telles questions que nous ne voudrions jamais aborder si nous partions de zéro.
EDIT: Ajout d'un exemple où il
new
faudrait utiliser pour assurer la compatibilité des interfaces.EDIT: En parcourant les commentaires, je suis également tombé sur un article écrit par Eric Lippert (alors membre du comité de conception de C #) sur d’autres exemples de scénarios (mentionnés par Brian).
PARTIE 2: Basé sur la question mise à jour
Qui décide si
SpaceShip
est prépondérant en fait laVehicle.accelerate()
ou si elle est différente? Ce doit être leSpaceShip
développeur. Ainsi, si leSpaceShip
développeur décide de ne pas conserver le contrat de la classe de base, votre appelVehicle.accelerate()
ne doit pas être adresséSpaceShip.accelerate()
ou doit-il l'être? C'est à ce moment qu'ils le marqueront commenew
. Toutefois, s’ils décident que le contrat est effectivement conservé, ils le marqueront en faitoverride
. Dans les deux cas, votre code se comportera correctement en appelant la méthode correcte en fonction du contrat . Comment votre code peut-il décider s'ilSpaceShip.accelerate()
s'agit d'une substitutionVehicle.accelerate()
ou s'il s'agit d'une collision de noms? (Voir mon exemple ci-dessus).Cependant, en cas d'héritage implicite, même si
SpaceShip.accelerate()
le contrat de n'est pas conservéVehicle.accelerate()
, l'appel de la méthode ira quand même àSpaceShip.accelerate()
.la source
Cela a été fait parce que c'est la bonne chose à faire. Le fait est que permettre à toutes les méthodes d'être écrasées est une erreur. cela conduit au problème de la classe de base fragile, où vous n'avez aucun moyen de dire si un changement dans la classe de base va casser les sous-classes. Par conséquent, vous devez soit mettre en liste noire les méthodes qui ne doivent pas être remplacées, soit mettre en liste blanche celles qui sont autorisées à être remplacées. Parmi les deux, la liste blanche est non seulement plus sûre (car vous ne pouvez pas créer accidentellement une classe de base fragile ), elle nécessite également moins de travail car vous devez éviter les héritages en faveur de la composition.
la source
final
modificateur, alors quel est le problème?HashMap
- vous ?). Soit concevoir pour l'héritage et clarifier le contrat ou ne pas le faire et assurez-vous que personne ne peut hériter de la classe.@Override
a été ajouté en tant que pansement car il était trop tard pour modifier le comportement à ce moment-là, mais même les développeurs Java (notamment Josh Bloch) d’origine sont d’accord pour dire que c’était une mauvaise décision.Comme l'a dit Robert Harvey, tout est dans ce que vous avez l'habitude de faire. Je trouve étrange le manque de flexibilité de Java .
Cela dit, pourquoi avoir cela en premier lieu? Pour la même raison que C # a
public
,internal
( « rien » aussi),protected
,protected internal
etprivate
, mais Java vientpublic
,protected
rien, etprivate
. Il offre un contrôle plus fin du grain sur le comportement de ce que vous codez, au détriment du nombre de termes et de mots-clés à surveiller.Dans le cas de
new
vs.virtual+override
, cela ressemble à ceci:abstract
, etoverride
dans la sous-classe.virtual
-la etoverride
dans la sous-classe.new
dans la sous - classe.sealed
dans la classe de base.Pour un exemple concret: un projet sur lequel j'ai travaillé sur des commandes de commerce électronique traitées provenant de nombreuses sources différentes. Il y avait une base
OrderProcessor
qui avait la plupart de la logique, avec certainesabstract
/virtual
méthodes à remplacer par la classe enfant de chaque source. Cela a bien fonctionné, jusqu'à ce que nous ayons une nouvelle source qui traitait les commandes de manière complètement différente, de sorte que nous devions remplacer une fonction essentielle. Nous avions deux choix à ce stade: 1) Ajoutervirtual
à la méthode de base etoverride
à l'enfant; ou 2) Ajouternew
à l'enfant.Tandis que l’un ou l’autre pourrait fonctionner, le premier facilitera considérablement le remplacement de cette méthode dans le futur. Cela s'afficherait en saisie automatique, par exemple. C'était un cas exceptionnel, cependant, nous avons donc choisi d'utiliser à la
new
place. Cela a préservé le standard de "cette méthode n'a pas besoin d'être remplacée", tout en permettant le cas particulier où cela s'est produit. C'est une différence sémantique qui facilite la vie.Prenez note, toutefois, qu'il y a une différence de comportement associé à cela, non seulement une différence sémantique. Voir cet article pour plus de détails. Cependant, je ne me suis jamais trouvé dans une situation où je devais tirer parti de ce comportement.
la source
new
cette manière est un bug en attente. Si j'appelle une méthode sur une instance donnée, je m'attends à ce qu'elle fasse toujours la même chose, même si cette instance est convertie en classe de base. Mais ce n'est pas comme ça que lesnew
méthodes ed se comportent.new
WebViewPage <TModel> du framework MVC constitue une excellente utilisation de . Mais j'ai aussi été ébranlé par un bug qui a duré desnew
heures, alors je ne pense pas que ce soit une question déraisonnable.La conception de Java est telle que, quelle que soit la référence à un objet, un appel à un nom de méthode particulier avec des types de paramètre particuliers, s'il est autorisé, invoque toujours la même méthode. Il est possible que le type d'une référence ait une incidence sur les conversions implicites de type paramètre, mais une fois que toutes ces conversions ont été résolues, le type de la référence n'est plus pertinent.
Cela simplifie l'exécution, mais peut causer des problèmes malheureux. Suppose
GrafBase
n'implémente pasvoid DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3)
, mais l'GrafDerived
implémente comme une méthode publique qui dessine un parallélogramme dont le quatrième point calculé est opposé au premier. Supposons en outre qu'une version ultérieure d'GrafBase
implémente une méthode publique avec la même signature, mais son quatrième point calculé en regard du second. Les clients qui reçoivent un,GrafBase
mais reçoivent une référence àGrafDerived
, s'attendentDrawParallelogram
à calculer le quatrième point à la manière de la nouvelleGrafBase
méthode, mais les clients qui utilisaientGrafDerived.DrawParallelogram
avant que la méthode de base ait été modifiée s'attendent au comportementGrafDerived
mis en œuvre à l'origine.En Java, il serait impossible pour l'auteur de
GrafDerived
faire coexister cette classe avec des clients qui utilisent la nouvelleGrafBase.DrawParallelogram
méthode (et qui ignorentGrafDerived
même l'existence) sans rompre la compatibilité avec le code client existant utiliséGrafDerived.DrawParallelogram
auparavantGrafBase
. Puisque leDrawParallelogram
ne peut pas dire quel type de client l'invoque, il doit se comporter de manière identique lorsqu'il est appelé par des types de code client. Étant donné que les deux types de code client ont des attentes différentes quant à la manière dont il doit se comporter, il est impossible d'GrafDerived
éviter de violer les attentes légitimes de l'un d'entre eux (c'est-à-dire de casser le code client légitime).En C #, si
GrafDerived
on ne recompilé, le moteur d' exécution suppose que le code qui invoque laDrawParallelogram
méthode sur des références de typeGrafDerived
nous attendrons le comportementGrafDerived.DrawParallelogram()
avait quand il était la dernière compilation, mais le code qui appelle la méthode sur des références de typeGrafBase
vont s'attendreGrafBase.DrawParallelogram
(le comportement était ajouté). S'ilGrafDerived
est ultérieurement recompilé en présence de la version amélioréeGrafBase
, le compilateur résoudra jusqu'à ce que le programmeur spécifie si sa méthode était censée remplacer le membre héritéGrafBase
, ou si son comportement doit être lié à des références de typeGrafDerived
, mais ne doit pas remplace le comportement des références de typeGrafBase
.On pourrait raisonnablement penser qu'une méthode permettant de
GrafDerived
faire quelque chose de différent d'un membreGrafBase
dont la signature est identique indiquerait une mauvaise conception et ne devrait donc pas être prise en charge. Malheureusement, étant donné que l'auteur d'un type de base n'a aucun moyen de savoir quelles méthodes peuvent être ajoutées aux types dérivés, ni l'inverse, la situation dans laquelle les clients des classes de base et des classes dérivées ont des attentes différentes pour les méthodes portant le même nom est essentiellement inévitable sauf si personne n'est autorisé à ajouter un nom que quelqu'un d'autre pourrait également ajouter. La question n'est pas de savoir si une telle duplication de nom devrait avoir lieu, mais plutôt comment minimiser les dommages quand cela se produit.la source
DerivedGraphics? A class using
GrafDerived`?GrafDerived
. J'ai commencé à utiliserDerivedGraphics
, mais je pensais que c'était un peu long. EvenGrafDerived
est toujours un peu long, mais ne savait pas comment nommer les types de rendu graphique qui devraient avoir une relation base / dérivée claire.Situation standard:
Vous êtes le propriétaire d'une classe de base utilisée par plusieurs projets. Vous souhaitez modifier cette classe de base, qui se situera entre 1 et d'innombrables classes dérivées, qui font partie de projets offrant une valeur réelle la chose en cours d'exécution sur le cadre). Bonne chance à tous les propriétaires occupés des classes dérivées; "eh bien, vous devez changer, vous n'auriez pas dû remplacer cette méthode" sans obtenir un représentant en tant que "Framework: Delayer of Projects and Causer of Bugs" pour les personnes qui doivent approuver les décisions.
Surtout que, en ne déclarant pas qu'il est impossible de le remplacer, vous avez implicitement déclaré qu'ils pouvaient faire ce qui empêche maintenant votre modification.
Et si vous n'avez pas un nombre significatif de classes dérivées fournissant une valeur réelle en surchargeant votre classe de base, pourquoi est-ce une classe de base en premier lieu? L'espoir est un puissant facteur de motivation, mais également un très bon moyen de se retrouver avec du code non référencé.
Résultat final: votre code de classe de base de structure devient incroyablement fragile et statique, et vous ne pouvez pas réellement apporter les modifications nécessaires pour rester à jour / efficace. Alternativement, votre framework obtient un représentant pour l’instabilité (les classes dérivées continuent à se briser) et les utilisateurs ne l’utiliseront pas du tout, car la principale raison de l’utilisation d’un framework est de rendre le codage plus rapide et plus fiable.
En termes simples, vous ne pouvez pas demander aux propriétaires de projets très occupés de retarder leur projet afin de corriger les bogues que vous introduisez, et de vous attendre à quelque chose de mieux qu'un "départ", à moins que vous ne leur apportiez des avantages significatifs, même si la "faute" d'origine était la leur, ce qui est au mieux discutable.
Mieux vaut ne pas les laisser faire la mauvaise chose en premier lieu, c'est-à-dire où "non par défaut virtuel" entre en jeu. Et quand quelqu'un vient à vous avec une raison très claire pour laquelle ils ont besoin de cette méthode particulière pour pouvoir être annulée, et pourquoi il devrait être sûr, vous pouvez le "déverrouiller" sans risquer de casser le code de quelqu'un d'autre.
la source
Le réglage par défaut sur non-virtuel suppose que le développeur de la classe de base est parfait. D'après mon expérience, les développeurs ne sont pas parfaits. Si le développeur d'une classe de base ne peut imaginer un cas d'utilisation où une méthode pourrait être remplacée ou oublie d'ajouter du virtuel, je ne peux pas tirer parti du polymorphisme pour étendre la classe de base sans modifier la classe de base. Dans le monde réel, modifier la classe de base n'est souvent pas une option.
En C #, le développeur de la classe de base ne fait pas confiance au développeur de la sous-classe. En Java, le développeur de la sous-classe ne fait pas confiance au développeur de la classe de base. Le développeur de la sous-classe est responsable de la sous-classe et devrait (à mon humble avis) avoir le pouvoir d'étendre la classe de base comme il l'entend (sauf interdiction explicite, et en java, ils peuvent même se tromper).
C'est une propriété fondamentale de la définition du langage. Ce n'est ni bon ni mauvais, c'est ce que c'est et ne peut pas changer.
la source