Pourquoi toutes les classes de .NET héritent globalement de la classe Object?

16

C'est très intéressant pour moi quels avantages donne une approche "classe racine globale" pour le framework. En termes simples, les raisons pour lesquelles le framework .NET a été conçu pour avoir une classe d' objet racine avec des fonctionnalités générales adaptées à toutes les classes.

Aujourd'hui, nous concevons un nouveau cadre à usage interne (le cadre sous la plate-forme SAP) et nous sommes tous divisés en deux camps - le premier qui pense que ce cadre devrait avoir une racine mondiale, et le second - qui pense le contraire.

Je suis au camp "global root". Et mes raisons pour lesquelles une telle approche donnerait une bonne flexibilité et une réduction des coûts de développement, car nous ne développerons plus de fonctionnalités générales.

Donc, je suis très intéressé de savoir quelles raisons poussent vraiment les architectes .NET à concevoir le framework de cette manière.

Anton Semenov
la source
6
N'oubliez pas que le .NET Framework fait partie d'un environnement de développement logiciel généralisé. Les cadres plus spécifiques, comme le vôtre, peuvent ne pas avoir besoin d'un objet "racine". Il y a une racine objectdans le framework .NET en partie parce qu'il fournit des capacités de base sur tous les objets, tels que ToString()etGetHashCode()
Robert Harvey
10
"Je suis très intéressant de savoir quelles raisons poussent vraiment les architectes .NET à concevoir le framework de cette manière." Il y a trois très bonnes raisons à cela: 1. Java l'a fait de cette façon, 2. Java l'a fait de cette façon, et 3. Java l'a fait de cette façon.
dasblinkenlight
2
@vcsjones: et Java par exemple déjà pollué par wait()/ notify()/ notifyAll()et clone().
Joachim Sauer
3
@daskblinkenlight C'est caca. Java ne l'a pas fait de cette façon.
Dante
2
@dasblinkenlight: Pour info, Java est actuellement celui qui copie C #, avec lambdas, fonctions LINQ, etc ...
user541686

Réponses:

18

La cause la plus pressante pour les Objectconteneurs (avant les génériques) qui pourraient contenir n'importe quoi au lieu d'avoir à utiliser le style C "Réécrivez-le pour tout ce dont vous avez besoin". Bien sûr, sans doute, l'idée que tout devrait hériter d'une classe spécifique, puis abuser de ce fait pour perdre complètement chaque grain de sécurité de type est si terrible qu'elle aurait dû être un voyant géant sur l'expédition de la langue sans génériques, et signifie également qui Objectest complètement redondant pour le nouveau code.

DeadMG
la source
6
C'est la bonne réponse. Le manque de support générique était la seule raison. Les autres arguments des "méthodes communes" ne sont pas de vrais arguments parce que les méthodes courantes n'ont pas besoin d'être communes - Exemple: 1) ToString: abusé dans la plupart des cas; 2) GetHashCode: selon ce que fait le programme, la plupart des classes n'ont pas besoin de cette méthode; 3) GetType: ne doit pas être une méthode d'instance
Codism
2
De quelle manière .NET "perd-il complètement chaque point de sécurité de type" en "abusant" de l'idée que tout devrait hériter d'une classe spécifique? Y a-t-il un shitcode qui peut être écrit et Objectqui ne peut pas être écrit void *en C ++?
Carson63000
3
Qui a dit que je pensais que void*c'était mieux? Ce n'est pas le cas. Si quoi que ce soit. c'est encore pire .
DeadMG
void*est pire en C avec toutes les conversions de pointeurs implicites, mais C ++ est plus strict à cet égard. Au moins, vous devez lancer explicitement.
Tamás Szelei
2
@fish: une petite histoire de terminologie: en C, il n'y a rien de tel qu'une «distribution implicite». Un cast est un opérateur explicite qui spécifie une conversion. Vous voulez dire " conversions de pointeurs implicites ".
Keith Thompson
11

Je me souviens qu'Eric Lippert avait dit une fois que l'héritage de la System.Objectclasse offrait «la meilleure valeur pour le client». [modifier: oui, il l'a dit ici ]:

... Ils n'ont pas besoin d' un type de base commun. Ce choix n'a pas été fait par nécessité. Il est né d'une volonté d'offrir le meilleur rapport qualité-prix au client.

Lorsque vous concevez un système de type, ou toute autre chose d'ailleurs, vous atteignez parfois des points de décision - vous devez décider X ou non-X ... Les avantages d'avoir un type de base commun l'emportent sur les coûts, et l'avantage net ainsi accumulé est plus grand que l'avantage net de ne pas avoir de type de base commun. Nous choisissons donc d'avoir un type de base commun.

C'est une réponse assez vague. Si vous voulez une réponse plus spécifique, essayez de poser une question plus spécifique.

Le fait que tout dérive de la System.Objectclasse offre une fiabilité et une utilité que je respecte beaucoup. Je sais que tous les objets auront un type ( GetType), qu'ils seront en ligne avec les méthodes de finalisation du CLR ( Finalize), et que je peux les utiliser GetHashCodepour traiter des collections.

gws2
la source
2
C'est imparfait. Vous n'avez pas besoin GetType, vous pouvez simplement l'étendre typeofpour remplacer sa fonctionnalité. Quant à Finalize, alors en fait, de nombreux types ne sont pas du tout finalisables et ne possèdent pas de méthode Finalize significative. En outre, de nombreux types ne sont ni lavables ni convertibles en chaînes, en particulier les types internes qui ne sont jamais destinés à une telle chose et ne sont jamais distribués à un usage public. Tous ces types ont leurs interfaces inutilement polluées.
DeadMG
5
Non, ce n'est pas viciée - je ne prétendais que vous utilisez GetType, Finalizeou GetHashCodepour tous System.Object. J'ai simplement déclaré qu'ils étaient là si vous les vouliez. Quant à "leurs interfaces inutilement polluées", je me réfère à ma citation originale d'elippert: "meilleur rapport qualité / prix pour le client". De toute évidence, l'équipe .NET était disposée à faire face aux compromis.
gws2
Si ce n'est pas nécessaire, c'est la pollution de l'interface. "Si vous le souhaitez" n'est jamais une bonne raison d'inclure une méthode. Elle ne devrait exister que parce que vous en avez besoin et que vos consommateurs l'appelleront . Sinon, ce n'est pas bon. En outre, votre citation de Lippert est un appel à l'erreur de l'autorité, car il ne dit rien d'autre que "c'est bon".
DeadMG
Je n'essaie pas de faire valoir votre point de vue @DeadMG - la question était précisément "Pourquoi toutes les classes dans .NET héritent globalement de la classe Object" et j'ai donc lié à un post précédent par quelqu'un très proche de l'équipe .NET à Microsoft qui a donné une réponse légitime - quoique vague - sur les raisons pour lesquelles l'équipe de développement .NET a choisi cette approche.
gws2
Trop vague pour être une réponse à cette question.
DeadMG
4

Je pense que les concepteurs Java et C # ont ajouté un objet racine car il était essentiellement gratuit pour eux, comme dans le déjeuner gratuit .

Les coûts d'ajout d'un objet racine sont très égaux aux coûts d'ajout de votre première fonction virtuelle. Étant donné que CLR et JVM sont des environnements de récupération de place avec des finaliseurs d'objets, vous devez avoir au moins un virtuel pour votre java.lang.Object.finalizeou pour votre System.Object.Finalizeméthode. Par conséquent, les coûts d'ajout de votre objet racine sont déjà prépayés, vous pouvez obtenir tous les avantages sans payer pour cela. C'est le meilleur des deux mondes: les utilisateurs qui ont besoin d'une classe racine commune obtiendraient ce qu'ils veulent, et les utilisateurs qui s'en moquent seraient en mesure de programmer comme si elle n'était pas là.

dasblinkenlight
la source
4

Il me semble que vous avez trois questions ici: la première est pourquoi une racine commune a été introduite dans .NET, la seconde est quels sont les avantages et les inconvénients de cela, et la troisième est de savoir si c'est une bonne idée pour un élément de framework d'avoir un global racine.

Pourquoi .NET a-t-il une racine commune?

Au sens le plus technique, avoir une racine commune est essentiel pour la réflexion et pour les conteneurs pré-génériques.

De plus, à ma connaissance, avoir une racine commune avec des méthodes de base telles que equals()et a hashCode()été reçu très positivement en Java, et C # a été influencé par (entre autres) Java, alors ils voulaient également avoir cette fonctionnalité.

Quels sont les avantages et les inconvénients d'avoir une racine commune dans une langue?

L'avantage d'avoir une racine commune:

  • Vous pouvez autoriser tous les objets à avoir certaines fonctionnalités que vous jugez importantes, telles que equals()et hashCode(). Cela signifie, par exemple, que chaque objet en Java ou C # peut être utilisé dans une carte de hachage - comparez cette situation avec C ++.
  • Vous pouvez vous référer à des objets de types inconnus - par exemple, si vous transférez simplement des informations, comme l'ont fait les conteneurs pré-génériques.
  • Vous pouvez vous référer à des objets de type inconnaissable - par exemple lors de l'utilisation de la réflexion.
  • Il peut être utilisé dans les rares cas où vous voulez pouvoir accepter un objet qui peut être de types différents et non liés - par exemple comme paramètre d'une printfméthode similaire.
  • Il vous donne la flexibilité de changer le comportement de tous les objets en changeant une seule classe. Par exemple, si vous souhaitez ajouter une méthode à tous les objets de votre programme C #, vous pouvez ajouter une méthode d'extension à object. Pas très courant, peut-être, mais sans racine commune, ce ne serait pas possible.

L'inconvénient d'avoir une racine commune:

  • Il peut être abusif de faire référence à un objet d'une certaine valeur même si vous auriez pu utiliser un type plus précis pour lui.

Devriez-vous aller avec une racine commune dans votre projet-cadre?

Très subjectif, bien sûr, mais quand je regarde la liste ci-dessus, je répondrais certainement oui . Plus précisément, la dernière puce de la liste des professionnels - la flexibilité de modifier ultérieurement le comportement de tous les objets en changeant la racine - devient très utile dans une plate-forme, dans laquelle les modifications apportées à la racine sont plus susceptibles d'être acceptées par les clients que les modifications apportées à un ensemble Langue.

En outre - bien que cela soit encore plus subjectif - je trouve le concept d'une racine commune très élégant et attrayant. Cela facilite également l'utilisation de certains outils, par exemple, il est maintenant facile de demander à un outil d'afficher tous les descendants de cette racine commune et de recevoir un bon aperçu rapide des types de framework.

Bien sûr, les types doivent être au moins légèrement liés pour cela, et en particulier, je ne sacrifierais jamais la règle "is-a" juste pour avoir une racine commune.

Chêne
la source
Je pense que c # n'a pas été simplement influencé par Java, à un moment donné, c'était Java. Microsoft ne pouvait plus utiliser Java et perdrait ainsi des milliards d'investissements. Ils ont commencé par donner à la langue qu'ils avaient déjà un nouveau nom.
Mike Braun
3

Mathématiquement, c'est un peu plus élégant d'avoir un système de type qui contient top , et permet de définir un langage un peu plus complètement.

Si vous créez un framework, alors les choses seront nécessairement consommées par une base de code existante. Vous ne pouvez pas avoir un supertype universel car tous les autres types de ce qui consomme le framework existent.

À ce stade, la décision d'avoir une classe de base commune dépend de ce que vous faites. Il est très rare que beaucoup de choses aient un comportement commun, et qu'il est utile de référencer ces choses via le seul comportement commun.

Mais ça arrive. Si c'est le cas, alors allez de l'avant en faisant abstraction de ce comportement commun.

Telastyn
la source
2

Ils ont poussé pour un objet racine car ils voulaient que toutes les classes du framework prennent en charge certaines choses (obtenir un code de hachage, convertir en chaîne, vérifier l'égalité, etc.). C # et Java ont trouvé utile de mettre toutes ces fonctionnalités communes d'un objet dans une classe racine.

Gardez à l'esprit qu'ils ne violent aucun principe de la POO ou quoi que ce soit. Tout élément de la classe Object a un sens dans n'importe quelle sous-classe (lire: n'importe quelle classe) du système. Si vous choisissez d'adopter cette conception, assurez-vous de suivre ce modèle. Autrement dit, n'incluez pas dans la racine des éléments qui n'appartiennent pas à chaque classe unique de votre système. Si vous suivez attentivement cette règle, je ne vois aucune raison pour laquelle vous ne devriez pas avoir une classe racine qui contient du code commun utile pour l'ensemble du système.

Oleksi
la source
2
Ce n'est pas vrai du tout. Il existe de nombreuses classes internes qui ne sont jamais hachées, jamais réfléchies, jamais converties en chaîne. Héritage inutile et méthodes superflues certainement ne violent de nombreux principes.
DeadMG
2

Quelques raisons, des fonctionnalités communes que toutes les classes enfants peuvent partager. Et cela a également donné aux rédacteurs de framework la possibilité d'écrire beaucoup d'autres fonctionnalités dans le framework que tout le monde pouvait utiliser. Un exemple serait la mise en cache et les sessions ASP.NET. Presque tout peut y être stocké, car ils ont écrit les méthodes add pour accepter des objets.

Une classe racine peut être très séduisante, mais elle est très, très facile à mal utiliser. Serait-il possible d'avoir une interface root à la place? Et juste une ou deux petites méthodes? Ou cela ajouterait-il beaucoup plus de code que ce qui est requis pour votre framework?

Je demande je suis curieux de savoir quelles fonctionnalités vous devez exposer à toutes les classes possibles dans le cadre que vous écrivez. Cela sera-t-il vraiment utilisé par tous les objets? Ou par la plupart d'entre eux? Et une fois que vous aurez créé la classe racine, comment empêcherez-vous les gens d'y ajouter des fonctionnalités aléatoires? Ou des variables aléatoires qu'ils veulent être "globales"?

Il y a plusieurs années, il y avait une très grande application où j'ai créé une classe racine. Après quelques mois de développement, il était peuplé de code qui n'avait rien à faire là-dedans. Nous convertissions une ancienne application ASP et la classe racine était le remplacement de l'ancien fichier global.inc que nous avions utilisé dans le passé. J'ai dû apprendre cette leçon à la dure.

bwalk2895
la source
2

Tous les objets de tas autonomes héritent de Object ; cela a du sens car tous les objets de tas autonomes doivent avoir certains aspects communs, comme un moyen d'identifier leur type. Sinon, si le garbage collector avait une référence à un objet tas de type inconnu, il n'aurait aucun moyen de savoir quels bits dans le blob de mémoire associé à cet objet devraient être considérés comme des références à d'autres objets tas.

De plus, dans le système de types, il est commode d'utiliser le même mécanisme pour définir les membres des structures et les membres des classes. Le comportement des emplacements de stockage de type valeur (variables, paramètres, champs, emplacements de tableau, etc.) est très différent de celui des emplacements de stockage de type classe, mais ces différences de comportement sont obtenues dans les compilateurs de code source et le moteur d'exécution (y compris le compilateur JIT) plutôt que d'être exprimé dans le système de type.

Une conséquence de cela est que la définition d'un type de valeur définit effectivement deux types - un type d'emplacement de stockage et un type d'objet de tas. Les premiers peuvent être implicitement convertis en seconds, et les seconds peuvent être convertis en premiers via typecast. Les deux types de conversion fonctionnent en copiant tous les champs publics et privés d'une instance du type en question à une autre. De plus, il est possible d'utiliser des contraintes génériques pour appeler directement des membres d'interface sur un emplacement de stockage de type valeur, sans en faire une copie au préalable.

Tout cela est important car les références aux objets de tas de type valeur se comportent comme des références de classe et non comme des types de valeur. Considérez, par exemple, le code suivant:

string testEnumerator <T> (T it) où T: IEnumerator <string>
{
  var it2 = it;
  it.MoveNext ();
  it2.MoveNext ();
  le retourner. Actuel;
}
test de vide public ()
{
  var theList = new List <chaîne> ();
  theList.Add ("Fred");
  theList.Add ("George");
  theList.Add ("Percy");
  theList.Add ("Molly");
  theList.Add ("Ron");

  var enum1 = theList.GetEnumerator ();
  IEnumerator <string> enum2 = enum1;

  Debug.Print (testEnumerator (enum1));
  Debug.Print (testEnumerator (enum1));
  Debug.Print (testEnumerator (enum2));
  Debug.Print (testEnumerator (enum2));
}

Si la testEnumerator()méthode est transmise, un emplacement de stockage de type valeur itrecevra une instance dont les champs public et privé sont copiés à partir de la valeur transmise. La variable locale it2contiendra une autre instance dont tous les champs sont copiés it. L' appel MoveNextà it2n'affectera pas it.

Si le code ci-dessus reçoit un emplacement de stockage de type classe, la valeur transmise,, itet it2, fera tous référence au même objet, et donc appeler l' MoveNext()un d'eux l'appellera effectivement sur chacun d'eux.

Notez que la conversion List<String>.Enumeratorpour IEnumerator<String>le transformer efficacement d'un type de valeur en un type de classe. Le type de l'objet tas est List<String>.Enumeratormais son comportement sera très différent du type de valeur du même nom.

supercat
la source
+1 pour ne pas mentionner ToString ()
JeffO
1
Il s'agit de détails de mise en œuvre. Il n'est pas nécessaire de les exposer à l'utilisateur.
DeadMG
@DeadMG: Que voulez-vous dire? Le comportement observable d'un List<T>.Enumeratorqui est stocké dans un List<T>.Enumeratorest significativement différent du comportement de celui qui est stocké dans un IEnumerator<T>, en ce que le stockage d'une valeur de l'ancien type dans un emplacement de stockage de l'un ou l'autre type fera une copie de l'état de l'énumérateur, tout en stockant une valeur de ce dernier type dans un emplacement de stockage qui est également de ce dernier type ne fera pas de copie.
supercat
Oui, mais cela Objectn'a rien à voir avec cela.
DeadMG
@DeadMG: Mon point est qu'une instance de tas de type System.Int32est également une instance de System.Object, mais un emplacement de stockage de type System.Int32ne contient ni une System.Objectinstance ni une référence à une.
supercat
2

Cette conception remonte vraiment à Smalltalk, que je considérerais en grande partie comme une tentative de poursuivre l'orientation de l'objet au détriment de presque toutes les autres préoccupations. En tant que tel, il a tendance (à mon avis) à utiliser l'orientation objet, même lorsque d'autres techniques sont probablement (ou même certainement) supérieures.

Le fait d'avoir une seule hiérarchie avec Object(ou quelque chose de similaire) à la racine facilite (par exemple) la création de vos classes de collection en tant que collections de Object, il est donc trivial qu'une collection contienne tout type d'objet.

En contrepartie de cet avantage plutôt mineur, vous obtenez cependant toute une série d'inconvénients. D'abord, du point de vue du design, vous vous retrouvez avec des idées vraiment folles. Au moins selon la vision Java de l'univers, qu'est-ce que l'athéisme et une forêt ont en commun? Qu'ils ont tous les deux des codes de hachage! Une carte est-elle une collection? Selon Java, non, ce n'est pas le cas!

Dans les années 70, lorsque Smalltalk était conçu, ce genre de non-sens a été accepté, principalement parce que personne n'avait conçu une alternative raisonnable. Smalltalk a été finalisé en 1980, et en 1983, Ada (qui comprend les génériques) a été conçu. Bien qu'Ada n'ait jamais atteint le genre de popularité que certains prédisaient, ses génériques étaient suffisants pour supporter des collections d'objets de types arbitraires - sans la folie inhérente aux hiérarchies monolithiques.

Lorsque Java (et dans une moindre mesure, .NET) a été conçu, la hiérarchie des classes monolithiques était probablement considérée comme un choix «sûr» - avec des problèmes, mais surtout des problèmes connus . La programmation générique, en revanche, était celle que presque tout le monde (même alors) a réalisé était au moins théoriquement une bien meilleure approche du problème, mais que beaucoup de développeurs à orientation commerciale considéraient plutôt mal explorée et / ou risquée (c'est-à-dire dans le monde commercial , Ada a été largement rejeté comme un échec).

Mais soyons clairs: la hiérarchie monolithique était une erreur. Les raisons de cette erreur étaient au moins compréhensibles, mais c'était quand même une erreur. C'est une mauvaise conception, et ses problèmes de conception imprègnent presque tout le code qui l'utilise.

Pour un nouveau design aujourd'hui, cependant, il n'y a pas de question raisonnable: l'utilisation d'une hiérarchie monolithique est une erreur claire et une mauvaise idée.

Jerry Coffin
la source
Il convient de préciser que les modèles étaient connus pour être géniaux et utiles avant l'expédition de .NET.
DeadMG
Dans le monde commercial, Ada était un échec, non pas tant en raison de sa verbosité, mais en raison du manque d'interfaces standardisées pour les services du système d'exploitation. Aucune API standard pour le système de fichiers, les graphiques, le réseau, etc. a rendu le développement d'Ada d'applications «hébergées» extrêmement coûteux.
kevin cline
@kevincline: J'aurais probablement dû formuler cela un peu différemment - à l'effet qu'Ada dans son ensemble a été rejetée comme un échec en raison de son échec sur le marché. Refuser d'utiliser Ada était (IMO) tout à fait raisonnable - mais refuser d'en apprendre beaucoup moins (au mieux).
Jerry Coffin
@Jerry: Je pense que les modèles C ++ ont adopté les idées des génériques Ada, puis les ont considérablement améliorés avec une instanciation implicite.
kevin cline
1

Ce que vous voulez faire est en fait intéressant, il est difficile de prouver si c'est mal ou bien jusqu'à ce que vous ayez terminé.

Voici quelques pistes de réflexion:

  • Quand passeriez-vous délibérément cet objet de niveau supérieur sur un objet plus spécifique?
  • Quel code comptez-vous mettre dans chacune de vos classes?

Ce sont les deux seules choses qui peuvent bénéficier d'une racine partagée. Dans un langage, il est utilisé pour définir quelques méthodes très courantes et vous permet de passer des objets qui n'ont pas encore été définis, mais ceux-ci ne devraient pas s'appliquer à vous.

De plus, par expérience personnelle:

J'ai utilisé une boîte à outils écrite par un développeur qui avait une formation de Smalltalk. Il a fait en sorte que toutes ses classes "Data" étendent une seule classe et toutes les méthodes ont pris cette classe - jusqu'ici tout va bien. Le problème est que les différentes classes de données n'étaient pas toujours interchangeables, alors maintenant mon éditeur et mon compilateur ne pouvaient plus me donner d'aide sur ce qu'il fallait passer dans une situation donnée et j'ai dû me référer à ses documents qui ne l'ont pas toujours aidé. .

C'était la bibliothèque la plus difficile à utiliser que j'ai jamais eu à gérer.

Bill K
la source
0

Parce que c'est la voie de la POO. Il est important d'avoir un type (objet) qui puisse faire référence à n'importe quoi. Un argument de type objet peut accepter n'importe quoi. Il y a aussi l'héritage, vous pouvez être sûr que ToString (), GetHashCode () etc. sont disponibles sur tout et n'importe quoi.

Même Oracle a réalisé l'importance d'avoir un tel type de base et prévoit de supprimer les primitives en Java vers 2017

Dante
la source
6
Ce n'est ni la voie de la POO ni importante. Vous ne donnez aucun argument réel autre que "c'est bon".
DeadMG
1
@DeadMG Vous pouvez lire les arguments après la première phrase. Les primitives se comportent différemment des objets, forçant ainsi le programmeur à écrire de nombreuses variantes du même code de but pour les objets et les primitives.
Dante
2
@DeadMG bien, il a dit que cela signifie que vous pouvez assumer ToString()et que vous GetType()serez sur chaque objet. Il convient probablement de souligner que vous pouvez utiliser une variable de type objectpour stocker n'importe quel objet .NET.
JohnL
De plus, il est parfaitement possible d'écrire un type défini par l'utilisateur qui peut contenir une référence de n'importe quel type. Vous n'avez pas besoin de fonctionnalités linguistiques spéciales pour y parvenir.
DeadMG
Vous ne devriez avoir un objet qu'à des niveaux qui contiennent du code spécifique, vous ne devriez jamais en avoir un juste pour relier votre graphique. L '"objet" implémente en fait quelques méthodes importantes et agit comme un moyen de passer un objet dans un outillage où le type n'a même pas encore été créé.
Bill K