Blog dedicado a la programación .NET y la informática en general

Hay ciertas ocasiones en las que necesitamos que nuestras entidades se puedan hacer copias de las mismas para poder manipularlas sin modificar el original. También en otras ocasiones necesitamos que éstas se puedan coleccionar en diccionarios para evitar la duplicidad de las mismas. El problema viene en que una clase de por sí no conlleva esta funcionalidad. Hoy voy a tratar de exponer el código con el que yo me siento cómodo para llevar a cabo ambas funcionalidades.

Para este fin me he decidido por utilizar una clase base a la que llamaré «EntidadBase». Se supone que heredarán todas mis clases de negocio de esta, pero OJO estas implementaciones no significan que sirvan en cualquier situación, se debe conocer bien la implementación ya que un mal uso puede llevar a un problema mayor.

Implementación de ICloneable

Lo primero que hay que advertir es que no está recomendada la implementación de ICloneable tal y como Microsoft advierte aquí en la sección de «Notas a los implementadores». ¿Por qué?, porque el método «Clone» no informa sobre la copia devuelta en sí, no indica si la copia se ha realizado en profuncidad, es intermedia o sólo superficial, por tanto en APIs públicas puede suponer un problema el uso de este método.

Pero en ciertas circunstancias si que es apropiado su uso, como por ejemplo cuando queremos modificar una entidad sin cambiar la original, de tal modo que podamos distinguir los cambios producidos.

Partamos de la necesidad y la practicidad. Podríamos usar el método object.MemberwiseClone si tan sólo necesitamos una copia simple de las propiedades del objeto, pero OJO, esto sólo va bien para campos de tipo valor, para los campos de tipo referencia copia la referencia tal cual y por tanto si cambiamos el campo también lo cambiaremos en el original.

Codificamos una clase sencilla EntidadSimple, con tipos valor:

public class EntidadSimple : EntidadBase 
{
	#region Propiedades
	public int Id { get; set; }
	public string Descripcion { get; set; }
	#endregion
}

Ahora una clase más compleja EntidadCompleja, una de sus propiedades es un listado y otra propiedad su tipo es otra clase. Además pondremos otros tipos de propiedad para que veamos cómo funcionan entre los campos de tipo valor y los campos de tipo referencia.

// SPOILER : MAL faltaría la sobrecarga del método Clone, pero lo dejamos para mas adelante
public class EntidadCompleja : EntidadBase 
{
	#region Propiedades
	public int Id { get; set; }
	public string Nombre { get; set; }
	public List<string> Coleccion { get; set; }
	public EntidadSimple Simple { get; set; }
	public DateTime Fecha { get; set; }
	#endregion
}

Implementemos ICloneable mediante MemberwiseClone y demos una vuelta de tuerca preparándolo para que devuelva la clase base EntidadBase:

public class EntidadBase : ICloneable
{
	#region Implementación ICloneable
	public virtual EntidadBase Clone()
	{
		// ATENCIÓN : Como no se puede predecir el comportamiento del método Clone se recomienda no
		// implementar ICloneable en APIs públicas
		// OJO para propiedades de colecciones, tipos por referencia o propiedades "IsReadOnly" hay que 
		// programarlo manualmente por esta razón se deja como "Virtual" el método, ya que MemberwiseClone 
		// tan sólo hace una copia superficial
		return this.MemberwiseClone() as EntidadBase;
	}

	object ICloneable.Clone()
	{
		return Clone();
	}
	#endregion
}

Podríamos clonar tal cual la clase EntidadSimple y no habría problema, crearía una copia superficial, el problema lo tendríamos si clonamos EntidadCompleja, el código que lo invoca sería el siguiente:

static void Main(string[] args)
{
	Console.WriteLine("┌────────────────────────────────────────────────────────────────────────────┐");
	Console.WriteLine("│                          Entidades Extendidas PoC                          │");
	Console.WriteLine("└────────────────────────────────────────────────────────────────────────────┘");

	try
	{
		#region Pruebas ICloneable
		// Clonado de la entidad simple
		EntidadSimple entSim = new EntidadSimple
		{
			Id = 1,
			Descripcion = "Simple 1"
		};

		EntidadSimple entSimCopia = entSim.Clone() as EntidadSimple;
		entSimCopia.Id = 2;
		entSimCopia.Descripcion = "Simple 2";
		if (entSim.Descripcion == entSimCopia.Descripcion)
		{
			Console.WriteLine("¡Vaya no ha hecho bien la copia simple!");
		}
		else
		{
			Console.WriteLine("Clonado de la entidad simple correcto.");
		}

		// Clonado de la entidad compleja
		bool clonadoComplejoCorrecto = true;
		EntidadCompleja entCom = new EntidadCompleja
		{
			Id = 1,
			Nombre = "Compleja 1",
			Coleccion = new List<string> { "Primero", "Segundo" },
			Simple = entSim,
			Fecha = new DateTime(2019, 6, 10)
		};

		EntidadCompleja entComCopia = entCom.Clone() as EntidadCompleja;
		entComCopia.Id = 2;
		if (entCom.Id == entComCopia.Id)
		{
			clonadoComplejoCorrecto = false;
			Console.WriteLine("¡Vaya no ha hecho bien la copia simple!");
		}

		entComCopia.Coleccion[0] = "Tercero";
		if (entCom.Coleccion[0] == entComCopia.Coleccion[0])
		{
			clonadoComplejoCorrecto = false;
			Console.WriteLine("¡Vaya no ha hecho bien la copia del listado!");
		}

		entComCopia.Simple.Id = 3;
		if (entCom.Simple.Id == entComCopia.Simple.Id)
		{
			clonadoComplejoCorrecto = false;
			Console.WriteLine("¡Vaya no ha hecho bien la copia del objeto!");
		}

		entComCopia.Fecha = entComCopia.Fecha.AddDays(1);
		if (entCom.Fecha == entComCopia.Fecha)
		{
			clonadoComplejoCorrecto = false;
			Console.WriteLine("¡Vaya no ha hecho bien la copia de la fecha!");
		}

		if (clonadoComplejoCorrecto)
		{
			Console.WriteLine("Clonado de la entidad compleja correcto.");
		}
		#endregion
	}
	catch (Exception ex)
	{
		Console.WriteLine($"ERROR : {ex.Message}");
	}
}

Obtendríamos en este punto lo siguiente:

┌────────────────────────────────────────────────────────────────────────────┐
│                          Entidades Extendidas PoC                          │
└────────────────────────────────────────────────────────────────────────────┘
Clonado de la entidad simple correcto.
¡Vaya no ha hecho bien la copia del listado!
¡Vaya no ha hecho bien la copia del objeto!

Se comprueba que a pesar que el tipo string es de tipo referencia realiza el clonado correctamente, también pasa lo mismo con el tipo DateTime que, a pesar de lo que algunos creen, es de tipo valor. Donde ya no realiza la copia bien son en el resto de objetos de tipo referencia como son el listado y la propiedad con la clase EntidadSimple.

Por tanto para que el clonado funcione correctamente se deja el método Clone en la clase base como virtual para poder sobrecargarla y tratar las propiedades de tipo referencia de forma manual.

public class EntidadCompleja : EntidadBase
{
	#region Propiedades
	public int Id { get; set; }
	public string Nombre { get; set; }
	public List<string> Coleccion1 { get; set; }
	public string[] Coleccion2 { get; set; }
	public ArrayList Coleccion3 { get; set; }
	public EntidadSimple Simple { get; set; }
	public DateTime Fecha { get; set; }
	#endregion

	#region Sobrecarga de métodos
	public override EntidadBase Clone()
	{
		EntidadCompleja copia = base.Clone() as EntidadCompleja;

		// Inicialización de propiedades de tipo referencia
		copia.Coleccion1 = null;
		copia.Simple = null;

		// Copia de valores si los objetos no son nulos
		if (this.Coleccion1 != null)
		{
			copia.Coleccion1 = this.Coleccion1.ToList();
		}

		if (this.Coleccion2 != null)
		{
			copia.Coleccion2 = this.Coleccion2.ToArray();
		}

		if (this.Coleccion3 != null)
		{
			copia.Coleccion3 = this.Coleccion3.Clone() as ArrayList;
		}

		if (this.Simple != null)
		{
			copia.Simple = this.Simple.Clone() as EntidadSimple;
		}

		return copia;
	}
	#endregion
}

En esta nueva codificación de EntidadCompleja he agregado más propiedades de tipo listado para comprobar su comportamiento con cada una de ellas.

Ahora al volver a ejecutar la aplicación saldría el siguiente texto:

┌────────────────────────────────────────────────────────────────────────────┐
│                          Entidades Extendidas PoC                          │
└────────────────────────────────────────────────────────────────────────────┘
Clonado de la entidad simple correcto.
Clonado de la entidad compleja correcto.

Implementación para listados Hash y Diccionarios

Bien imaginemos que queremos hacer una caché simple de objetos de EntidadCompleja, lo primero que se nos ocurre es realizar un diccionario cualquiera e introducir valores esperando que el diccionario detecte si ya existe.

#region Pruebas Hash
Dictionary<EntidadCompleja, object> dic = new Dictionary<EntidadCompleja, object>();
entComCopia = entCom.Clone() as EntidadCompleja;
dic.Add(entCom, "Uno");
if(!dic.ContainsKey(entComCopia))
{
    dic.Add(entComCopia, "Dos");
    Console.WriteLine("Si pasa por aquí es que se ha duplicado la entidad en el diccionario.");
}
#endregion

Como la clase no está preparada para comprobar si otro objeto del mismo tipo es igual a él mismo, el diccionario entiende que son distintos y lo incluye, por lo que hay que codificar que de alguna forma distinga si son iguales o no.

ATENCIÓN : Partamos por tanto de la hipótesis que NECESITAMOS que para que un objeto de la misma clase sea igual a otro deberá tener los mismos valores en sus propiedades. Es decir, no vamos a entrar a evaluar variables, ni métodos, tan sólo propiedades

:

public class EntidadBase : ICloneable, IEquatable<EntidadBase>
{
	#region Implementación ICloneable
	public virtual EntidadBase Clone()
	{
		// ATENCIÓN : Como no se puede predecir el comportamiento del método Clone se recomienda no
		// implementar ICloneable en APIs públicas
		// OJO para propiedades de colecciones, tipos por referencia o propiedades "IsReadOnly" hay que 
		// programarlo manualmente por esta razón se deja como "Virtual" el método, ya que MemberwiseClone 
		// tan sólo hace una copia superficial
		return this.MemberwiseClone() as EntidadBase;
	}

	object ICloneable.Clone()
	{
		return Clone();
	}
	#endregion

	#region Implementación IEquatable
	public bool Equals(EntidadBase other)
	{
		if (other as object == null || GetType() != other.GetType())
		{
			return false;
		}

		Type tipo = this.GetType();

		foreach (PropertyInfo prop in tipo.GetProperties())
		{
			var valorEsperado = prop.GetValue(other);
			var valorRecibido = prop.GetValue(this);
			if (valorEsperado == null && valorRecibido != null)
			{
				return false;
			}
			if (valorEsperado != null && valorRecibido == null)
			{
				return false;
			}
			if (valorEsperado != null && valorRecibido != null)
			{
				// Comprueba si la propiedad es algún tipo de array
				var isGenericICollection = valorRecibido.GetType().GetInterfaces().Any(
					x => x.IsGenericType &&
					x.GetGenericTypeDefinition() == typeof(ICollection<>));
				var isICollection = valorRecibido.GetType().GetInterfaces().Any(
					x => x == typeof(ICollection));

				if (isGenericICollection || isICollection)
				{
					ICollection coleccionEsperada = valorEsperado as ICollection;
					ICollection coleccionRecibida = valorRecibido as ICollection;
					if (coleccionEsperada.Count != coleccionRecibida.Count)
					{
						return false;
					}

					object[] listaEsperada = new object[coleccionEsperada.Count];
					coleccionEsperada.CopyTo(listaEsperada, 0);
					object[] listaRecibida = new object[coleccionRecibida.Count];
					coleccionRecibida.CopyTo(listaRecibida, 0);

					for (int i = 0; i < coleccionRecibida.Count; i++)
					{
						if(!listaRecibida[i].Equals(listaEsperada[i]))
						{
							return false;
						}
					}
				}
				else if(!valorEsperado.Equals(valorRecibido))
				{
					return false;
				}
			}
		}

		return true;
	}
	#endregion

	#region Sobrecarga de Métodos
	public override int GetHashCode()
	{
		int hashCode = 0;

		foreach (PropertyInfo prop in this.GetType().GetProperties())
		{
			object value = prop.GetValue(this);
			if (value != null)
			{
				// Comprueba si la propiedad es algún tipo de array
				var isGenericICollection = value.GetType().GetInterfaces().Any(
					x => x.IsGenericType &&
					x.GetGenericTypeDefinition() == typeof(ICollection<>));
				var isICollection = value.GetType().GetInterfaces().Any(
					x => x == typeof(ICollection));

				if (isGenericICollection || isICollection)
				{
					ICollection col = value as ICollection;
					foreach (var valueColeccion in col)
					{
						// Se recorre los objetos del array obteniendo su código hash
						hashCode = CrearHash(hashCode, valueColeccion);
					}
				}
				else
				{
					hashCode = CrearHash(hashCode, value);
				}
			}
		}

		return hashCode;
	}

	public override bool Equals(object obj)
	{
		// Se sobreescribe el método para poder comprobar si dos objetos de la misma clase tienen las mismas propiedades
		return Equals(obj as EntidadBase);
	}

	public override string ToString()
	{
		// Este no hace falta pero lo he incluido para depurar las variables
		try
		{
			return JsonConvert.SerializeObject(this, Formatting.Indented);
		}
		catch (Exception ex)
		{
			return base.ToString() + $" - Excepción obteniendo los valores de las propiedades: {ex.Message}";
		}
	}
	#endregion

	#region Codificación de operadores
	public static bool operator ==(EntidadBase obj1, EntidadBase obj2)
	{
		if (obj1 as object == null && obj2 as object == null)
		{
			return true;
		}
		if (obj1 as object == null && obj2 as object != null)
		{
			return false;
		}
		return obj1.Equals(obj2);
	}

	public static bool operator !=(EntidadBase obj1, EntidadBase obj2)
	{
		if (obj1 as object == null && obj2 as object == null)
		{
			return false;
		}
		if (obj1 as object == null && obj2 as object != null)
		{
			return true;
		}
		return !obj1.Equals(obj2);
	}
	#endregion

	#region Métodos Auxiliares
	private static int CrearHash(int hashCode, object value)
	{
		if (hashCode == 0)
		{
			hashCode = value.GetHashCode();
		}
		else
		{
			hashCode = hashCode ^ value.GetHashCode();
		}

		return hashCode;
	}

	#endregion
}

He separado el código en regiones para que quede algo más claro.

Lo primero que hago es implementar la interfaz IEquatable. Esta va a llevar la responsabilidad mayor en la comparación de objetos. A través de foreach (PropertyInfo prop in tipo.GetProperties()) recorre todas las propieades, se podría hacer que sólo tuviera en cuenta las públicas, pero en esta ocasión no lo he hecho. Comprueba los posibles valores nulos en la comparación, y después de comprobar que no vienen a nulos distingue si la propiedad es una colección o no mediante las variables isGenericICollection y isICollection.

Cuando NO es una colección directamente se llama al método object.Equals, por tanto funcionará correctamente con campos tipo valor, y con campos tipo referencia siempre y cuando tengan implementado Equals en su clase. Si se respeta que las propiedades de la clase de tipo referencia sean de otra clase que herede de EntidadBase entonces no habrá problema para distinguirlas y sabrá si son iguales o no.

OJO esto significa que cualquier propiedad que contenga un campo de tipo referencia de una clase que no tenga implementada el método Equals dará como distinta.

Por esta razón las listas hay que tratarlas de otra forma, ya que un array es un campo de tipo referncia al que no tenemos acceso para implementarle el método Equals. Las propiedades detectadas como listados se convierten en ICollection para tratarlas individualmente en arrays ya recorribles.

ICollection coleccionEsperada = valorEsperado as ICollection;
ICollection coleccionRecibida = valorRecibido as ICollection;
if (coleccionEsperada.Count != coleccionRecibida.Count)
{
    return false;
}

object[] listaEsperada = new object[coleccionEsperada.Count];
coleccionEsperada.CopyTo(listaEsperada, 0);
object[] listaRecibida = new object[coleccionRecibida.Count];
coleccionRecibida.CopyTo(listaRecibida, 0);

for (int i = 0; i < coleccionRecibida.Count; i++)
{
    if(!listaRecibida[i].Equals(listaEsperada[i]))
    {
        return false;
    }
}

Si probamos esto en la aplicación de consola tendríamos el siguiente código:

entSimCopia = entSim.Clone() as EntidadSimple;
int hash1s = entSim.GetHashCode();
int hash2s = entSimCopia.GetHashCode();

Dictionary<EntidadCompleja, object> dic = new Dictionary<EntidadCompleja, object>();
entComCopia = entCom.Clone() as EntidadCompleja;
int hash1 = entCom.GetHashCode();
int hash2 = entComCopia.GetHashCode();
bool sonIguales = entCom == entComCopia;
dic.Add(entCom, "Uno");
if(!dic.ContainsKey(entComCopia))
{
	dic.Add(entComCopia, "Dos");
	Console.WriteLine("Si pasa por aquí es que se ha duplicado la entidad en el diccionario.");
}

En el código se compruba que los hash creados en las entidades simples son correctos, además se crea un diccionario para ver como se comporta con los objetos clonados y comprobar que efectivamente es capaz de distinguirlos.

Conclusión

Insisto que esto es una prueba de concepto totalmente personal, que me ha servido como base para otros desarrollos, pero antes de implementarlo en una solución profesional hay que tener en cuentas los siguientes criterios.

Criterios para la implementación de IClonable:

  • La clonación deberá tener en cuenta aquellas propiedades y variables que sean de tipo referencia (que no sean string) para sobrecargar el método base Clone y codificarlas individualmente.
  • De poco servirá si las clases de sus propiedades y variables no implementan IClonable.
  • No se recomienda su uso en APIs públicas.

Criterios para la implementación de IEquatable<>:

  • No es recomendable esta implementación en una clase que tiene como propiedades listas de longitud considerable si lo que queremos es usarla en tablas hash, ya que cada vez que realice una búsqueda tiene que recorrerse sus listas para comprobar si es igual y esto aumenta demasiado las búsquedas.
  • Esta implementación sólo tiene en cuenta las propiedades de la clase, no las variables, ni las de tipo estático o de sólo lectura.
  • Se debe tener especial cuidado con las propiedades de tipo referencia, sobre todo los listados, y considerar si realmente merece la pena usar esta implementación.

Espero que el que haya llegado hasta aquí haya entendido mejor el funcionamiento de IClonable e IEquatable y que le pueda servir en futuros desarrollos.

El código fuente está subido en GitHub y podéis descargarlo desde aquí.

Saludos.

Anuncio publicitario

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

A %d blogueros les gusta esto: