Pourquoi les objets sont-ils passés par référence?


31

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?


2
Demandez-vous pourquoi Java et C # avez-vous passé des paramètres par référence plutôt que par valeur, ou par référence plutôt que par pointeur?
robert

@Robert, y a-t-il au plus haut niveau une différence entre "référence plutôt que par pointeur"? Pensez-vous que je devrais changer le titre en quelque chose comme "pourquoi les objets sont toujours référence?"?
Gustavo Cardoso

Les références sont des pointeurs.
compman

2
@Anto: Une référence Java est à tous égards identique à un pointeur C correctement utilisé (correctement utilisé: non transtypé, non défini sur une mémoire invalide, non défini par un littéral).
Zan Lynx

5
Aussi pour être vraiment pédant, le titre est incorrect (du moins en ce qui concerne .net). Les objets NE SONT PAS passés par référence, les références sont passées par valeur. Lorsque vous passez un objet à une méthode, la valeur de référence est copiée dans une nouvelle référence dans le corps de la méthode. Je pense que c'est une honte que "les objets sont passés par référence" est entré dans la liste des citations courantes de programmeur quand il est incorrect et conduit à une meilleure compréhension des références pour les nouveaux programmeurs qui commencent.
SecretDeveloper

Réponses:


15

Les raisons de base se résument à ceci:

  • Les pointeurs sont techniques pour bien faire les choses
  • Vous avez besoin de pointeurs pour implémenter certaines structures de données
  • Vous avez besoin de pointeurs pour être efficace dans l'utilisation de la mémoire
  • Vous n'avez pas besoin d'indexation manuelle de la mémoire pour fonctionner si vous n'utilisez pas directement le matériel.

Par conséquent, les références.


32

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.


Je suis d'accord avec vous, mais je pense qu'il y a aussi une motivation esthétique ou de conception OO le long de celles-ci.
Gustavo Cardoso

9
@Gustavo Cardoso: "une certaine motivation esthétique ou design OO". Non, c'est simplement une optimisation.
S.Lott

6
@ S.Lott: Non, il est logique en termes OO de passer par référence parce que, sémantiquement, vous ne voulez pas faire de copies d'objets. Vous voulez passer l'objet plutôt qu'une copie de celui-ci. Si vous passez par la valeur, cela casse quelque peu la métaphore OO parce que vous avez tous ces clones d'objets générés partout qui n'ont pas de sens à un niveau supérieur.
intuition

@ Gustavo: Je pense que nous discutons du même point. Vous mentionnez la sémantique de la POO et faites référence à la métaphore de la POO comme étant des raisons supplémentaires aux miennes. Il me semble que les créateurs de la POO ont fait ce qu'ils ont fait pour "minimiser la consommation de mémoire" et "économiser sur le temps CPU"
Tim

14

En C ++, vous avez deux options principales: retour par valeur ou retour par pointeur. Regardons le premier:

MyClass getNewObject() {
    MyClass newObj;
    return newObj;
}

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:

  • newObj est construit comme un objet temporaire et placé sur la pile locale.
  • Une copie de newObj est créée et renvoyée.

Nous avons fait une copie inutile de l'objet. C'est une perte de temps de traitement.

Regardons plutôt le retour par pointeur:

MyClass* getNewObject() {
    MyClass newObj = new MyClass();
    return newObj;
}

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:

MyClass someObj = getNewObject();
delete someObj;

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.


Le retour par valeur est déjà assez mauvais, mais le passage par valeur est encore pire quand il s'agit d'objets, et je pense que c'était le vrai problème qu'ils essayaient d'éviter.
Mason Wheeler

à coup sûr, vous avez un point valide. Mais le problème des conceptions OO que @Mason a souligné était la motivation finale du changement. Il n'y avait aucun sens à conserver la différence entre référence et valeur lorsque vous souhaitez simplement utiliser la référence.
Gustavo Cardoso

10

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 Classobjets).

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 transientcas échéant.


J'ai du mal à comprendre le passage de bonnes objections au clonage dans certaines conditions à l'affirmation qu'il n'est jamais nécessaire. Et j'ai rencontré des situations où un doublon exact était nécessaire, où aucune fonction statique n'était impliquée, aucune connexion d'E / S ou ouverte ne pouvait être en cause ... Je comprends les risques du clonage, mais je ne vois jamais la couverture .
Inca

2
@Inca - Vous me comprenez peut-être mal. Mis en œuvre intentionnellement cloneest 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 de clone.
Nicole

L'utilisation de références à des objets immuables est intelligente. Il n'est pas possible de créer des valeurs simples comme Date mutable, puis de créer plusieurs références.
Kevin Cline

@NickC: La principale raison pour laquelle "cloner des choses à sa guise" est dangereuse est que les langages / frameworks comme Java et .net n'ont aucun moyen d'indiquer de manière déclarative si une référence encapsule un état mutable, une identité, les deux ou aucun. Si le champ contient une référence d'objet qui encapsule l'état mutable mais pas l'identité, le clonage de l'objet nécessite que l'objet contenant l'état soit dupliqué et une référence à ce doublon stockée dans le champ. Si la référence encapsule l'identité mais pas l'état mutable, le champ de la copie doit faire référence au même objet que dans l'original.
supercat

L'aspect copie profonde est un point important. La copie d'objets est problématique lorsqu'ils contiennent des références à d'autres objets, en particulier si le graphe d'objets contient des objets mutables.
Caleb

4

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.


Parfois, cela devient vraiment moche lorsque vous utilisez '(* objet) ->'
Gustavo Cardoso

1
Il convient de noter que C ++ distingue les pointeurs, les références et les valeurs. SomeClass * est un pointeur sur un objet. SomeClass & est une référence à un objet. SomeClass est un type de valeur.
Ant

J'ai déjà demandé à @Rober sur la question initiale, mais je le ferai ici aussi: la différence entre * et & sur C ++ est juste une chose technique de bas niveau, n'est-ce pas? Sont-ils, à haut niveau, sémantiquement ce sont les mêmes.
Gustavo Cardoso

3
@Gustavo Cardoso: La différence est sémantique; à un faible niveau technique, ils sont généralement identiques. Un pointeur pointe vers un objet ou est NULL (une mauvaise valeur définie). Sauf si constsa 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.
David Thornley

+1 pour "le polymorphisme doit être fait par référence." C'est un détail incroyablement crucial que la plupart des autres réponses ont ignoré.
Doval

4

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 refmot clé, ce comportement fonctionne comme prévu.


3

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.


3

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.


2

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.


1

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.


Pourrait-il simplement faire une copie superficielle et conserver les valeurs réelles et les pointeurs vers d'autres objets?
Gustavo Cardoso

#Gustavo Cardoso, alors vous pourriez modifier d'autres objets à travers celui-ci, est-ce que vous attendez d'un objet NON passé comme référence?
David

0

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:

#include <iostream> 
class Parent 
{ 
public: 
   int a;
   int b;
   int c;
   Parent(int ia, int ib, int ic) { 
      a = ia; b = ib; c = ic;
   };
   virtual void doSomething(void) { 
      std::cout << "Parent doSomething" << std::endl;
   }
};

class Child : public Parent {
public:
   int d;
   int e;
   Child(int id, int ie) : Parent(1,2,3) { 
      d = id; e = ie;
   };
   virtual void doSomething(void) {
      std::cout << "Child doSomething : D = " << d << std::endl;
   }
};

void foo(Parent a) {
   a.doSomething();
}

int main(void)
{
   Child c(4, 5);
   foo(c);
   return 0;
}

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.


Très bon point. Cependant, je me suis concentré sur les problèmes de retour car les contourner demande beaucoup d'efforts; ce programme peut être corrigé avec l'ajout d'une seule esperluette: void foo (Parent & a)
Ant

OO ne fonctionne vraiment pas sans pointeurs
Gustavo Cardoso

-1.000000000000
P Shved

5
Il est important de se rappeler que Java est pass-by-value (il transmet les références d' objet par valeur, tandis que les primitives sont transmises uniquement par valeur).
Nicole

3
@ Pavé Shved - "deux vaut mieux qu'un!" Ou, en d'autres termes, plus de corde pour vous accrocher.
Nicole

0

Parce qu'il n'y aurait pas de polymorphisme autrement.

Dans la programmation OO, vous pouvez créer une Derivedclasse plus grande à partir d'une classe Base, puis la transmettre aux fonctions qui en attendent une Base. 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).


-1

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É


-2

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

class Top 
{   
    int arrTop[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

class Middle : Top 
{   
    int arrMiddle[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

class Bottom : Middle
{   
    int arrBottom[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

int main()
{   
    using namespace std;

    int arr[20];
    cout << "Size of array of 20 ints: " << sizeof(arr) << endl;

    Top top;
    Middle middle;
    Bottom bottom;

    cout << "Size of Top Class: " << sizeof(top) << endl;
    cout << "Size of middle Class: " << sizeof(middle) << endl;
    cout << "Size of bottom Class: " << sizeof(bottom) << endl;

}   

Ce qui vous montrerait:

Size of array of 20 ints: 80
Size of Top Class: 80
Size of middle Class: 160
Size of bottom Class: 240

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.


1
cela ne semble pas offrir quoi que ce soit de substantiel par rapport aux points avancés et expliqués dans 13 réponses précédentes
gnat
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.