¿Cuándo quieres dos referencias al mismo objeto?


20

En Java específicamente, pero probablemente también en otros lenguajes: ¿cuándo sería útil tener dos referencias al mismo objeto?

Ejemplo:

Dog a = new Dog();
Dob b = a;

¿Hay alguna situación en la que esto sea útil? ¿Por qué sería esta una solución preferida para usar acuando quiera interactuar con el objeto representado por a?


Es la esencia detrás del patrón Flyweight .

@MichaelT ¿Podrías dar más detalles?
Bassinator

77
Si ay b siempre hace referencia a lo mismo Dog, entonces no tiene sentido. Si a veces podrían, entonces es bastante útil.
Gabe

Respuestas:


45

Un ejemplo es cuando desea tener el mismo objeto en dos listas separadas :

Dog myDog = new Dog();
List dogsWithRabies = new ArrayList();
List dogsThatCanPlayPiano = new ArrayList();

dogsWithRabies.add(myDog);
dogsThatCanPlayPiano.add(myDog);
// Now each List has a reference to the same dog

Otro uso es cuando tienes el mismo objeto jugando varios roles :

Person p = new Person("Bruce Wayne");
Person batman = p;
Person ceoOfWayneIndustries = p;

44
Lo siento, pero creo que para su ejemplo de Bruce Wayne hay una falla de diseño, Batman y el puesto de director ejecutivo deben ser roles para su persona.
Silviu Burcea

44
-1 por revelar la identidad secreta de Batman.
Ampt

2
@ Silviu Burcea - Estoy totalmente de acuerdo en que el ejemplo de Bruce Wayne no es un buen ejemplo. Por un lado, si una edición de texto global cambió el nombre 'ceoOfWayneIndustries' y 'batman' a 'p' (suponiendo que no haya conflictos de nombre o cambios de alcance), y la semántica del programa cambió, entonces su algo está roto. El objeto al que se hace referencia representa el significado real, no el nombre de la variable dentro del programa. Para tener una semántica diferente, es un objeto diferente o algo con más comportamiento que una referencia (que debería ser transparente) y, por lo tanto, no es un ejemplo de más de 2 referencias.
Gbulmer

2
Aunque el ejemplo de Bruce Wayne tal como está escrito podría no funcionar, creo que la intención declarada es correcta. Quizás un ejemplo más cercano podría ser Persona batman = new Persona("Batman"); Persona bruce = new Persona("Bruce Wayne"); Persona currentPersona = batman;: donde tiene múltiples valores posibles (o una lista de valores disponibles) y una referencia al valor actualmente activo / seleccionado.
Dan Puzey

1
@gbulmer: Yo diría que no puedes tener una referencia a dos objetos; la referencia currentPersonaapunta a un objeto u otro, pero nunca a ambos. En particular, podría ser fácilmente posible que nuncacurrentPersona esté configurado , en cuyo caso ciertamente no es una referencia a dos objetos. Yo diría que, en mi ejemplo, ambos y son referencias a la misma instancia, pero tienen un significado semántico diferente dentro del programa. brucebatmancurrentPersona
Dan Puzey

16

¡Esa es realmente una pregunta sorprendentemente profunda! La experiencia de C ++ moderno (y los lenguajes que toman de C ++ moderno, como Rust) sugieren que muy a menudo, ¡no quieres eso! Para la mayoría de los datos, desea una referencia única o única ("propietaria"). Esta idea es también la razón principal de los sistemas de tipo lineal .

Sin embargo, incluso entonces, por lo general, desea algunas referencias "prestadas" de corta duración que se utilizan para acceder a la memoria brevemente pero que no duran una fracción significativa del tiempo que existen los datos. Lo más común cuando pasa un objeto como argumento a una función diferente (¡los parámetros también son variables!):

void encounter(Dog a) {
  hissAt(a);
}

void hissAt(Dog b) {
  // ...
}

Un caso menos común cuando usa uno de dos objetos dependiendo de una condición, haciendo esencialmente lo mismo independientemente de cuál elija:

Dog a, b;
Dog goodBoy = whoseAGoodBoy ? a : b;
feed(goodBoy);
walk(goodBoy);
pet(goodBoy);

Volviendo a los usos más comunes, pero dejando atrás las variables locales, pasamos a los campos: por ejemplo, los widgets en los marcos de la GUI a menudo tienen widgets principales, por lo que su marco grande que contiene diez botones tendría al menos diez referencias apuntando a él (además de algunos más de su padre y quizás de oyentes de eventos, etc.). Cualquier tipo de gráfico de objetos, y algunos tipos de árboles de objetos (aquellos con referencias padre / hermano), tienen múltiples objetos que se refieren a cada uno de ellos. Y prácticamente cada conjunto de datos es en realidad un gráfico ;-)


3
El "caso menos común" es bastante común: tiene los objetos almacenados en una lista o un mapa, recupera el que desea y realiza las operaciones requeridas. No borra sus referencias del mapa o la lista.
SJuan76

1
@ SJuan76 El "caso menos común" se trata únicamente de tomar de variables locales, cualquier cosa que involucre estructuras de datos cae bajo el último punto.

¿Es este ideal de una sola referencia debido a la gestión de memoria de conteo de referencias? Si es así, vale la pena mencionar que la motivación no es relevante para otros lenguajes con diferentes recolectores de basura (por ejemplo, C #, Java, Python)
MarkJ

@MarkJ Los tipos lineales no son solo un ideal, gran parte del código existente se ajusta a él sin saberlo porque es lo semánticamente correcto en algunos casos. Tiene ventajas potenciales de rendimiento (pero ni para el conteo de referencias ni para el rastreo de GC, solo ayuda cuando usa una estrategia diferente que realmente explota ese conocimiento para omitir tanto los reembolsos como el rastreo). Más interesante es su aplicación a la gestión de recursos simple y determinista. Piense que RAII y C ++ 11 mueven la semántica, pero mejor (se aplica con más frecuencia y el compilador detecta los errores).

6

Variables temporales: considere el siguiente pseudocódigo.

Object getMaximum(Collection objects) {
  Object max = null;
  for (Object candidate IN objects) {
    if ((max is null) OR (candidate > max)) {
      max = candidate;
    }
  }
  return max;
}

Las variables maxy candidatepueden apuntar al mismo objeto, pero la asignación de variables cambia usando diferentes reglas y en diferentes momentos.


3

Para complementar las otras respuestas, también puede atravesar una estructura de datos de manera diferente, comenzando desde el mismo lugar. Por ejemplo, si tuviera un BinaryTree a = new BinaryTree(...); BinaryTree b = a, podría recorrer el camino más a la izquierda del árbol con ay su camino más a la derecha con b, usando algo como:

while (!a.equals(null) && !b.equals(null)) {
    a = a.left();
    b = b.right();
}

Ha pasado un tiempo desde que escribí Java, por lo que el código puede no ser correcto o sensato. Tómelo más como pseudocódigo.


3

Este método es excelente cuando tiene varios objetos que todos devuelven la llamada a otro objeto que puede usarse sin contexto.

Por ejemplo, si tiene una interfaz con pestañas, puede tener Tab1, Tab2 y Tab3. Es posible que también desee utilizar una variable común independientemente de la pestaña en la que se encuentre el usuario para simplificar su código y reducir la necesidad de averiguar sobre la marcha una y otra vez en qué pestaña se encuentra su usuario.

Tab Tab1 = new Tab();
Tab Tab2 = new Tab();
Tab Tab3 = new Tab();
Tab CurrentTab = new Tab();

Luego, en cada una de las pestañas numeradas en Click, puede cambiar CurrentTab para hacer referencia a esa pestaña.

CurrentTab = Tab3;

Ahora en su código puede llamar a "CurrentTab" con impunidad sin necesidad de saber en qué pestaña está realmente. También puede actualizar las propiedades de CurrentTab y fluirán automáticamente a la pestaña referenciada.


3

Hay muchos escenarios en los que b debe ser una referencia a un " a" desconocido para ser útil. En particular:

  • Cada vez que no sabes a qué bapunta en tiempo de compilación.
  • Cada vez que necesite iterar sobre una colección, ya sea que se conozca en el momento de la compilación o no
  • Cada vez que tenga un alcance limitado

Por ejemplo:

Parámetros

public void DoSomething(Thing &t) {
}

t es una referencia a una variable desde un ámbito externo.

Valores devueltos y otros valores condicionales

Thing a = Thing.Get("a");
Thing b = Thing.Get("b");
Thing biggerThing = Thing.max(a, b);
Thing z = a.IsMoreZThan(b) ? a : b;

biggerThingy zson referencias a ao b. No sabemos cuál en tiempo de compilación.

Lambdas y sus valores de retorno

Thing t = someCollection.FirstOrDefault(x => x.whatever > 123);

xes un parámetro (ejemplo 1 anterior) y tes un valor de retorno (ejemplo 2 anterior)

Colecciones

indexByName.add(t.name, t);
process(indexByName["some name"]);

index["some name"]es, en gran medida, un aspecto más sofisticado b. Es un alias para un objeto que se creó y se insertó en la colección.

Bucles

foreach (Thing t in things) {
 /* `t` is a reference to a thing in a collection */
}

t es una referencia a un elemento devuelto (ejemplo 2) por un iterador (ejemplo anterior).


Sus ejemplos son difíciles de seguir. No me malinterpreten, los superé, pero tuve que trabajar en ello. En el futuro, sugiero separar los ejemplos de código en bloques individuales (con algunas notas que expliquen específicamente cada uno), sin poner algunos ejemplos no relacionados en un bloque.
Bassinator

1
@HCBPshenanigans No espero que cambie las respuestas seleccionadas; pero, he actualizado el mío para ayudar a la legibilidad y completar algunos de los casos de uso que faltan en la respuesta seleccionada.
svidgen

2

Es un punto crucial, pero en mi humilde opinión vale la pena entenderlo.

Todos los lenguajes OO siempre hacen copias de referencias, y nunca copian un objeto 'invisiblemente'. Sería mucho más difícil escribir programas si los lenguajes OO funcionaran de otra manera. Por ejemplo, funciones y métodos, nunca podrían actualizar un objeto. Java, y la mayoría de los lenguajes OO serían casi imposibles de usar sin una complejidad adicional significativa.

Se supone que un objeto en un programa tiene algún significado. Por ejemplo, representa algo específico en el mundo físico real. Por lo general, tiene sentido tener muchas referencias a lo mismo. Por ejemplo, la dirección de mi casa se puede dar a muchas personas y organizaciones, y esa dirección siempre se refiere a la misma ubicación física. Entonces, el primer punto es que los objetos a menudo representan algo que es específico, real o concreto; y así poder tener muchas referencias a lo mismo es extremadamente útil. De lo contrario, sería más difícil escribir programas.

Cada vez que pasa acomo argumento / parámetro a otra función, por ejemplo, llamando
foo(Dog aDoggy);
o aplicando un método a, el código del programa subyacente hace una copia de la referencia, para producir una segunda referencia al mismo objeto.

Además, si el código con una referencia copiada está en un hilo diferente, entonces ambos pueden usarse simultáneamente para acceder al mismo objeto.

Entonces, en la mayoría de los programas útiles, habrá múltiples referencias al mismo objeto, porque esa es la semántica de la mayoría de los lenguajes de programación OO.

Ahora, si lo pensamos bien, dado que pasar por referencia es el único mecanismo disponible en muchos lenguajes OO (C ++ admite ambos), podríamos esperar que sea el comportamiento predeterminado 'correcto' .

En mi humilde opinión, el uso de referencias es el valor predeterminado correcto , por un par de razones:

  1. Garantiza que el valor de un objeto utilizado en dos lugares diferentes es el mismo. Imagine colocar un objeto en dos estructuras de datos diferentes (matrices, listas, etc.) y realizar algunas operaciones en un objeto que lo cambia. Eso podría ser una pesadilla para depurar. Más importante aún, es el mismo objeto en ambas estructuras de datos, o el programa tiene un error.
  2. Puede refactorizar felizmente el código en varias funciones, o fusionar el código de varias funciones en una sola, y la semántica no cambia. Si el lenguaje no proporcionara semántica de referencia, sería aún más complejo modificar el código.

También hay un argumento de eficiencia; hacer copias de objetos completos es menos eficiente que copiar una referencia. Sin embargo, creo que se pierde el punto. Las referencias múltiples al mismo objeto tienen más sentido y son más fáciles de usar, ya que coinciden con la semántica del mundo físico real.

Entonces, en mi humilde opinión, generalmente tiene sentido tener múltiples referencias al mismo objeto. En los casos inusuales en los que eso no tiene sentido en el contexto de un algoritmo, la mayoría de los idiomas proporcionan la capacidad de hacer un 'clon' o copia profunda. Sin embargo, ese no es el valor predeterminado.

Creo que las personas que argumentan que este no debería ser el predeterminado están utilizando un lenguaje que no proporciona recolección de basura automática. Por ejemplo, anticuado C ++. El problema es que necesitan encontrar una manera de recolectar objetos 'muertos' y no reclamar objetos que aún pueden ser necesarios; tener múltiples referencias al mismo objeto lo hace difícil.

Creo que si C ++ tuvo una recolección de basura de costo suficientemente bajo, de modo que todos los objetos a los que se hace referencia son recolección de basura, entonces gran parte de la objeción desaparece. Todavía habrá algunos casos en los que la semántica de referencia no es lo que se necesita. Sin embargo, en mi experiencia, las personas que pueden identificar esas situaciones también suelen ser capaces de elegir la semántica adecuada de todos modos.

Creo que hay alguna evidencia de que una gran cantidad de código en un programa C ++ está ahí para manejar o mitigar la recolección de basura. Sin embargo, escribir y mantener ese tipo de código 'infraestructural' agrega costos; está ahí para hacer que el lenguaje sea más fácil de usar o más robusto. Entonces, por ejemplo, el lenguaje Go está diseñado con un enfoque en remediar algunas de las debilidades de C ++, y no tiene otra opción que la recolección de basura.

Por supuesto, esto es irrelevante en el contexto de Java. También fue diseñado para ser fácil de usar, y también lo ha sido la recolección de basura. Por lo tanto, tener múltiples referencias es la semántica predeterminada, y es relativamente seguro en el sentido de que los objetos no se reclaman mientras haya una referencia a ellos. Por supuesto, una estructura de datos podría retenerlos porque el programa no se ordena correctamente cuando realmente ha terminado con un objeto.

Entonces, volviendo a su pregunta (con un poco de generalización), ¿cuándo querría más de una referencia al mismo objeto? Prácticamente en cada situación que se me ocurre. Son la semántica predeterminada del mecanismo de paso de parámetros de la mayoría de los idiomas. Sugiero que se debe a que la semántica predeterminada de manejar objetos que existe en el mundo real tiene que ser básicamente por referencia (porque los objetos reales están ahí fuera).

Cualquier otra semántica sería más difícil de manejar.

Dog a = new Dog("rover");  // initialise with name 
DogList dl = new DogList()
dl.add(a)
...
a.setOwner("Mr Been")

Sugiero que el "rover" en dldebería ser el afectado setOwnero los programas se volverán difíciles de escribir, comprender, depurar o modificar. Creo que la mayoría de los programadores estarían perplejos o consternados de lo contrario.

luego se vende el perro:

soldDog = dl.lookupOwner("rover", "Mr Been")
soldDog.setOwner("Mr Mcgoo")

Este tipo de procesamiento es común y normal. Por lo tanto, la semántica de referencia es la predeterminada porque generalmente tiene más sentido.

Resumen: siempre tiene sentido tener múltiples referencias al mismo objeto.


buen punto, pero esto es más adecuado como comentario
Bassinator

@HCBPshenanigans: creo que el punto podría haber sido demasiado breve y dejar demasiado sin decir. Así que me he expandido en el razonamiento. Creo que es una respuesta 'meta' a su pregunta. En resumen, las referencias múltiples al mismo objeto son cruciales para que los programas sean fáciles de escribir porque muchos objetos en un programa representan instancias específicas o únicas de cosas en el mundo real.
Gbulmer

1

Por supuesto, otro escenario en el que podrías terminar con:

Dog a = new Dog();
Dog b = a;

es cuando mantiene el código y bsolía ser un perro diferente, o una clase diferente, pero ahora es atendido por a.

En general, en el mediano plazo, debe volver a trabajar todo el código para referirse adirectamente, pero eso puede no suceder de inmediato.


1

Desearía esto cada vez que su programa tenga la posibilidad de extraer una entidad en la memoria en más de un lugar, posiblemente porque diferentes componentes lo están utilizando.

Identity Maps proporcionó una tienda local definitiva de entidades para que podamos evitar tener dos o más representaciones separadas. Cuando representamos el mismo objeto dos veces, nuestro cliente corre el riesgo de causar un problema de concurrencia si una referencia del objeto persiste sus cambios de estado antes que la otra instancia. La idea es que queremos asegurarnos de que nuestro cliente siempre esté tratando la referencia definitiva a nuestra entidad / objeto.


0

Usé esto cuando escribía un solucionador de Sudoku. Cuando sé el número de una celda al procesar filas, quiero que la columna que contiene sepa también el número de esa celda cuando procesa columnas. Entonces, tanto las columnas como las filas son matrices de objetos Cell que se superponen. Exactamente como mostró la respuesta aceptada.


-2

En las aplicaciones web, Object Relational Mappers puede usar la carga diferida para que todas las referencias al mismo objeto de la base de datos (al menos dentro del mismo hilo) apunten a lo mismo.

Por ejemplo, si tuvieras dos tablas:

Perros:

  • id | propietario_id | nombre
  • 1 | 1 | Corteza Kent

Propietarios:

  • id | nombre
  • 1 | yo
  • 2 | tú

Hay varios enfoques que su ORM puede hacer si se realizan las siguientes llamadas:

dog = Dog.find(1)  // fetch1
owner = Owner.find(1) // fetch2
superdog = owner.dogs.first() // fetch3
superdog.name = "Superdog"
superdog.save! // write1
owner = dog.owner // fetch4
owner.name = "Mark"
owner.save! // write2
dog.owner = Owner.find(2)
dog.save! // write3

En la estrategia ingenua, todas las llamadas a los modelos y las referencias relacionadas recuperan objetos separados. Dog.find(), Owner.find(), owner.dogs, Y el dog.ownerresultado en una base de datos alcanzó la primera vez, después de lo cual se guardan en la memoria. Y entonces:

  • la base de datos se obtiene al menos 4 veces
  • dog.owner no es lo mismo que superdog.owner (obtenido por separado)
  • dog.name no es lo mismo que superdog.name
  • dog y superdog intentan escribir en la misma fila y se sobrescribirán los resultados de cada uno: write3 deshacerá el cambio de nombre en write1.

Sin una referencia, tiene más recuperaciones, usa más memoria e introduce la posibilidad de sobrescribir actualizaciones anteriores.

Suponga que su ORM sabe que todas las referencias a la fila 1 de la tabla perros deben apuntar a lo mismo. Luego:

  • fetch4 puede eliminarse ya que hay un objeto en la memoria correspondiente a Owner.find (1). fetch3 aún resultará en al menos un escaneo de índice, ya que puede haber otros perros propiedad del propietario, pero no activará una recuperación de fila.
  • perro y superperro apuntan al mismo objeto.
  • dog.name y superdog.name apuntan al mismo objeto.
  • dog.owner y superdog.owner apuntan al mismo objeto.
  • write3 no sobrescribe el cambio en write1.

En otras palabras, el uso de referencias ayuda a codificar el principio de que un solo punto de verdad (al menos dentro de ese hilo) es la fila en la base de datos.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.