En la programación orientada a objetos , la copia de objetos es el acto de crear e inicializar un nuevo objeto en función del estado de un objeto existente. Las distintas formas de implementar la copia tienen implicaciones que un programador debe comprender para escribir un programa informático que sea correcto y eficaz.
La copia permite utilizar e incluso modificar el estado emergente del objeto original (representado por su estado interno) sin afectar al objeto original.
En general, un objeto se asemeja a un concepto monolítico, pero tiene una estructura interna compuesta por datos : un árbol de estados. Se han desarrollado varias estrategias para copiar este estado interno en función de las necesidades del programa y el costo de tiempo de ejecución.
Las primeras que se analizaron fueron la copia superficial y la copia profunda, cuya terminología se remonta a Smalltalk -80. [1]
Una distinción similar se aplica a la comparación de objetos para determinar su igualdad . Para que dos objetos sean iguales, su estado debe ser el mismo de manera significativa. Dos objetos podrían considerarse iguales si sus campos son iguales sin atravesar subobjetos (superficial). O tal vez se consideren iguales solo si el estado es igual en todo el árbol de objetos (profundo). [ Aclaración necesaria ]
Si dos variables contienen el mismo valor de referencia, entonces claramente se refieren al mismo objeto, lo cual es incluso más específico que igual.
Incluso más superficial que la copia superficial, copiar una referencia es una forma de copiar un objeto. Esta estrategia se emplea comúnmente cuando se pasa un objeto a un método. La referencia se pasa por valor: una copia del valor de referencia (probablemente una dirección).
La copia superficial implica crear un nuevo objeto no inicializado, B, y copiar cada valor de campo del original, A. [2] [3] [4] Debido a este procedimiento, esto también se conoce como copia campo por campo , [5] [6] [7] copia campo por campo o copia de campo . [8] Si el valor del campo es un tipo primitivo (como int), el valor se copia de manera que los cambios en el valor en B no afecten al valor en A. Si el valor del campo es una referencia a un objeto (por ejemplo, una dirección de memoria), la referencia se copia, por lo tanto, hace referencia al mismo objeto que A. Cambiar el estado del objeto interno afecta el estado emergente de A y B ya que los objetos se comparten. En un lenguaje sin tipos primitivos (donde todo es un objeto), todos los campos de la copia hacen referencia a los mismos objetos que los campos del original.
Una copia superficial suele ser relativamente sencilla de implementar y su ejecución es económica desde el punto de vista computacional. Por lo general, se puede implementar simplemente copiando un bloque contiguo de memoria.
La copia profunda implica copiar el estado de todos los objetos subordinados, desreferenciando recursivamente las referencias de objetos en cada nivel del árbol que es el estado del objeto original y creando nuevos objetos y copiando campos. Una modificación del objeto original o copiado, incluidos sus objetos internos, no afecta al otro ya que no comparten contenido.
En casos más complejos, algunos campos de una copia deberían tener valores compartidos con el objeto original (como en una copia superficial), lo que corresponde a una relación de asociación ; y algunos campos deberían tener copias (como en una copia profunda), lo que corresponde a una relación de agregación . En estos casos, generalmente se requiere una implementación personalizada de la copia; este problema y su solución datan de Smalltalk-80. [9] Alternativamente, los campos se pueden marcar como que requieren una copia superficial o una copia profunda, y las operaciones de copia se pueden generar automáticamente (lo mismo para las operaciones de comparación). [ 10] Sin embargo, esto no está implementado en la mayoría de los lenguajes orientados a objetos, aunque hay soporte parcial en Eiffel. [10]
La copia diferida, relacionada con la copia al escribir , es una implementación de una copia profunda. Cuando se copia inicialmente un objeto, se realiza una copia superficial relativamente rápida. También se utiliza un contador para realizar un seguimiento de cuántos objetos comparten los datos. Cuando el programa desea modificar un objeto, puede determinar si los datos se comparten (examinando el contador) y puede realizar una copia profunda si es necesario.
La copia diferida proporciona la semántica de una copia profunda, pero aprovecha la velocidad de una copia superficial cuando es posible. La desventaja son los costos base bastante altos pero constantes debido al contador. Las referencias circulares pueden causar problemas.
En general, un lenguaje de programación orientado a objetos proporciona una forma de copiar un objeto. Un programador debe definir cómo se copia un objeto personalizado, así como también debe definir si dos objetos son iguales, comparables, etc.
Algunos lenguajes admiten una o ambas estrategias, superficiales o profundas, y definen una operación de copia u operaciones superficiales y profundas separadas. [10] Muchos lenguajes proporcionan algún comportamiento predeterminado.
En Java, siempre se accede a un objeto de forma indirecta (a través de una referencia) . Un objeto nunca se crea de forma implícita, sino que siempre se pasa o se asigna mediante una variable de referencia.
Los parámetros se pasan por valor, sin embargo, es el valor de la referencia el que se pasa. [11]
La máquina virtual Java administra la recolección de basura para que los objetos se limpien cuando ya no sean accesibles.
El lenguaje no proporciona una forma automática de copiar un objeto.
La copia se realiza normalmente mediante un método clone() . Este método suele llamar al método clone() de su clase padre para obtener una copia y, a continuación, realiza los procedimientos de copia personalizados. Finalmente, se llega al método clone() del objeto superior ( Object
), que crea una nueva instancia de la misma clase que el objeto y copia todos los campos en la nueva instancia (una copia superficial). Si se utiliza este método, la clase debe implementar la Cloneable
interfaz o, de lo contrario, se generará una "Excepción de clonación no compatible". Después de obtener una copia de la clase padre, el método clone() de una clase puede proporcionar una capacidad de clonación personalizada, como una copia profunda (es decir, duplicar algunas de las estructuras a las que hace referencia el objeto) o dar a la nueva instancia una nueva ID única.
El tipo de retorno de clone() es Object
, pero los implementadores de un método clone podrían escribir el tipo del objeto que se está clonando debido al soporte de Java para tipos de retorno covariantes . Una ventaja de usar clone() es que, dado que es un método reemplazable , podemos llamar a clone() en cualquier objeto, y usará el método clone() de su clase, sin que el código de llamada necesite saber cuál es esa clase (lo que sería necesario con un constructor de copia).
Una desventaja es que a menudo no se puede acceder al método clone() en un tipo abstracto. La mayoría de las interfaces y clases abstractas en Java no especifican un método clone() público. Por lo tanto, a menudo la única forma de usar el método clone() es si se conoce la clase de un objeto, lo que es contrario al principio de abstracción de usar el tipo más genérico posible. Por ejemplo, si uno tiene una referencia List en Java, no se puede invocar clone() en esa referencia porque List no especifica ningún método clone() público. Las implementaciones de List como Array List y Linked List generalmente tienen métodos clone(), pero es inconveniente y una mala abstracción llevar consigo el tipo de clase de un objeto.
Otra forma de copiar objetos en Java es serializarlos a través de la Serializable
interfaz. Esto se utiliza normalmente para fines de persistencia y protocolos de conexión , pero crea copias de objetos y, a diferencia de la clonación, una copia profunda que maneja con elegancia gráficos cíclicos de objetos está disponible con un mínimo esfuerzo por parte del programador.
Ambos métodos sufren un problema importante: el constructor no se utiliza para objetos copiados con clonación o serialización. Esto puede generar errores con datos inicializados incorrectamente, impide el uso de final
campos miembro y dificulta el mantenimiento. Algunas utilidades intentan superar estos problemas mediante el uso de la reflexión para realizar copias profundas de objetos, como la biblioteca de clonación profunda. [12]
Los objetos de tiempo de ejecución en Eiffel son accesibles indirectamente a través de referencias o como objetos expandidos cuyos campos están incrustados dentro de los objetos que los utilizan. Es decir, los campos de un objeto se almacenan externa o internamente .
La clase Eiffel ANY
contiene funciones para la copia superficial y profunda y la clonación de objetos. Todas las clases Eiffel heredan de ANY
, por lo que estas funciones están disponibles en todas las clases y son aplicables tanto a objetos de referencia como a objetos expandidos.
La copy
función realiza una copia superficial, campo por campo, de un objeto a otro. En este caso, no se crea ningún objeto nuevo. Si y
se copiaron a x
, entonces los mismos objetos a los que hacía referencia y
antes de la aplicación de copy
, también serán referenciados por x
después de que se complete la copy
función.
Para crear un nuevo objeto que sea un duplicado superficial de y
, se utiliza la función twin
. En este caso, se crea un nuevo objeto con sus campos idénticos a los de la fuente.
La función twin
depende de la función copy
, que puede redefinirse en los descendientes de ANY
, si es necesario. El resultado de twin
es del tipo anclado like Current
.
La copia profunda y la creación de gemelos profundos se pueden realizar utilizando las características deep_copy
y deep_twin
, nuevamente heredadas de la clase ANY
. Estas características tienen el potencial de crear muchos objetos nuevos, porque duplican todos los objetos en una estructura de objetos completa. Debido a que se crean nuevos objetos duplicados en lugar de simplemente copiar referencias a objetos existentes, las operaciones profundas se convertirán en una fuente de problemas de rendimiento con mayor facilidad que las operaciones superficiales.
En C# , en lugar de utilizar la interfaz ICloneable
, se puede utilizar un método de extensión genérico para crear una copia profunda mediante reflexión. Esto tiene dos ventajas: en primer lugar, proporciona la flexibilidad de copiar cada objeto sin tener que especificar manualmente cada propiedad y variable que se va a copiar. En segundo lugar, debido a que el tipo es genérico, el compilador garantiza que el objeto de destino y el objeto de origen tengan el mismo tipo. [ cita requerida ]
En Objective-C , los métodos copy
y mutableCopy
son heredados por todos los objetos y están destinados a realizar copias; el último es para crear un tipo mutable del objeto original. Estos métodos a su vez llaman a los métodos copyWithZone
y mutableCopyWithZone
, respectivamente, para realizar la copia. Un objeto debe implementar el método correspondiente copyWithZone
para poder copiarlo. [ cita requerida ]
En OCaml , la función de biblioteca Oo.copy realiza una copia superficial de un objeto.
En Python , el módulo de copia de la biblioteca proporciona una copia superficial y una copia profunda de objetos a través de las funciones copy()
y deepcopy()
, respectivamente. [13] Los programadores pueden definir métodos especiales __copy__()
y __deepcopy__()
en un objeto para proporcionar una implementación de copia personalizada.
En Ruby , todos los objetos heredan dos métodos para realizar copias superficiales: clone y dup. Los dos métodos se diferencian en que clone
copian el estado contaminado, el estado congelado y cualquier método singleton que pueda tener un objeto, mientras que dup
copian solo su estado contaminado. Las copias profundas se pueden lograr volcando y cargando el flujo de bytes de un objeto o la serialización YAML.[1] Alternativamente, puede usar la gema deep_dive para hacer una copia profunda controlada de sus gráficos de objetos. [2]
En Perl , las estructuras anidadas se almacenan mediante el uso de referencias, por lo que un desarrollador puede recorrer toda la estructura y volver a referenciar los datos o utilizar la dclone()
función del módulo Storable.
En VBA , una asignación de variables de un tipo Object
es una copia superficial, una asignación para todos los demás tipos (tipos numéricos, String, tipos definidos por el usuario, matrices) es una copia profunda. Por lo tanto, la palabra clave Set
para una asignación indica una copia superficial y la palabra clave (opcional) Let
indica una copia profunda. No existe un método integrado para realizar copias profundas de objetos en VBA. [ cita requerida ]
shallowCopy
); si los valores se copian, entonces no se comparten ( deepCopy
)."copy
es shallowCopy
. En las subclases en las que la copia debe dar como resultado una combinación especial de variables compartidas y no compartidas, el método asociado con la copia suele reimplementarse, en lugar del método asociado con shallowCopy
o deepCopy
."