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?
programming-languages
object-oriented
Gustavo Cardoso
fuente
fuente
Respuestas:
Las razones básicas se reducen a esto:
Por lo tanto, referencias.
fuente
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.
fuente
En C ++, tiene dos opciones principales: retorno por valor o retorno por puntero. Veamos el primero:
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:
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:
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:
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.
fuente
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
Class
objetos).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
transient
dónde sea apropiado.fuente
clone
está 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 declone
.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.fuente
const
su 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.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
ref
palabra clave, este comportamiento funciona como se esperaba.fuente
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.
fuente
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.
fuente
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.
fuente
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.
fuente
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:
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.
fuente
Porque de otro modo no habría polimorfismo.
En la programación OO, puede crear una
Derived
clase más grande a partir de unaBase
, y luego pasarla a las funciones que esperan unaBase
. 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).
fuente
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
fuente
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
Lo que te mostraría:
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.
fuente