Durante el día de hoy, hemos estado dando vueltas, tanto Daniel como yo, para ver si encontrábamos alguna forma de solucionar este problema que se nos había cruzado:
irb> a = [[1,2,3], [1,2,3]]
[[1,2,3], [1,2,3]]
irb> a = b
[[1,2,3], [1,2,3]]
irb> b[][] = 0
0
irb> b
[[,2,3], [1,2,3]]
irb> a
[[,2,3], [1,2,3]]
Con este código se entiende que al igualar dos objetos, en Ruby, no se hace una copia del objeto, sino una referencia al mismo.
Si intentamos hacer algo como:
irb> b = a.clone
irb> c = a.dup
Nos encontramos, al realizar la prueba de modificación sobre b y c el mismo resultado. Esto es porque los comandos de duplicación de objetos (y clonación) no trabajan con recursividad, sino que hacen solo la duplicidad de los objetos inmediatos, por tanto, si se trata de un Array, se duplica como nuevo objeto el Array, pero cada elemento dentro del Array, si es a su vez otro Array o Hash, los elementos que este puede contener se dejan sin duplicar (o clonar).
Esto está así pensado para que cada cual agregue sus propias funciones de clonación (en caso de clone). En ese caso, para los objetos de tipo Array y Hash se les olvidó hacerlo, claro.
Solución cutre
Buscando un poco por internet, en varios foros se puede encontrar esta solución, la cual es algo chapuza en muchos aspectos:
irb> b = Marshal.load(Marshal.dump(a))
[[1,2,3], [1,2,3]]
Esto lo que hace es realizar una serialización de los objetos y después una deserialización. Funciona, pero no con todos los tipos de objetos, hay que tener especial cuidado con esto.
Solución algo más elegante
Lo ideal sería sobrecargar la función de clone para los objetos de Array y Hash, ya que se ve que se les olvidó hacerlo a los programadores o, realmente, no se preocuparon de hacer esa tarea en profundidad, es decir, a través de todos los objetos.
class Array
def clone
a = Array.new
for i in ..(size - 1) do
if self[i].respond_to? :clone
a[i] = self[i].clone
else
a[i] = self[i]
end
end
a
end
end
Si se ejecuta con el ejemplo, se verá que se suceden algunos errores. Esto es debido a que los objetos como Fixnum, tienen implementado el objeto clone, pero como un error, ya que lógicamente se considera que el objeto Fixnum no se puede clonar.
Esto se puede resolver de dos formas. Agregar tantas excepciones como se encuentren en la función clone escrita antes, o escribir algo como esto:
class Fixnum
def clone
self
end
end
Conclusión
Cada lenguaje tiene ciertas características o lagunas que, cuando se choca con ellas, se convierten en verdaderos escollos en el camino. No obstante, siempre se puede salir de ellos de alguna forma, aunque haya que poner momentáneamente FIXME en los comentarios de nuestro código a fin de revisarlo cuando se tenga una solución algo mejor.