¿Por qué se pasan los objetos por referencia?

31

Un joven compañero de trabajo que estaba estudiando OO me preguntó por qué cada objeto se pasa por referencia, que es lo contrario de los tipos o estructuras primitivas. Es una característica común de lenguajes como Java y C #.

No pude encontrar una buena respuesta para él.

¿Cuáles son las motivaciones para esta decisión de diseño? ¿Los desarrolladores de estos lenguajes estaban cansados ​​de tener que crear punteros y typedefs todo el tiempo?

Gustavo Cardoso
fuente
2
¿Se pregunta por qué Java y C # han pasado parámetros por referencia en lugar de por valor, o por referencia en lugar de por puntero?
robert
@Robert, ¿hay en el alto nivel alguna diferencia entre "referencia en lugar de por puntero"? ¿Crees que debería cambiar el título a algo así como 'por qué los objetos siempre son referencia? "
Gustavo Cardoso
Las referencias son punteros.
compman
2
@Anto: una referencia de Java es idéntica en todos los aspectos a un puntero C utilizado correctamente (utilizado correctamente: sin conversión de tipos, no establecido en memoria no válida, no establecido por un literal).
Zan Lynx
55
También para ser realmente pedante, el título es incorrecto (al menos en lo que respecta a .net). Los objetos NO se pasan por referencia, las referencias se pasan por valor. Cuando pasa un objeto a un método, el valor de referencia se copia a una nueva referencia dentro del cuerpo del método. Creo que es una pena que "los objetos se pasan por referencia" haya ingresado en la lista de citas comunes de programadores cuando es incorrecto y conduce a una comprensión más pobre de las referencias para los nuevos programadores que comienzan.
SecretDeveloper

Respuestas:

15

Las razones básicas se reducen a esto:

  • Los punteros son técnicos para acertar
  • Necesita punteros para implementar ciertas estructuras de datos
  • Necesita punteros para ser eficiente en el uso de la memoria
  • No necesita la indexación de memoria manual para trabajar si no está utilizando el hardware directamente.

Por lo tanto, referencias.

Paul Nathan
fuente
32

Respuesta simple:

Minimizando el consumo de memoria
y
el tiempo de CPU al recrear y hacer una copia profunda de cada objeto que se pasa a algún lado.

Tim
fuente
Estoy de acuerdo con usted, pero creo que también hay alguna motivación estética o de diseño OO a lo largo de estos.
Gustavo Cardoso
99
@Gustavo Cardoso: "alguna motivación estética o de diseño OO". No. Es simplemente una optimización.
S.Lott
66
@ S.Lott: No, tiene sentido en términos OO pasar por referencia porque, semánticamente, no desea hacer copias de objetos. Desea pasar el objeto en lugar de una copia del mismo. Si pasa por valor, rompe un poco la metáfora OO porque tiene todos estos clones de objetos que se generan en todo el lugar que no tienen sentido en un nivel superior.
intuido
@Gustavo: Creo que estamos discutiendo el mismo punto. Usted menciona la semántica de OOP y se refiere a la metáfora de OOP como razones adicionales a las mías. Me parece que los creadores de la OOP hicieron lo que hicieron para "minimizar el consumo de memoria" y "Ahorrar tiempo de CPU"
Tim
14

En C ++, tiene dos opciones principales: retorno por valor o retorno por puntero. Veamos el primero:

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

Suponiendo que su compilador no es lo suficientemente inteligente como para usar la optimización del valor de retorno, lo que sucede aquí es esto:

  • newObj se construye como un objeto temporal y se coloca en la pila local.
  • Se crea y devuelve una copia de newObj.

Hemos hecho una copia del objeto sin sentido. Esto es una pérdida de tiempo de procesamiento.

Veamos el retorno por puntero en su lugar:

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

Hemos eliminado la copia redundante, pero ahora hemos introducido otro problema: hemos creado un objeto en el montón que no se destruirá automáticamente. Tenemos que lidiar con eso nosotros mismos:

MyClass someObj = getNewObject();
delete someObj;

Saber quién es responsable de eliminar un objeto asignado de esta manera es algo que solo se puede comunicar mediante comentarios o por convención. Conduce fácilmente a pérdidas de memoria.

Se han sugerido muchas soluciones para resolver estos dos problemas: la optimización del valor de retorno (en el que el compilador es lo suficientemente inteligente como para no crear la copia redundante en retorno por valor), pasando una referencia al método (por lo que la función se inyecta en un objeto existente en lugar de crear uno nuevo), punteros inteligentes (para que la cuestión de la propiedad sea discutible).

Los creadores de Java / C # se dieron cuenta de que siempre devolver el objeto por referencia era una mejor solución, especialmente si el lenguaje lo soportaba de forma nativa. Se relaciona con muchas otras características que tienen los idiomas, como la recolección de basura, etc.

Hormiga
fuente
El rendimiento por valor es suficientemente malo, pero el valor por paso es aún peor cuando se trata de objetos, y creo que ese era el verdadero problema que intentaban evitar.
Mason Wheeler
seguro que tienes un punto válido. Pero el problema de diseño de OO que @Mason señaló fue la motivación final del cambio. No tenía sentido mantener la diferencia entre referencia y valor cuando solo desea utilizar la referencia.
Gustavo Cardoso
10

Muchas otras respuestas tienen buena información. Me gustaría agregar un punto importante sobre la clonación que solo se ha abordado parcialmente.

Usar referencias es inteligente. Copiar cosas es peligroso.

Como otros han dicho, en Java, no existe un "clon" natural. Esta no es solo una característica que falta. Usted no quiere simplemente se quiera o no copia * (ya sea superficial o profunda) cada propiedad en un objeto. ¿Qué pasa si esa propiedad era una conexión de base de datos? No puede simplemente "clonar" una conexión de base de datos más de lo que puede clonar un humano. La inicialización existe por una razón.

Las copias profundas son un problema en sí mismas: ¿qué tan profundo es realmente ? Definitivamente no podría copiar nada que sea estático (incluidos los Classobjetos).

Entonces, por la misma razón por la que no existe un clon natural, los objetos que se pasan como copias crearían locura . Incluso si pudiera "clonar" una conexión de base de datos, ¿cómo se aseguraría ahora de que esté cerrada?


* Vea los comentarios: con esta declaración de "nunca", me refiero a un clon automático que clona todas las propiedades. Java no proporcionó uno, y probablemente no sea una buena idea para usted, como usuario del lenguaje, crear el suyo propio, por las razones que se enumeran aquí. La clonación solo de campos no transitorios sería un comienzo, pero incluso así, deberá ser diligente para definir transientdónde sea apropiado.

Nicole
fuente
Tengo problemas para entender el salto de las buenas objeciones a la clonación en ciertas condiciones a la afirmación de que nunca es necesario. Y me he encontrado con situaciones en las que se necesitaba un duplicado exacto, donde no había funciones estáticas involucradas, no había E / S ni conexiones abiertas ... Entiendo los riesgos de la clonación, pero no puedo ver la manta nunca .
Inca
2
@Inca - Puedes estar malinterpretándome. Implementado intencionalmente cloneestá bien. Por "willy-nilly" me refiero a copiar todas las propiedades sin pensar en ello, sin intención intencionada. Los diseñadores del lenguaje Java forzaron esta intención al requerir la implementación de clone.
Nicole
Usar referencias a objetos inmutables es inteligente. Hacer valores simples como Fecha mutable y luego crear múltiples referencias a ellos no lo es.
Kevin Cline
@NickC: La razón principal por la que "clonar cosas de cualquier manera" es peligroso es que los lenguajes / frameworks como Java y .net no tienen ningún medio de indicar declarativamente si una referencia encapsula un estado mutable, identidad, ambos o ninguno. Si el campo contiene una referencia de objeto que encapsula el estado mutable pero no la identidad, la clonación del objeto requiere que el objeto que contiene el estado esté duplicado, y una referencia a ese duplicado almacenado en el campo. Si la referencia encapsula identidad pero no estado mutable, el campo en la copia debe referirse al mismo objeto que en el original.
supercat
El aspecto de copia profunda es un punto importante. Copiar objetos es problemático cuando contienen referencias a otros objetos, particularmente si el gráfico de objetos contiene objetos mutables.
Caleb
4

Los objetos siempre están referenciados en Java. Nunca se pasan a su alrededor.

Una ventaja es que esto simplifica el lenguaje. Un objeto C ++ puede representarse como un valor o una referencia, creando la necesidad de usar dos operadores diferentes para acceder a un miembro: .y ->. (Hay razones por las que esto no se puede consolidar; por ejemplo, los punteros inteligentes son valores que son referencias y deben mantener esos valores distintos). Java solo necesita ..

Otra razón es que el polimorfismo debe hacerse por referencia, no por valor; un objeto tratado por valor solo está allí y tiene un tipo fijo. Es posible arruinar esto en C ++.

Además, Java puede cambiar la asignación predeterminada / copiar / lo que sea. En C ++, es una copia más o menos profunda, mientras que en Java es una simple asignación de puntero / copia / lo que sea, con .clone()y tal en caso de que necesite copiar.

David Thornley
fuente
A veces se vuelve realmente feo cuando usas '(* objeto) ->'
Gustavo Cardoso
1
Vale la pena señalar que C ++ distingue entre punteros, referencias y valores. SomeClass * es un puntero a un objeto. SomeClass & es una referencia a un objeto. SomeClass es un tipo de valor.
Ant
Ya le pregunté a @Rober sobre la pregunta inicial, pero también lo haré aquí: la diferencia entre * y & en C ++ es solo una cuestión técnica de bajo nivel, ¿no? ¿Son, en alto nivel, semánticamente son lo mismo?
Gustavo Cardoso
3
@Gustavo Cardoso: La diferencia es semántica; en un nivel técnico bajo son generalmente idénticos. Un puntero apunta a un objeto o es NULL (un valor incorrecto definido). A menos que constsu valor se pueda cambiar para apuntar a otros objetos. Una referencia es otro nombre para un objeto, no puede ser NULL y no se puede volver a colocar. Generalmente se implementa mediante el uso simple de punteros, pero ese es un detalle de implementación.
David Thornley
+1 para "el polimorfismo debe hacerse por referencia". Ese es un detalle increíblemente crucial que la mayoría de las otras respuestas han ignorado.
Doval
4

Su declaración inicial sobre los objetos C # que se pasan por referencia no es correcta. En C #, los objetos son tipos de referencia, pero de forma predeterminada se pasan por valor al igual que los tipos de valor. En el caso de un tipo de referencia, el "valor" que se copia como un parámetro del método de paso por valor es la referencia misma, por lo que los cambios en las propiedades dentro de un método se reflejarán fuera del alcance del método.

Sin embargo, si tuviera que reasignar la variable del parámetro dentro de un método, verá que este cambio no se refleja fuera del alcance del método. Por el contrario, si realmente pasa un parámetro por referencia usando la refpalabra clave, este comportamiento funciona como se esperaba.

newdayrising
fuente
3

Respuesta rápida

Los diseñadores de Java y lenguajes similares querían aplicar el concepto de "todo es un objeto". Y pasar datos como referencia es muy rápido y no consume mucha memoria.

Comentario aburrido extendido adicional

Además, esos lenguajes usan referencias de objetos (Java, Delphi, C #, VB.NET, Vala, Scala, PHP), la verdad es que las referencias de objetos son punteros a objetos disfrazados. ¡El valor nulo, la asignación de memoria, la copia de una referencia sin copiar todos los datos de un objeto, todos son punteros de objetos, no objetos simples!

En Object Pascal (no Delphi), anc C ++ (no Java, no C #), un objeto puede declararse como una variable asignada estática, y también con una variable asignada dinámica, mediante el uso de un puntero ("referencia de objeto" sin " sintaxis de azúcar "). Cada caso usa cierta sintaxis, y no hay forma de confundirse como en Java "y amigos". En esos idiomas, un objeto se puede pasar como valor o como referencia.

El programador sabe cuándo se requiere una sintaxis de puntero y cuándo no, pero en Java y otros lenguajes, esto es confuso.

Antes de que Java existiera o se generalizara, muchos programadores aprendieron OO en C ++ sin punteros, pasando por valor o por referencia cuando sea necesario. Cuando se pasa de las aplicaciones de aprendizaje a las de negocios, suelen utilizar punteros de objetos. La biblioteca QT es un buen ejemplo de eso.

Cuando aprendí Java, traté de seguir el concepto de que todo es un objeto, pero me confundí con la codificación. Eventualmente, dije "ok, estos son objetos asignados dinámicamente con un puntero con la sintaxis de un objeto asignado estáticamente", y no tuve problemas para codificar, nuevamente.

umlcat
fuente
3

Java y C # controlan la memoria de bajo nivel de usted. El "montón" donde residen los objetos que crea vive su propia vida; por ejemplo, el recolector de basura cosecha objetos cuando lo prefiere.

Como hay una capa separada de indirección entre su programa y ese "montón", las dos formas de referirse a un objeto, por valor y por puntero (como en C ++), se vuelven indistinguibles : siempre se refiere a objetos "por puntero" en algún lugar del montón. Es por eso que este enfoque de diseño hace que el paso por referencia sea la semántica predeterminada de la asignación. Java, C #, Ruby, etc.

Lo anterior solo concierne a los idiomas imperativos. En las lenguas mencionadas, en el control de la memoria se pasa al tiempo de ejecución, pero el diseño de lenguajes también dice "bueno, pero en realidad, no es la memoria, y no son los objetos, y hacer ocupar la memoria". Los lenguajes funcionales se resumen aún más, al excluir el concepto de "memoria" de su definición. Es por eso que el paso por referencia no se aplica necesariamente a todos los idiomas en los que no controlas la memoria de bajo nivel.

P Shved
fuente
2

Se me ocurren algunas razones:

  • Copiar tipos primitivos es trivial, generalmente se traduce en una instrucción de máquina.

  • Copiar objetos no es trivial, el objeto puede contener miembros que son objetos en sí mismos. Copiar objetos es costoso en tiempo de CPU y memoria. Incluso hay múltiples formas de copiar un objeto según el contexto.

  • Pasar objetos por referencia es barato y también resulta útil cuando desea compartir / actualizar la información del objeto entre varios clientes del objeto.

  • Las estructuras de datos complejas (especialmente aquellas que son recursivas) requieren punteros. Pasar objetos por referencia es solo una forma más segura de pasar punteros.

jeroko
fuente
1

Porque de lo contrario, la función debería poder crear automáticamente una copia (obviamente profunda) de cualquier tipo de objeto que se le pase. Y generalmente no se puede adivinar para lograrlo. Por lo tanto, tendría que definir la implementación del método copiar / clonar para todos sus objetos / clases.

David
fuente
¿Podría simplemente hacer una copia superficial y mantener los valores y punteros reales a otros objetos?
Gustavo Cardoso
#Gustavo Cardoso, entonces podría modificar otros objetos a través de este, ¿es lo que esperaría de un objeto NO pasado como referencia?
David
0

Debido a que Java fue diseñado como un mejor C ++, y C # fue diseñado como un mejor Java, y los desarrolladores de estos lenguajes estaban cansados ​​del modelo de objetos C ++ fundamentalmente roto, en el que los objetos son tipos de valores.

Dos de los tres principios fundamentales de la programación orientada a objetos son la herencia y el polimorfismo, y tratar los objetos como tipos de valor en lugar de tipos de referencia causa estragos en ambos. Cuando pasa un objeto a una función como parámetro, el compilador necesita saber cuántos bytes pasar. Cuando su objeto es un tipo de referencia, la respuesta es simple: el tamaño de un puntero, igual para todos los objetos. Pero cuando su objeto es un tipo de valor, tiene que pasar el tamaño real del valor. Como una clase derivada puede agregar nuevos campos, esto significa sizeof (derivado)! = Sizeof (base), y el polimorfismo desaparece.

Aquí hay un programa trivial de C ++ que demuestra el problema:

#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 salida de este programa no es lo que sería para un programa equivalente en cualquier lenguaje OO sano, porque no puede pasar un objeto derivado por valor a una función que espera un objeto base, por lo que el compilador crea un constructor de copias ocultas y pasa una copia de la parte principal del objeto secundario , en lugar de pasar el objeto secundario como le dijo que hiciera. Las trampas semánticas ocultas como esta son la razón por la cual se deben evitar los objetos por valor en C ++ y no es posible en casi todos los demás lenguajes OO.

Mason Wheeler
fuente
Muy buen punto. Sin embargo, me concentré en los problemas de retorno, ya que trabajar alrededor de ellos requiere bastante esfuerzo; Este programa se puede solucionar con la adición de un solo signo y: void foo (Parent & a)
Ant
OO realmente no funciona bien sin punteros
Gustavo Cardoso
-1.000000000000
P Shved
55
Es importante recordar que Java es paso por valor (pasa referencias de objeto por valor, mientras que las primitivas se pasan puramente por valor).
Nicole
3
@Pavel Shved: "¡dos es mejor que uno!" O, en otras palabras, más cuerda para ahorcarse.
Nicole
0

Porque de otro modo no habría polimorfismo.

En la programación OO, puede crear una Derivedclase más grande a partir de una Base, y luego pasarla a las funciones que esperan una Base. Bastante trivial ¿eh?

Excepto que el tamaño del argumento de una función es fijo y se determina en tiempo de compilación. Puede argumentar todo lo que quiera, el código ejecutable es así y los idiomas deben ejecutarse en un punto u otro (los idiomas puramente interpretados no están limitados por esto ...)

Ahora, hay una pieza de datos que está bien definida en una computadora: la dirección de una celda de memoria, generalmente expresada como una o dos "palabras". Es visible como punteros o referencias en lenguajes de programación.

Entonces, para pasar objetos de longitud arbitraria, lo más simple es pasar un puntero / referencia a este objeto.

Esta es una limitación técnica de la programación OO.

Pero dado que para los tipos grandes, generalmente prefiere pasar referencias de todos modos para evitar la copia, generalmente no se considera un gran golpe :)

Sin embargo, hay una consecuencia importante, en Java o C #, al pasar un objeto a un método, no tiene idea de si su objeto será modificado por el método o no. Hace que la depuración / paralelización sea más difícil, y este es el problema que los lenguajes funcionales y la referencia transparente están tratando de abordar -> copiar no es tan malo (cuando tiene sentido).

Matthieu M.
fuente
-1

La respuesta está en el nombre (bueno, casi de todos modos). Una referencia (como una dirección) solo se refiere a otra cosa, un valor es otra copia de otra cosa. Estoy seguro de que alguien probablemente ha mencionado algo para el siguiente efecto, pero habrá circunstancias en las que una y no la otra es adecuada (Seguridad de memoria frente a Eficiencia de memoria). Se trata de administrar la memoria, memoria, memoria ... ¡MEMORIA! :RE

Dark Star1
fuente
-2

Bien, no digo que esta sea exactamente la razón por la cual los objetos son tipos de referencia o pasados ​​por referencia, pero puedo darle un ejemplo de por qué es una muy buena idea a largo plazo.

Si no me equivoco, cuando hereda una clase en C ++, todos los métodos y propiedades de esa clase se copian físicamente en la clase secundaria. Sería como volver a escribir el contenido de esa clase dentro de la clase secundaria.

Esto significa que el tamaño total de los datos en su clase secundaria es una combinación de las cosas en la clase primaria y la clase derivada.

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;

}   

Lo que te mostraría:

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

Esto significa que si tiene una gran jerarquía de varias clases, el tamaño total del objeto, como se declara aquí, sería la combinación de todas esas clases. Obviamente, estos objetos serían considerablemente grandes en muchos casos.

La solución, creo, es crearla en el montón y usar punteros. Esto significa que el tamaño de los objetos de las clases con varios padres sería manejable, en cierto sentido.

Es por eso que usar referencias sería un método más preferible para hacer esto.

Dhaval Anjaria
fuente
1
Esto no parece ofrecer nada sustancial sobre los puntos hechos y explicados en 13 respuestas anteriores
mosquito