Shallow Copy y Deep Copy en C#
Introducción
Cuando trabajamos con objetos en cualquier lenguaje de programación, tendemos a trabajar con ellos, realizar operaciones con o sobre ellos, modificar sus datos, etc.
Hace unos años publiqué una entrada sobre la inmutabilidad de objetos y sus propiedades en C#.
En aquella entrada abordaba la inmutabilidad de objetos y propiedades, y cómo podemos creer estar ante un objeto y propiedad inmutable pero no ser realmente así sino consideramos algunas cosas.
La idea de un objeto inmutable es que su estado y valores sean establecidas en la inicialización del objeto, y que una vez inicializado éste, no debería permitirse su modificación.
De la misma forma, la inmutabilidad de propiedades hace referencia a que sus propiedades sean de sólo lectura.
Ambas combinaciones deben permanecer sólidas para asegurar la inmutabilidad de un objeto, pero dentro del mundo de la programación orientada a objetos existen otras consideraciones cuando trabajamos con objetos y propiedades, y hacer que una propiedad sea de sólo lectura no nos asegura que sea inmutable como demostré en aquella ocasión.
Es por lo tanto nuestra obligación como desarrolladores Software, considerar todos los escenarios posibles, incluida la manipulación o copia de objetos, pudiendo encontrarnos con situaciones que debemos controlar.
Lo que a continuación voy a explicar tiene relación con inmutabilidad y con lo que se denomina como Shallow Copy y Deep Copy.
Vamos con ello.
Tipos por valor y tipos por refencia
Una de las consideraciones más importantes es entender el concepto de tipos por valor, tipos por referencia, y cuando aplican.
Seguramente lo sepas, pero por si acaso, voy a hacer un repaso muy rápido sobre estos conceptos.
Un tipo por valor contiene el valor o dato en sí mismo.
Su valor es asignado de forma directa e inmutable.
Un tipo por referencia contiene una referencia o puntero a su dato.
Por lo que apuntamos a ese dato, que en el caso de que cambie, su puntero es el que apunta al dato, por lo que si hacemos referencia a él desde más de un sitio, ambos sitios apuntarán a la dirección de memoria en la que se encuentra el dato.
Los tipos por valor pueden ser de dos clases: tipo de estructura, y tipo enumerable.
También tenemos en este grupo a los tipos simples (tipos numéricos, en coma flotante, bool, char), y las variables.
Los tipos por referencia pueden ser: clases, interfaces, delegados, tipos dynamic, object o string.
Shallow Copy y Deep Copy
Una vez sentadas o repasadas las bases principales, inmutabilidad, tipos por valor y tipos por referencia, vamos a hablar de estos dos conceptos.
Estos conceptos vienen de la época de SmallTalk, aproximadamente hablo de la década de los 80.
Términos y conceptos aplicables a la programación orientada a objetos actual.
Ambos términos, tienen que ver o están relacionados con la copia de objetos, pero ambos son diferentes.
En la imagen que acompaña la cabecera de esta entrada se resumen visualmente ambos, pero hablaré más sobre ellos para entender mejor su alcance y consecuencias de forma práctica.
Copia de objetos
A la hora de copiar objetos, es necesario tener en consideración si nos encontramos ante un tipo por valor o un tipo por referencia, ya que el comportamiento y objetivos que buscamos, pueden verse afectados.
Supongamos esta clase inmutable:
public class MyImmutableObject { private readonly string _name; public string Name => _name; private readonly List _things; public List Things => _things; public MyImmutableObject(string name, List things) { _name = name; _things = things; } }
La naturaleza del tipo por referencia Things que está contenido en el objeto inmutable, no me impide que haga algo parecido a:
var immutableObject = new MyImmutableObject("Juan", new List() { "Uno", "Dos" }); immutableObject.Things.Add("Other");
Esta operación que hemos realizado es un acceso directo al objeto, el cuál es un tipo por referencia, es decir, un puntero al objeto.
Por lo que operaciones como estas, aunque la propiedad sea de sólo lectura, están permitidas cuando trabajamos con C#.
Algo similar ocurre cuando hacemos esto otro:
var immutableObject = new MyImmutableObject("Juan", new List() { "Uno", "Dos" }); var things = immutableObject.Things; things.Add("Other");
Aquí, lo que estamos haciendo es algo parecido a lo anterior, aunque sensiblemente diferente a primera vista.
Para entender bien este comportamiento y lo que tiene que ver con Shallow Copy y Deep Copy, lo que se establece por debajo en lo que hemos visto es lo que se denomina como Shallow Copy o Copia Superficial.
Existen lenguajes de programación que por defecto funcionan así, y otros, permiten indicar el tipo de copia que queremos realizar.
En nuestro caso y por defecto en C#, realizamos una copia superficial.
Copiamos el puntero al dato por ser un tipo por referencia, pudiendo actuar sobre él directamente.
Como podemos observar, si deseamos trabajar con objetos inmutables, esto se nos viene ligeramente abajo en lo que a inmutabilidad se refiere cuando trabajamos con tipos por referencia.
¿Cómo resolver esto?.
Pues básicamente realizando una Deep Copy o Copia Profunda de nuestra colección.
Una Deep Copy establece una copia de los elementos que forman parte del objeto, pero omitiendo la copia de sus referencias.
Es decir, creando un nuevo objeto en memoria, copiando o clonando su original, lo que representa un snapshoot del objeto en sí mismo en un momento dado.
La forma más sencilla de hacer un Deep Copy es recorriendo todos y cada uno de los elementos que forman parte de nuestro objeto original para crear el nuevo objeto.
Sin embargo, podríamos incurrir en creer que estamos haciendo un Deep Copy de un tipo por referencia cuando en realidad estaríamos haciendo un Shallow Copy.
Pongamos en práctica esto que comento con el siguiente código para enteder mejor esta implicación:
public class MyImmutableObject { private readonly string _name; public string Name => _name; private readonly List _things; public List Things => _things; public MyImmutableObject(string name, List things) { _name = name; _things = things; } } public static void Main(string[] args) { var immutableObject = new MyImmutableObject("Juan", new List() { "Uno", "Dos" }); var otherImmutableObject = new MyImmutableObject("Luis", immutableObject.Things); otherImmutableObject.Things.Add("Other"); Console.WriteLine(immutableObject.Name); foreach (var item in immutableObject.Things) Console.WriteLine(item); Console.WriteLine(); Console.WriteLine(otherImmutableObject.Name); foreach (var item in otherImmutableObject.Things) Console.WriteLine(item); Console.ReadKey(); }
En este ejemplo, hemos tratado (o era la intención) de hacer una copia de sus datos, sin embargo y en el caso de la colección, no estaremos haciendo un Deep Copy, sino un Shallow Copy, incurriendo en los problemas que antes comentaba.
Es decir, estamos haciendo una copia de la referencia al valor original.
Como podemos apreciar, es un tema delicado sobre el cuál es muy fácil despistarse y cometer algún error.
Una de las formas más adecuadas de resolver esto en C# es a través de una extensión genérica que nos permita crear un Deep Copy en toda regla utilizando Reflection para ello.
De esta manera, evitaremos los problemas que estoy mostrando en esta entrada.
Así que imaginemos que tenemos esa extensión (que ahora no voy a poner aquí).
Nuestro código debería ser similar ahora a:
public class MyImmutableObject { private readonly string _name; public string Name => _name; private readonly List _things; public List Things => _things.DeepCopy(); public MyImmutableObject(string name, List things) { _name = name; _things = things; } }
Ahora, omitiendo aún esa supuesta extensión, una aproximación aún mas sencilla sin la teórica extensión que haría esa acción sería de esta forma:
public class MyImmutableObject { private readonly string _name; public string Name => _name; private readonly List _things; public List Things => new List(_things); public MyImmutableObject(string name, List things) { _name = name; _things = things; } }
Lo que recibiremos al llamar a Things es una nueva copia del objeto (o mejor dicho, una creación nueva) referenciando a otra posición de memoria diferente a la del original.
Es decir, habremos aislado completamente el tipo por referencia original creando una Deep Copy de los valores de esa propiedad.
Ahora bien, haciendo uso de una extensión genérica muy sencilla para abordar ese comportamiento con las colecciones, podría ser el siguiente:
public static class DeepCopyExtensions { public static IList DeepCopy(this IList originalCollection) where T : ICloneable => originalCollection.Select(item => (T)item.Clone()).ToList(); } public class MyImmutableObject { private readonly string _name; public string Name => _name; private readonly List _things; public List Things => _things.DeepCopy().ToList(); public MyImmutableObject(string name, List things) { _name = name; _things = things; } }
Conclusiones
Todo esto es lo que debemos tener en cuenta cuando trabajamos con objectos inmutables y cuando copiamos sus valores.
Es muy fácil errar como vemos.
Entender bien no sólo lo que es y significa inmutabilidad, tipos por valor y tipos por referencia, no es suficiente para comprender todo lo que sucede «por debajo». Conceptos como Shallow Copy o Deep Copy nos ayuda a comprender mejor aún este tipo de comportamientos.
Recordemos que una Shallow Copy copia las referencias a los elementos y los valores de los elementos si los hubiera.
Una Deep Copy crea nuevas referencias y copia los valores de los elementos si los hubiera.
Como digo, la fiabilidad es clave y nuestra responsabilidad como desarrolladores debe asegurar que el propósito de nuestro Software, cumple perfectamente con lo esperado.
Espero que esta entrada sea de utilidad.
Happy Coding!