web-dev-qa-db-fra.com

Pourquoi C # a-t-il été créé avec des mots clés "nouveaux" et "virtuels + remplacer" contrairement à Java?

Dans Java il n'y a pas de mots clés virtual, new, override pour la définition de la méthode. Ainsi, le fonctionnement d'une méthode est facile à comprendre. Cause si DerivedClass s'étend BaseClass et a une méthode avec le même nom et la même signature de BaseClass , puis le remplacement aura lieu lors du polymorphisme au moment de l'exécution (à condition que la méthode ne soit pas static).

BaseClass bcdc = new DerivedClass(); 
bcdc.doSomething() // will invoke DerivedClass's doSomething method.

Maintenant, venez en C #, il peut y avoir tellement de confusion et difficile de comprendre comment new ou virtual+derive ou nouveau + remplacement virtuel fonctionne.

Je ne peux pas comprendre pourquoi dans le monde je vais ajouter une méthode dans mon DerivedClass avec le même nom et la même signature que BaseClass et définir un nouveau comportement mais au moment de l'exécution - polymorphisme temporel, la méthode BaseClass sera invoquée! (ce qui n'est pas prioritaire mais logiquement ça devrait l'être).

En cas de virtual + override bien que l'implémentation logique soit correcte, mais le programmeur doit penser à quelle méthode il doit donner la permission à l'utilisateur de passer outre au moment du codage. Qui a un pro-con (n'allons pas là-bas maintenant).

Alors pourquoi en C # il y a tellement d'espace pour un - raisonnement logique et confusion. Puis-je recadrer ma question comme dans quel contexte du monde réel devrais-je penser à utiliser virtual + override au lieu de new et utilisez également new au lieu de virtual + override?


Après quelques très bonnes réponses, en particulier de Omar , je constate que les concepteurs C # ont insisté davantage sur les programmeurs devraient réfléchir avant de faire une méthode, ce qui est bon et gère certaines erreurs de recrue de Java.

Maintenant, j'ai une question en tête. Comme dans Java si j'avais un code comme

Vehicle vehicle = new Car();
vehicle.accelerate();

et plus tard, je crée une nouvelle classe SpaceShip dérivée de Vehicle. Ensuite, je veux changer tous les car en un objet SpaceShip Je dois juste changer une seule ligne de code

Vehicle vehicle = new SpaceShip();
vehicle.accelerate();

Cela ne cassera aucune de ma logique à aucun point de code.

Mais dans le cas de C # si SpaceShip ne remplace pas la classe Vehicle 'accelerate et utilise new alors la logique de mon code sera brisée. N'est-ce pas un inconvénient?

61

Puisque vous avez demandé pourquoi C # l'a fait de cette façon, il est préférable de demander aux créateurs C #. Anders Hejlsberg, l'architecte principal de C #, a expliqué pourquoi ils ont choisi de ne pas utiliser le virtuel par défaut (comme en Java) dans un interview , des extraits pertinents sont ci-dessous.

Gardez à l'esprit que Java a 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 savent pas sur le mot-clé final ou ne pas utiliser de manière proactive. C # oblige à utiliser virtuel et nouveau/remplacer pour prendre consciemment ces décisions.

Il existe plusieurs raisons. L'un est la performance . Nous pouvons observer que lorsque les gens écrivent du code en Java, ils oublient de marquer leurs méthodes comme finales. Par conséquent, ces méthodes sont virtuelles. Parce qu'ils sont virtuels, ils ne fonctionnent pas aussi bien. Il y a juste une surcharge de performances associée au fait d'être une méthode virtuelle. Voilà un problème.

Un problème plus important est le contrôle de version . Il existe deux écoles de pensée sur les méthodes virtuelles. L'école de pensée universitaire dit: "Tout devrait être virtuel, car je pourrais vouloir un jour l'emporter." L'école de pensée pragmatique, qui vient de la création d'applications réelles qui s'exécutent dans le monde réel, dit: "Nous devons faire très attention à ce que nous rendons virtuel".

Lorsque nous créons quelque chose de virtuel dans une plate-forme, nous faisons énormément de promesses sur la façon dont il évoluera à l'avenir. Pour une méthode non virtuelle, nous promettons que lorsque vous appelez cette méthode, x et y se produiront. Lorsque nous publions une méthode virtuelle dans une API, nous promettons non seulement que lorsque vous appelez cette méthode, x et y se produiront. Nous promettons également que lorsque vous substituez cette méthode, nous l'appellerons dans cette séquence particulière par rapport à ces autres et l'état sera dans ceci et cela invariant.

Chaque fois que vous dites virtuel dans une API, vous créez un hook de rappel. En tant que concepteur de framework OS ou API, vous devez être très prudent à ce sujet. Vous ne voulez pas que les utilisateurs remplacent et accrochent à tout moment arbitraire dans une API, car vous ne pouvez pas nécessairement faire ces promesses. Et les gens peuvent ne pas comprendre pleinement les promesses qu'ils font lorsqu'ils rendent quelque chose de virtuel.

L'interview a plus de discussions sur la façon dont les développeurs pensent la conception de l'héritage de classe et comment cela a conduit à leur décision.

Passons maintenant à la question suivante:

Je ne peux pas comprendre pourquoi dans le monde je vais ajouter une méthode dans ma DerivedClass avec le même nom et la même signature que BaseClass et définir un nouveau comportement mais au polymorphisme d'exécution, la méthode BaseClass sera invoquée! (ce qui n'est pas prioritaire mais logiquement ça devrait l'être).

Ce serait quand une classe dérivée veut déclarer qu'elle ne respecte pas le contrat de la classe de base, mais a une méthode avec le même nom. (Pour ceux qui ne connaissent pas la différence entre new et override en C #, voir ceci page MSDN ).

Un scénario très pratique est le suivant:

  • Vous avez créé une API, qui a une classe appelée Vehicle.
  • J'ai commencé à utiliser votre API et dérivé Vehicle.
  • Votre classe Vehicle n'avait pas de méthode PerformEngineCheck().
  • Dans ma classe Car, j'ajoute une méthode PerformEngineCheck().
  • Vous avez publié une nouvelle version de votre API et ajouté une PerformEngineCheck().
  • Je ne peux pas renommer ma méthode car mes clients dépendent de mon API et cela les briserait.
  • Donc, lorsque je recompile avec votre nouvelle API, C # m'avertit de ce problème, par exemple.

    Si la base PerformEngineCheck() n'était pas virtual:

    app2.cs(15,17): warning CS0108: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
    Use the new keyword if hiding was intended.
    

    Et si la base PerformEngineCheck() était virtual:

    app2.cs(15,17): warning CS0114: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
    To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
    
  • 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 du même nom.

  • En le faisant 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 faisant référence à Vehicle ne verra pas l'appel de Car.PerformEngineCheck(), mais le code ayant une référence à Car continuera de voir la même fonctionnalité que celle que j'avais offerte dans PerformEngineCheck().

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êcher d'appeler PerformEngineCheck() de la classe dérivée? En Java, cette décision appartiendrait à la classe de base, mais elle ne sait rien de la classe dérivée. En C #, cette décision repose les deux sur la classe de base (via le mot clé virtual), et sur la classe dérivée (via le new et override mots-clés).

Bien sûr, les erreurs que le compilateur jette fournissent également un outil utile pour que les programmeurs ne commettent pas d'erreurs de manière inattendue (c'est-à-dire qu'ils remplacent ou fournissent de nouvelles fonctionnalités sans s'en rendre compte).

Comme l'a dit Anders, le monde réel nous oblige à de telles questions dans lesquelles, si nous partions de zéro, nous ne voudrions jamais y entrer.

EDIT: ajout d'un exemple où new devrait être utilisé pour garantir la compatibilité de l'interface.

EDIT: En parcourant les commentaires, je suis également tombé sur un écrit par Eric Lippert (alors l'un des membres du comité de conception C #) sur d'autres exemples de scénarios (mentionnés par Brian).


PARTIE 2: Basé sur la question mise à jour

Mais dans le cas de C # si SpaceShip ne remplace pas l'accélération et l'utilisation de la classe Vehicle, la logique de mon code sera rompue. N'est-ce pas un inconvénient?

Qui décide si SpaceShip remplace réellement la Vehicle.accelerate() ou si elle est différente? Il doit s'agir du développeur SpaceShip. Donc, si le développeur SpaceShip décide qu'il ne respecte pas le contrat de la classe de base, votre appel à Vehicle.accelerate() ne devrait pas aller à SpaceShip.accelerate(), ou le devrait-il? C'est alors qu'ils le marqueront comme new. Cependant, s'ils décident qu'il conserve effectivement le contrat, ils le marqueront en fait override. Dans les deux cas, votre code se comportera correctement en appelant la bonne méthode basée sur le contrat . Comment votre code peut-il décider si SpaceShip.accelerate() remplace réellement Vehicle.accelerate() ou s'il s'agit d'une collision de noms? (Voir mon exemple ci-dessus).

Cependant, dans le cas de l'héritage implicite, même si SpaceShip.accelerate() n'a pas conservé le contrat de Vehicle.accelerate(), l'appel de méthode irait toujours à SpaceShip.accelerate().

88
Omer Iqbal

Cela a été fait parce que c'est la bonne chose à faire. Le fait est qu'autoriser toutes les méthodes à remplacer est incorrect; cela conduit au problème fragile de la classe de base, où vous n'avez aucun moyen de dire si un changement à la classe de base cassera les sous-classes. Par conséquent, vous devez soit mettre sur liste noire les méthodes qui ne doivent pas être écrasées, soit mettre en liste blanche celles qui peuvent être écrasées. Des deux, la liste blanche est non seulement plus sûre (puisque vous ne pouvez pas créer une classe de base fragile accidentellement), elle nécessite également moins de travail car vous devez éviter l'héritage en faveur de la composition.

94
Doval

Comme l'a dit Robert Harvey, tout est dans ce à quoi vous êtes habitué. Je trouve étrange manque de Java de cette flexibilité.

Cela dit, pourquoi cela en premier lieu? Pour la même raison que C # a public, internal (également "rien"), protected, protected internal et private, mais Java a juste public, protected, rien et private. Il fournit un contrôle plus fin du grain sur le comportement de ce que vous codez, au détriment d'avoir plus de termes et de mots clés à suivre.

Dans le cas de new contre virtual+override, Ca fait plutot comme ca:

  • Si vous souhaitez forcer les sous-classes à implémenter la méthode, utilisez abstract et override dans la sous-classe.
  • Si vous souhaitez fournir des fonctionnalités mais permettre à la sous-classe de la remplacer, utilisez virtual et override dans la sous-classe.
  • Si vous souhaitez fournir des fonctionnalités que les sous-classes ne devraient jamais avoir à remplacer, n'utilisez rien.
    • Si vous avez alors une sous-classe de cas spécial qui fait doit se comporter différemment, utilisez new dans la sous-classe.
    • Si vous voulez vous assurer qu'aucune sous-classe ne peut jamais remplacer le comportement, utilisez 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 certaines méthodes abstract/virtual pour la classe enfant de chaque source à remplacer. Cela a bien fonctionné, jusqu'à ce que nous obtenions une nouvelle source qui avait une complètement manière différente de traiter les commandes, de sorte que nous devions remplacer une fonction de base. Nous avions deux choix à ce stade: 1) Ajouter virtual à la méthode de base et override dans l'enfant; ou 2) Ajoutez new à l'enfant.

Alors que l'un ou l'autre pourrait fonctionner, le premier rendrait très facile de remplacer cette méthode particulière à l'avenir. Il apparaîtrait en saisie semi-automatique, par exemple. Cependant, comme c'était un cas exceptionnel, nous avons choisi d'utiliser new à la place. Cela a préservé le standard de "cette méthode n'a pas besoin d'être remplacée", tout en permettant le cas spécial où elle l'a fait. C'est une différence sémantique qui facilite la vie.

Notez cependant qu'il y a c'est une différence de comportement associée à cela, pas seulement une différence sémantique. Voir cet article pour plus de détails. Cependant, je n'ai jamais rencontré de situation où je devais profiter de ce comportement.

33
Bobson

La conception de Java est telle que, étant donné toute référence à un objet, un appel à un nom de méthode particulier avec des types de paramètres particuliers, s'il est autorisé, invoquera toujours la même méthode. C'est il est possible que les conversions implicites de type paramètre soient affectées par le type d'une référence, 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 entraîner des problèmes malheureux. Supposons que GrafBase n'implémente pas void DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3), mais GrafDerived l'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 de GrafBase implémente une méthode publique avec la même signature, mais son quatrième point calculé en face du second. Les clients qui reçoivent attendent un GrafBase mais reçoivent une référence à un GrafDerived s'attendront à ce que DrawParallelogram calcule le quatrième point à la manière de la nouvelle méthode GrafBase, mais les clients qui utilisaient GrafDerived.DrawParallelogram avant le changement de la méthode de base s'attendront au comportement que GrafDerived a implémenté à l'origine.

En Java, il n'y aurait aucun moyen pour l'auteur de GrafDerived de faire coexister cette classe avec des clients qui utilisent la nouvelle méthode GrafBase.DrawParallelogram (Et peut ignorer que GrafDerived existe même ) sans rompre la compatibilité avec le code client existant qui utilisait GrafDerived.DrawParallelogram avant que GrafBase ne le définisse. Puisque le DrawParallelogram 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 façon dont il doit se comporter, il n'y a aucun moyen que GrafDerived puisse éviter de violer les attentes légitimes de l'un d'entre eux (c'est-à-dire casser le code client légitime).

En C #, si GrafDerived n'est pas recompilé, le runtime supposera que le code qui invoque la méthode DrawParallelogram sur les références de type GrafDerived attendra le comportement GrafDerived.DrawParallelogram() avait lors de sa dernière compilation, mais le code qui invoque la méthode sur les références de type GrafBase attendra GrafBase.DrawParallelogram (le comportement qui a été ajouté). Si GrafDerived est recompilé plus tard en présence du GrafBase amélioré, le compilateur squawk jusqu'à ce que le programmeur spécifie si sa méthode devait être un remplacement valide pour le membre hérité GrafBase, ou si son comportement doit être lié à des références de type GrafDerived, mais ne doit pas remplacer le comportement de références de type GrafBase.

On pourrait raisonnablement affirmer que le fait d'avoir une méthode de GrafDerived faire quelque chose de différent d'un membre de GrafBase qui a la même signature indiquerait une mauvaise conception, et en tant que telle ne devrait pas être prise en charge. Malheureusement, puisque 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 vice versa, la situation où les clients de classe de base et de classe dérivée ont des attentes différentes pour les méthodes portant le même nom est essentiellement inévitable à moins que 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 doit se produire, mais plutôt de savoir comment minimiser les dommages lorsqu'elle se produit.

8
supercat

Situation standard:

Vous êtes le propriétaire d'une classe de base utilisée par plusieurs projets. Vous souhaitez apporter une modification à ladite classe de base qui se répartira entre 1 et d'innombrables classes dérivées, qui se trouvent dans des projets fournissant une valeur réelle (un cadre fournit au mieux de la valeur en une seule suppression, aucun être humain réel ne veut un cadre, ils veulent la chose fonctionnant sur le Framework). Bonne chance aux propriétaires occupés des classes dérivées; "eh bien, vous devez changer, vous ne devriez pas avoir outrepassé cette méthode" sans obtenir un représentant en tant que "Framework: Delayer of Projects and Causer of Bugs" aux personnes qui doivent approuver les décisions.

D'autant plus que, en ne le déclarant pas non substituable, vous avez implicitement déclaré qu'ils étaient d'accord pour faire la chose qui empêche maintenant votre changement.

Et si vous n'avez pas un nombre important de classes dérivées fournissant une valeur réelle en remplaçant votre classe de base, pourquoi est-ce une classe de base en premier lieu? L'espoir est un puissant facteur de motivation, mais aussi un très bon moyen de se retrouver avec du code non référencé.

Résultat final: le code de votre classe de base d'infrastructure devient incroyablement fragile et statique, et vous ne pouvez pas vraiment 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 de se casser) et les gens ne l'utiliseront pas du tout, car la principale raison d'utiliser 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 occupés de retarder leur projet afin de corriger les bogues que vous introduisez, et vous attendre à rien de mieux qu'un "s'en aller" à moins que vous ne leur fournissiez des avantages significatifs, même si la "faute" d'origine était le leur, ce qui est au mieux défendable.

Mieux vaut ne pas les laisser faire la mauvaise chose en premier lieu, c'est là qu'intervient "non virtuel par défaut". Et quand quelqu'un vient à vous avec une raison très claire pour laquelle il a besoin que cette méthode particulière soit redéfinissable, et pourquoi cela devrait être sûr, vous pouvez le "déverrouiller" sans risquer de casser le code de quelqu'un d'autre.

6
deworde

La valeur par défaut non virtuelle 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 pas imaginer un cas d'utilisation où une méthode pourrait être surchargée ou oublie d'ajouter virtuel alors je ne peux pas profiter du polymorphisme lors de l'extension de la classe de base sans modifier la classe de base. Dans le monde réel, la modification de 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. Dans 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 bon lui semble (sauf déni 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 qu'il est et ne peut pas changer.

0
clish