Un jeune collègue qui étudiait OO m'a demandé pourquoi chaque objet est passé par référence, ce qui est l'opposé des types ou des structures primitifs. C'est une caractéristique commune de langages tels que Java et C #.
Je n'ai pas pu trouver de bonne réponse pour lui.
Quelles sont les motivations de cette décision de conception? Les développeurs de ces langages étaient-ils fatigués de devoir créer des pointeurs et des typedefs à chaque fois?
programming-languages
object-oriented
Gustavo Cardoso
la source
la source
Réponses:
Les raisons de base se résument à ceci:
Par conséquent, les références.
la source
Réponse simple:
Minimiser la consommation de mémoire
et le
temps CPU pour recréer et faire une copie complète de chaque objet passé quelque part.
la source
En C ++, vous avez deux options principales: retour par valeur ou retour par pointeur. Regardons le premier:
En supposant que votre compilateur n'est pas assez intelligent pour utiliser l'optimisation de la valeur de retour, ce qui se passe ici est le suivant:
Nous avons fait une copie inutile de l'objet. C'est une perte de temps de traitement.
Regardons plutôt le retour par pointeur:
Nous avons éliminé la copie redondante, mais nous avons maintenant introduit un autre problème: nous avons créé un objet sur le tas qui ne sera pas automatiquement détruit. Nous devons y faire face nous-mêmes:
Savoir qui est responsable de la suppression d'un objet ainsi alloué est quelque chose qui ne peut être communiqué que par des commentaires ou par convention. Cela conduit facilement à des fuites de mémoire.
De nombreuses solutions ont été suggérées pour résoudre ces deux problèmes - l'optimisation de la valeur de retour (dans laquelle le compilateur est suffisamment intelligent pour ne pas créer la copie redondante en retour par valeur), en passant une référence à la méthode (de sorte que la fonction injecte dans un objet existant plutôt que d'en créer un nouveau), des pointeurs intelligents (pour que la question de la propriété soit théorique).
Les créateurs Java / C # ont réalisé que toujours renvoyer un objet par référence était une meilleure solution, surtout si le langage le supportait nativement. Il est lié à de nombreuses autres fonctionnalités des langues, telles que la collecte des ordures, etc.
la source
Beaucoup d'autres réponses contiennent de bonnes informations. Je voudrais ajouter un point important sur le clonage qui n'a été que partiellement traité.
L'utilisation de références est intelligente. Copier des choses est dangereux.
Comme d'autres l'ont dit, en Java, il n'y a pas de "clone" naturel. Ce n'est pas seulement une fonctionnalité manquante. Vous ne voulez jamais copier bon gré mal gré (peu profond ou profond) toutes les propriétés d'un objet. Et si cette propriété était une connexion à une base de données? Vous ne pouvez pas "cloner" une connexion à une base de données plus que vous ne pouvez cloner un humain. L'initialisation existe pour une raison.
Les copies profondes sont un problème en soi - à quelle profondeur allez-vous vraiment ? Vous ne pouvez certainement pas copier quoi que ce soit qui soit statique (y compris les
Class
objets).Donc, pour la même raison pour laquelle il n'y a pas de clone naturel, les objets qui sont passés en tant que copies créeraient une folie . Même si vous pouviez "cloner" une connexion DB - comment pourriez-vous vous assurer qu'elle est fermée?
* Voir les commentaires - Par cette déclaration "jamais", je veux dire un auto-clone qui clone chaque propriété. Java n'en a pas fourni, et ce n'est probablement pas une bonne idée pour vous en tant qu'utilisateur du langage de créer le vôtre, pour les raisons énumérées ici. Le clonage uniquement des champs non transitoires serait un début, mais même dans ce cas, vous devriez être diligent pour définir le
transient
cas échéant.la source
clone
est très bien. Par "bon gré mal gré", je veux dire copier toutes les propriétés sans y penser - sans intention délibérée. Les concepteurs du langage Java ont forcé cette intention en exigeant une implémentation créée par l'utilisateur declone
.Les objets sont toujours référencés en Java. Ils ne sont jamais contournés.
Un avantage est que cela simplifie la langue. Un objet C ++ peut être représenté comme une valeur ou une référence, ce qui crée le besoin d'utiliser deux opérateurs différents pour accéder à un membre:
.
et->
. (Il y a des raisons pour lesquelles cela ne peut pas être consolidé; par exemple, les pointeurs intelligents sont des valeurs qui sont des références et doivent les garder distinctes.) Java n'a besoin que.
.Une autre raison est que le polymorphisme doit être fait par référence, pas par valeur; un objet traité par valeur est juste là, et a un type fixe. Il est possible de visser cela en C ++.
En outre, Java peut changer l'affectation / copie / par défaut. En C ++, c'est une copie plus ou moins approfondie, tandis qu'en Java, c'est une simple affectation / copie / n'importe quel pointeur, avec
.clone()
et tel au cas où vous auriez besoin de copier.la source
const
sa valeur peut être modifiée pour pointer vers d'autres objets. Une référence est un autre nom pour un objet, ne peut pas être NULL et ne peut pas être réinstallée. Il est généralement implémenté par une simple utilisation de pointeurs, mais c'est un détail d'implémentation.Votre déclaration initiale sur les objets C # transmis par référence n'est pas correcte. En C #, les objets sont des types de référence, mais par défaut, ils sont transmis par valeur, tout comme les types de valeur. Dans le cas d'un type de référence, la "valeur" qui est copiée en tant que paramètre de méthode passe-par-valeur est la référence elle-même, donc les modifications apportées aux propriétés à l'intérieur d'une méthode seront reflétées en dehors de la portée de la méthode.
Cependant, si vous deviez réaffecter la variable de paramètre elle-même à l'intérieur d'une méthode, vous verrez que cette modification ne se reflète pas en dehors de la portée de la méthode. En revanche, si vous passez réellement un paramètre par référence à l'aide du
ref
mot clé, ce comportement fonctionne comme prévu.la source
Réponse rapide
Les concepteurs de langages Java et similaires ont voulu appliquer le concept "tout est un objet". Et passer des données comme référence est très rapide et ne consomme pas beaucoup de mémoire.
Commentaire ennuyeux étendu supplémentaire
Altougth, ces langages utilisent des références d'objet (Java, Delphi, C #, VB.NET, Vala, Scala, PHP), la vérité est que les références d'objet sont des pointeurs vers des objets déguisés. La valeur nulle, l'allocation de mémoire, la copie d'une référence sans copier toutes les données d'un objet, tous sont des pointeurs d'objet, pas des objets simples !!!
En Object Pascal (pas Delphi), anc C ++ (pas Java, pas C #), un objet peut être déclaré comme une variable allouée statique, et aussi avec une variable allouée dynamique, à travers l'utilisation d'un pointeur ("référence d'objet" sans " syntaxe du sucre "). Chaque cas utilise une certaine syntaxe, et il n'y a aucun moyen de se confondre comme en Java "et amis". Dans ces langages, un objet peut être transmis à la fois comme valeur ou comme référence.
Le programmeur sait quand une syntaxe de pointeur est requise et quand elle n'est pas requise, mais dans les langages Java et similaires, cela prête à confusion.
Avant que Java n'existe ou devienne courant, de nombreux programmeurs apprenaient OO en C ++ sans pointeurs, en passant par valeur ou par référence lorsque cela était nécessaire. Lorsqu'ils sont passés de l'apprentissage à des applications d'entreprise, ils utilisent généralement des pointeurs d'objet. La bibliothèque QT en est un bon exemple.
Quand j'ai appris Java, j'ai essayé de suivre le tout est un concept d'objet, mais je suis devenu confus lors du codage. Finalement, j'ai dit "ok, ce sont des objets alloués dynamiquement avec un pointeur avec la syntaxe d'un objet alloué statiquement", et je n'ai pas eu de mal à coder, encore.
la source
Java et C # prennent le contrôle de la mémoire de bas niveau de votre part. Le "tas" où résident les objets que vous créez vit sa propre vie; par exemple, le garbage collector récolte des objets quand il le souhaite.
Puisqu'il existe une couche d'indirection distincte entre votre programme et ce "tas", les deux façons de faire référence à un objet, par valeur et par pointeur (comme en C ++), deviennent indiscernables : vous faites toujours référence aux objets "par pointeur" à quelque part dans le tas. C'est pourquoi une telle approche de conception fait du pass-by-reference la sémantique par défaut de l'affectation. Java, C #, Ruby, et cetera.
Ce qui précède ne concerne que les langues impératives. Dans les langues mentionnées ci - dessus le contrôle de la mémoire est passé à l'exécution, mais la conception de la langue dit aussi « bon, mais en fait, il est la mémoire, et il y a les objets, et ils n'occupent la mémoire ». Les langages fonctionnels s'abstiennent encore plus loin, en excluant le concept de «mémoire» de leur définition. C'est pourquoi le passage par référence ne s'applique pas nécessairement à toutes les langues où vous ne contrôlez pas la mémoire de bas niveau.
la source
Je peux penser à quelques raisons:
La copie de types primitifs est triviale, elle se traduit généralement par une instruction machine.
La copie d'objets n'est pas anodine, l'objet peut contenir des membres qui sont eux-mêmes des objets. La copie d'objets coûte cher en temps CPU et en mémoire. Il existe même plusieurs façons de copier un objet selon le contexte.
Passer des objets par référence est bon marché et cela devient également pratique lorsque vous souhaitez partager / mettre à jour les informations d'objet entre plusieurs clients de l'objet.
Les structures de données complexes (en particulier celles qui sont récursives) nécessitent des pointeurs. Passer des objets par référence n'est qu'un moyen plus sûr de passer des pointeurs.
la source
Car sinon, la fonction devrait pouvoir créer automatiquement une copie (évidemment profonde) de tout type d'objet qui lui est transmis. Et généralement, il ne peut pas deviner pour le faire. Vous devez donc définir l'implémentation de la méthode copy / clone pour tous vos objets / classes.
la source
Parce que Java a été conçu comme un meilleur C ++ et C # a été conçu comme un meilleur Java, et les développeurs de ces langages étaient fatigués du modèle d'objet C ++ fondamentalement cassé, dans lequel les objets sont des types de valeur.
Deux des trois principes fondamentaux de la programmation orientée objet sont l'héritage et le polymorphisme, et le fait de traiter les objets comme des types de valeur au lieu de types de référence fait des ravages avec les deux. Lorsque vous passez un objet à une fonction en tant que paramètre, le compilateur doit savoir combien d'octets passer. Lorsque votre objet est un type de référence, la réponse est simple: la taille d'un pointeur, identique pour tous les objets. Mais lorsque votre objet est un type de valeur, il doit transmettre la taille réelle de la valeur. Puisqu'une classe dérivée peut ajouter de nouveaux champs, cela signifie sizeof (dérivé)! = Sizeof (base), et le polymorphisme sort par la fenêtre.
Voici un programme C ++ trivial qui illustre le problème:
La sortie de ce programme n'est pas ce qu'elle serait pour un programme équivalent dans un langage OO sensé, car vous ne pouvez pas passer un objet dérivé par valeur à une fonction attendant un objet de base, donc le compilateur crée un constructeur de copie caché et passe une copie de la partie parent de l'objet enfant , au lieu de passer l'objet enfant comme vous l'avez demandé. Les gotchas sémantiques cachés comme celui-ci sont la raison pour laquelle le passage d'objets par valeur doit être évité en C ++ et n'est pas possible du tout dans presque tous les autres langages OO.
la source
Parce qu'il n'y aurait pas de polymorphisme autrement.
Dans la programmation OO, vous pouvez créer une
Derived
classe plus grande à partir d'une classeBase
, puis la transmettre aux fonctions qui en attendent uneBase
. Assez banal hein?Sauf que la taille de l'argument d'une fonction est fixe et déterminée au moment de la compilation. Vous pouvez argumenter tout ce que vous voulez, le code exécutable est comme ça, et les langages doivent être exécutés à un moment ou à un autre (les langages purement interprétés ne sont pas limités par cela ...)
Maintenant, il y a une donnée bien définie sur un ordinateur: l'adresse d'une cellule mémoire, généralement exprimée en un ou deux "mots". Il est visible sous forme de pointeurs ou de références dans les langages de programmation.
Donc, pour passer des objets de longueur arbitraire, la chose la plus simple à faire est de passer un pointeur / référence à cet objet.
Il s'agit d'une limitation technique de la programmation OO.
Mais comme pour les gros caractères, vous préférez généralement passer des références de toute façon pour éviter la copie, ce n'est généralement pas considéré comme un coup dur :)
Il y a cependant une conséquence importante, en Java ou en C #, lorsque vous passez un objet à une méthode, vous n'avez aucune idée si votre objet sera modifié par la méthode ou non. Cela rend le débogage / parallélisation plus difficile, et c'est le problème que les langages fonctionnels et la référence transparente tentent de résoudre -> la copie n'est pas si mauvaise (quand cela a du sens).
la source
La réponse est dans le nom (enfin presque de toute façon). Une référence (comme une adresse) fait simplement référence à autre chose, une valeur est une autre copie de quelque chose d'autre. Je suis sûr que quelqu'un a probablement mentionné quelque chose à l'effet suivant, mais il y aura des circonstances dans lesquelles l'un et non l'autre conviendra (sécurité de la mémoire vs efficacité de la mémoire). Il s'agit de gérer la mémoire, la mémoire, la mémoire ... MÉMOIRE! :RÉ
la source
D'accord, je ne dis pas que c'est exactement pourquoi les objets sont des types de référence ou passés par référence, mais je peux vous donner un exemple de la raison pour laquelle c'est une très bonne idée à long terme.
Si je ne me trompe pas, lorsque vous héritez d'une classe en C ++, toutes les méthodes et propriétés de cette classe sont physiquement copiées dans la classe enfant. Ce serait comme réécrire le contenu de cette classe à l'intérieur de la classe enfant.
Cela signifie donc que la taille totale des données de votre classe enfant est une combinaison des éléments de la classe parent et de la classe dérivée.
EG: #include
Ce qui vous montrerait:
Cela signifie que si vous avez une grande hiérarchie de plusieurs classes, la taille totale de l'objet, telle que déclarée ici, serait la combinaison de toutes ces classes. De toute évidence, ces objets seraient considérablement volumineux dans de nombreux cas.
La solution, je crois, est de le créer sur le tas et d'utiliser des pointeurs. Cela signifie que la taille des objets des classes avec plusieurs parents serait en quelque sorte gérable.
C'est pourquoi l'utilisation de références serait une méthode plus préférable pour ce faire.
la source