Pourquoi C # implémente des méthodes comme non virtuelles par défaut?

105

Contrairement à Java, pourquoi C # traite-t-il les méthodes comme des fonctions non virtuelles par défaut? Est-ce plus susceptible d'être un problème de performance plutôt que d'autres résultats possibles?

Je me souviens d'avoir lu un paragraphe d'Anders Hejlsberg sur plusieurs avantages de l'architecture existante. Mais qu'en est-il des effets secondaires? Est-ce vraiment un bon compromis d'avoir des méthodes non virtuelles par défaut?

Burcu Dogan
la source
1
Les réponses qui mentionnent des raisons de performances ignorent le fait que le compilateur C # compile principalement les appels de méthode à callvirt et non à appeler . C'est pourquoi en C # il n'est pas possible d'avoir une méthode qui se comporte différemment si la thisréférence est nulle. Voir ici pour plus d'informations.
Andy
Vrai! L' instruction call IL est principalement destinée aux appels effectués à des méthodes statiques.
RBT
1
Les pensées de l'architecte C # Anders Hejlsberg ici et ici .
RBT

Réponses:

98

Les classes doivent être conçues pour l'héritage afin de pouvoir en profiter. Avoir des méthodes virtualpar défaut signifie que chaque fonction de la classe peut être déconnectée et remplacée par une autre, ce qui n'est pas vraiment une bonne chose. Beaucoup de gens pensent même que les classes auraient dû être sealedpar défaut.

virtualLes méthodes peuvent également avoir une légère implication sur les performances. Ce ne sera probablement pas la raison principale, cependant.

Mehrdad Afshari
la source
7
Personnellement, je doute de cette partie de la performance. Même pour les fonctions virtuelles, le compilateur est très capable de déterminer où remplacer callvirtpar un simple calldans le code IL (ou même plus en aval lors de JITting). Java HotSpot fait de même. Le reste de la réponse est juste.
Konrad Rudolph
5
Je conviens que les performances ne sont pas au premier plan, mais elles peuvent aussi avoir des implications sur les performances. C'est probablement très petit. Ce n'est certainement pas la raison de ce choix, cependant. Je voulais juste le mentionner.
Mehrdad Afshari
71
Les cours scellés me donnent envie de frapper les bébés.
mxmissile
6
@mxmissile: moi aussi, mais une bonne conception d'API est de toute façon difficile. Je pense que le scellé par défaut est parfaitement logique pour le code que vous possédez. Plus souvent qu'autrement, l'autre programmeur de votre équipe n'a pas considéré l'héritage quand ils ont implémenté la classe dont vous avez besoin, et scellé par défaut transmettrait assez bien ce message.
Roman Starkov
49
Beaucoup de gens sont aussi des psychopathes, cela ne veut pas dire que nous devrions les écouter. Virtual doit être la valeur par défaut en C #, le code doit être extensible sans avoir à changer d'implémentation ou à le réécrire complètement. Les personnes qui surprotègent leurs API se retrouvent souvent avec des API mortes ou à moitié utilisées, ce qui est bien pire que le scénario d'une personne en abusant.
Chris Nicola
89

Je suis surpris qu'il semble y avoir un tel consensus ici que le non-virtuel par défaut est la bonne façon de faire les choses. Je vais descendre de l'autre côté - je pense pragmatique - de la clôture.

La plupart des justifications me lisent comme le vieil argument «Si nous vous donnons le pouvoir, vous pourriez vous blesser». Des programmeurs?!

Il me semble que le codeur qui ne savait pas assez (ou qui n'avait pas assez de temps) pour concevoir sa bibliothèque pour l'héritage et / ou l'extensibilité est le codeur qui a produit exactement la bibliothèque que je devrai probablement corriger ou modifier - exactement le bibliothèque où la possibilité de remplacer serait la plus utile.

Le nombre de fois où j'ai eu à écrire du code de contournement laid et désespéré (ou à abandonner l'utilisation et à lancer ma propre solution alternative) parce que je ne peux pas passer outre, dépasse de loin le nombre de fois où j'ai été mordu ( par exemple en Java) en remplaçant là où le concepteur n'aurait peut-être pas pensé que je pourrais.

Le non-virtuel par défaut me rend la vie plus difficile.

MISE À JOUR: Il a été souligné [tout à fait correctement] que je n'ai pas vraiment répondu à la question. Donc - et avec des excuses pour être plutôt en retard ...

Je voulais en quelque sorte être capable d'écrire quelque chose de concis comme "C # implémente des méthodes comme non virtuelles par défaut parce qu'une mauvaise décision a été prise qui valorisait plus les programmes que les programmeurs". (Je pense que cela pourrait être quelque peu justifié sur la base de certaines des autres réponses à cette question - comme la performance (optimisation prématurée, n'importe qui?), Ou la garantie du comportement des classes.)

Cependant, je me rends compte que je ne ferais que donner mon opinion et non la réponse définitive que Stack Overflow souhaite. Sûrement, j'ai pensé, au plus haut niveau, la réponse définitive (mais inutile) est:

Ils ne sont pas virtuels par défaut car les concepteurs de langage avaient une décision à prendre et c'est ce qu'ils ont choisi.

Maintenant, je suppose que la raison exacte pour laquelle ils ont pris cette décision, nous ne ... oh, attendez! La transcription d'une conversation!

Il semblerait donc que les réponses et les commentaires ici sur les dangers de la substitution des API et la nécessité de concevoir explicitement pour l'héritage sont sur la bonne voie, mais manquent tous un aspect temporel important: la principale préoccupation d'Anders était de maintenir implicite une classe ou une API. contrat entre les versions . Et je pense qu'il est en fait plus préoccupé par le fait de permettre à la plate-forme .Net / C # de changer sous le code plutôt que par le changement du code utilisateur au-dessus de la plate-forme. (Et son point de vue «pragmatique» est l'exact opposé du mien parce qu'il regarde de l'autre côté.)

(Mais ne pourraient-ils pas simplement avoir choisi virtuel par défaut, puis "final" parsemé de la base de code? Peut-être que ce n'est pas tout à fait la même chose ... et Anders est clairement plus intelligent que moi, alors je vais laisser tomber.)

mwardm
la source
2
Je ne pourrais pas être plus d'accord. Très frustrant lorsque vous utilisez une API tierce et que vous voulez ignorer certains comportements et ne pas pouvoir le faire.
Andy
3
Je suis d'accord avec enthousiasme. Si vous allez publier une API (que ce soit en interne dans une entreprise ou dans le monde externe), vous pouvez et devez vraiment vous assurer que votre code est conçu pour l'héritage. Vous devriez rendre l'API bonne et perfectionnée si vous la publiez pour qu'elle soit utilisée par de nombreuses personnes. Comparé à l'effort nécessaire pour une bonne conception globale (bon contenu, cas d'utilisation clairs, tests, documentation), la conception pour l'héritage n'est vraiment pas si mal. Et si vous ne publiez pas, la virtualisation par défaut prend moins de temps et vous pouvez toujours résoudre la petite partie des cas où cela pose problème.
Gravity
2
Maintenant, s'il n'y avait qu'une fonctionnalité d'éditeur dans Visual Studio pour baliser automatiquement toutes les méthodes / propriétés comme virtual... Complément Visual Studio n'importe qui?
kevinarpe
1
Bien que je sois entièrement d'accord avec vous, ce n'est pas exactement une réponse.
Chris Morgan
2
Compte tenu des pratiques de développement modernes telles que l'injection de dépendances, les frameworks moqueurs et les ORM, il semble clair que nos concepteurs C # ont un peu manqué la cible. Il est très frustrant de devoir tester une dépendance lorsque vous ne pouvez pas remplacer une propriété par défaut.
Jeremy Holovacs
17

Parce qu'il est trop facile d'oublier qu'une méthode peut être remplacée et ne pas être conçue pour cela. C # vous fait réfléchir avant de le rendre virtuel. Je pense que c'est une excellente décision de conception. Certaines personnes (comme Jon Skeet) ont même dit que les classes devraient être scellées par défaut.

Zifre
la source
12

Pour résumer ce que les autres ont dit, il y a plusieurs raisons:

1- En C #, il y a beaucoup de choses dans la syntaxe et la sémantique qui viennent directement de C ++. Le fait que les méthodes n'étaient pas virtuelles par défaut en C ++ a influencé C #.

2- Avoir chaque méthode virtuelle par défaut est un problème de performances car chaque appel de méthode doit utiliser la table virtuelle de l'objet. De plus, cela limite fortement la capacité du compilateur Just-In-Time à intégrer des méthodes et à effectuer d'autres types d'optimisation.

3- Plus important encore, si les méthodes ne sont pas virtuelles par défaut, vous pouvez garantir le comportement de vos classes. Lorsqu'elles sont virtuelles par défaut, comme en Java, vous ne pouvez même pas garantir qu'une méthode getter simple fonctionnera comme prévu, car elle pourrait être remplacée pour faire quoi que ce soit dans une classe dérivée (bien sûr, vous pouvez et devriez faire le méthode et / ou la classe finale).

On peut se demander, comme Zifre l'a mentionné, pourquoi le langage C # n'est pas allé plus loin et a rendu les classes scellées par défaut. Cela fait partie de tout le débat sur les problèmes d'héritage d'implémentation, qui est un sujet très intéressant.

Trillian
la source
1
J'aurais préféré les classes scellées par défaut aussi. Ensuite, ce serait cohérent.
Andy
9

C # est influencé par C ++ (et plus). C ++ n'active pas la répartition dynamique (fonctions virtuelles) par défaut. Un (bon?) Argument pour cela est la question: "À quelle fréquence implémentez-vous des classes qui sont membres d'une classe hiearchy?". L'encombrement mémoire est une autre raison pour éviter d'activer la répartition dynamique par défaut. Une classe sans pointeur virtuel (vpointer) pointant vers une table virtuelle , est bien sûr plus petite que la classe correspondante avec la liaison tardive activée.

Le problème des performances n'est pas si facile de dire «oui» ou «non». La raison en est la compilation Just In Time (JIT) qui est une optimisation d'exécution en C #.

Une autre question similaire sur la " vitesse des appels virtuels. "

Schildmeijer
la source
Je doute un peu que les méthodes virtuelles aient des implications sur les performances pour C #, à cause du compilateur JIT. C'est l'un des domaines dans lesquels JIT peut être meilleur que la compilation hors ligne, car ils peuvent appeler des fonctions en ligne qui sont "inconnues" avant l'exécution
David Cournapeau
1
En fait, je pense que c'est plus influencé par Java que par C ++ qui le fait par défaut.
Mehrdad Afshari
5

La raison simple est le coût de conception et de maintenance en plus des coûts de performance. Une méthode virtuelle a un coût supplémentaire par rapport à une méthode non virtuelle car le concepteur de la classe doit prévoir ce qui se passe lorsque la méthode est remplacée par une autre classe. Cela a un impact important si vous vous attendez à ce qu'une méthode particulière mette à jour l'état interne ou ait un comportement particulier. Vous devez maintenant planifier ce qui se passe lorsqu'une classe dérivée change ce comportement. Il est beaucoup plus difficile d'écrire du code fiable dans cette situation.

Avec une méthode non virtuelle, vous avez un contrôle total. Tout ce qui ne va pas est la faute de l'auteur original. Le code est beaucoup plus facile à raisonner.

JaredPar
la source
C'est un article très ancien, mais pour un débutant qui écrit son premier projet, je m'inquiète constamment des conséquences involontaires / inconnues du code que j'écris. C'est très réconfortant de savoir que les méthodes non virtuelles sont totalement de MA faute.
trevorc
2

Si toutes les méthodes C # étaient virtuelles, le vtbl serait beaucoup plus gros.

Les objets C # ont uniquement des méthodes virtuelles si la classe a des méthodes virtuelles définies. Il est vrai que tous les objets ont des informations de type qui incluent un équivalent vtbl, mais si aucune méthode virtuelle n'est définie, seules les méthodes Object de base seront présentes.

@Tom Hawtin: Il est probablement plus exact de dire que C ++, C # et Java appartiennent tous à la famille des langages C :)

Thomas Bratt
la source
1
Pourquoi serait-ce un problème pour la vtable d'être beaucoup plus grande? Il n'y a qu'une seule vtable par classe (et non par instance), donc sa taille ne fait pas beaucoup de différence.
Gravity
1

Venant d'un arrière-plan perl, je pense que C # a scellé la perte de tout développeur qui aurait pu vouloir étendre et modifier le comportement d'une classe de base via une méthode non virtuelle sans forcer tous les utilisateurs de la nouvelle classe à être conscients de potentiellement derrière la scène détails.

Considérez la méthode Add de la classe List. Et si un développeur souhaitait mettre à jour l'une des nombreuses bases de données potentielles chaque fois qu'une liste particulière est «ajoutée» à? Si 'Add' était virtuel par défaut, le développeur pourrait développer une classe 'BackedList' qui écrasait la méthode 'Add' sans forcer tout le code client à savoir qu'il s'agissait d'une 'BackedList' au lieu d'une 'List' normale. À toutes fins pratiques, la «BackedList» peut être considérée comme une simple «liste» à partir du code client.

Cela a du sens du point de vue d'une grande classe principale qui pourrait fournir l'accès à un ou plusieurs composants de liste qui eux-mêmes sont soutenus par un ou plusieurs schémas dans une base de données. Étant donné que les méthodes C # ne sont pas virtuelles par défaut, la liste fournie par la classe principale ne peut pas être un simple IEnumerable ou ICollection ou même une instance de List, mais doit à la place être publiée au client en tant que `` BackedList '' afin de garantir que la nouvelle version de l'opération "Ajouter" est appelée pour mettre à jour le schéma correct.

Travis
la source
Certes, les méthodes virtuelles par défaut facilitent le travail, mais cela a-t-il un sens? Il y a beaucoup de choses que vous pourriez employer pour vous faciliter la vie. Pourquoi pas seulement des champs publics dans les classes? Modifier son comportement est trop facile. À mon avis, tout dans la langue doit être strictement encapsulé, rigide et résilient au changement par défaut. Changez-le uniquement si nécessaire. Juste être de petites décisions de conception philosophiques. Suite ..
nawfal
... Pour parler du sujet actuel, le modèle d'héritage ne doit être utilisé que si Bc'est le cas A. Si Bnécessite quelque chose de différent, Ace n'est pas le cas A. Je crois que le dépassement en tant que capacité dans le langage lui-même est un défaut de conception. Si vous avez besoin d'une Addméthode différente , votre classe de collection n'est pas un List. Essayer de dire que c'est truquer. La bonne approche ici est la composition (et non la simulation). Il est vrai que tout le cadre est construit sur des capacités primordiales, mais je n'aime pas ça.
nawfal
Je pense que je comprends votre point, mais sur l'exemple concret fourni: «BackedList» pourrait simplement implémenter l'interface «IList» et le client ne connaît que l'interface. droite? est-ce que je manque quelque chose? cependant, je comprends le point plus large que vous essayez de faire valoir.
Vetras le
0

Ce n'est certainement pas un problème de performance. L'interpréteur Java de Sun utilise le même code pour distribuer ( invokevirtualbytecode) et HotSpot génère exactement le même code que ce soit finalou non. Je crois que tous les objets C # (mais pas les structs) ont des méthodes virtuelles, vous aurez donc toujours besoin de l' vtblidentification de classe / runtime. C # est un dialecte de "langages de type Java". Suggérer qu'il vient de C ++ n'est pas tout à fait honnête.

Il y a une idée que vous devriez "concevoir pour l'héritage ou bien l'interdire". Ce qui semble être une excellente idée jusqu'au moment où vous avez une analyse de rentabilisation sérieuse à mettre en place rapidement. Peut-être héritant d'un code que vous ne contrôlez pas.

Tom Hawtin - Tacle
la source
Hotspot est obligé de se donner beaucoup de mal pour faire cette optimisation, précisément parce que toutes les méthodes sont virtuelles par défaut, ce qui a un impact énorme sur les performances. CoreCLR est capable d'atteindre des performances similaires tout en étant beaucoup plus simple
Yair Halberstadt
@YairHalberstadt Hotspot doit pouvoir récupérer le code compilé pour diverses autres raisons. Cela fait des années que je n'ai pas regardé la source, mais la différence entre finalles finalméthodes et les méthodes efficaces est triviale. Il est également intéressant de noter qu'il peut faire de l'inlining bimorphique, c'est-à-dire des méthodes en ligne avec deux implémentations différentes.
Tom Hawtin - tackline