sábado, 29 de diciembre de 2012

Clone Wars (o el arte de clonar objetos). Parte 1

Empiezo con mi primer artículo, un tema recurrente cuando estamos desarrollando: ¿Como clonamos nuestros objetos?

Antes que nada aclarar, que en tipos por referencia (clases por ejemplo), no basta con igualar, ya que estaríamos copiando la referencia.

Si hiciesemos:

var coche=new Car() {Marca="Opel", Modelo="Astra"};
var coche2=coche;

coche.Marca="Seat";

¿Que marca tendríamos en coche2? (redoble de tambores)
Pues Seat... porque  hemos copiado las referencias (punteros para los viejos rockeros que empezamos con C++  )

Aqui es donde entra el clonado, y por clonar, me refiero a una copia bit a bit... una copia exacta de nuestros objetos.

Tenemos 2 opciones, clonar de un modo manual, o dejar que mecanismos automáticos lo hagan por nosotros:

Clonado manual:


¿Os suena la interfaz ICloneable?  Se trata de que la implementemos


   1:  public class Car: ICloneable{
   2:   
   3:     public string Marca;
   4:   
   5:     public Engine MainEngine;
   6:   
   7:     public object Clone()
   8:   
   9:     {
  10:   
  11:         Car c = new Car ();
  12:   
  13:         c.Marca= this.Marca;
  14:   
  15:         if (this.MainEngine!= null)
  16:   
  17:                          c.MainEngine= (Engine )this.MainEngine.Clone();
  18:   
  19:         return c;
  20:   
  21:     }
  22:   
  23:  }

Y claro... ahora tocaría implementar el clonado para la clase Engine... y así hasta el infinito.

Parece evidente que un clonado manual va a ser el más efectivo (desde un punto de vista del rendimiento), pero no es oro todo lo que reluce, tiene 2 grandes problemas:

  • Es tedioso (imagina implementar el clonado para decenas o centenares de tipos)
  • Es muy propenso a errores... y creedme, estos errores son de los difíciles de encontrar

Clonado automático:


Existen métodos para realizar los clonados de un modo automatico, y así evitarnos la implementación del método Clone() para cada uno de nuestros tipos.

Los más comunes son el clonado mediante serialización y el clonado mediante reflection. Hay otros métodos más sofisticados... esos los dejo para un segundo post sobre este tema.

1. Clonado mediante serialización

Este método es realmente sencillo, se trata de serializar y deserializar el objeto a clonar, con lo que obtenemos una copia exacta. Sería algo así:

   1:   public override T Clone<T>(T source)
   2:          {
   3:              StartTime();
   4:   
   5:              if (!typeof(T).IsSerializable)
   6:              {
   7:                  throw new ArgumentException("The type must be serializable.", "source");
   8:              }
   9:   
  10:              // Don't serialize a null object, simply return the default for that object
  11:              if (Object.ReferenceEquals(source, null))
  12:              {
  13:                  return default(T);
  14:              }
  15:   
  16:              IFormatter formatter = new BinaryFormatter();
  17:              
  18:              using (Stream stream = new MemoryStream())
  19:              {
  20:                  formatter.Serialize(stream, source);
  21:                  stream.Seek(0, SeekOrigin.Begin);
  22:   
  23:                  var result= (T)formatter.Deserialize(stream);
  24:                  TraceTime();
  25:                  return result;
  26:              }
  27:          }        


Sencillo y suele funcionar bien. ¿Por que digo suele? Porque dependiendo del tipo de serialización que usemos (binaria, XML o de contrato), algunos miembros privados podrían no clonarse bien. Por otro lado, a priori parece que no será muy bueno. Esto lo veremos más adelante.

¿Cual es su ventaja? Bajo mi punto de vista la simplicidad. supongo que estareis de acuerdo cuando veais el siguiente tipo de clonado, que usa el API Reflection de .NET.

2. Clonado con Reflection


Vamos cuanto antes al código, solo un apunte:  Queremos lo que se llama un clonado en profundidad, es decir si nuestro objeto tiene dentro un atributo de un tipo por referencia, debemos clonarlo también, no copiar la referencia. En el tipo de clonado anterior (serialización) no tuvimos que tenerlo en cuenta, ya que el serializador nos abstrae de esto, aqui no podemos obviarlo, así que he realizado una implementación recursiva:


   1:  protected T CloneRec<T>(T source)
   2:          {
   3:              if (Object.ReferenceEquals(source, null))
   4:              {
   5:                  return default(T);
   6:              }
   7:              var destination = Activator.CreateInstance(source.GetType());
   8:              var props = destination.GetType().GetProperties();
   9:              int propIndex = 0;
  10:   
  11:              foreach (var propInfo in props)
  12:              {
  13:                  if (propInfo.GetValue(source,null) != null)
  14:                  {
  15:   
  16:                      if (ImplementsInterface(propInfo.PropertyType, "IList"))
  17:                      {
  18:                          var enumerable = (IEnumerable)propInfo.GetValue(source,null);
  19:                          //int i = 0;
  20:                          IList list = (IList)propInfo.GetValue(destination,null);
  21:                          foreach (var item in enumerable)
  22:                              list.Add(CloneRec(item));
  23:                      }
  24:                      else if (ImplementsInterface(propInfo.PropertyType, "ICloneable"))
  25:                      {
  26:                          props[propIndex].SetValue(destination, ((ICloneable)propInfo.GetValue(source, null)).Clone(), null);
  27:                      }
  28:                      else if(propInfo.PropertyType.IsValueType)
  29:                      {
  30:                          props[propIndex].SetValue(destination, propInfo.GetValue(source, null), null);
  31:                      }
  32:                      else
  33:                      {
  34:                          props[propIndex].SetValue(destination, CloneRec(propInfo.GetValue(source, null)),null);
  35:                      }
  36:                  }
  37:                  propIndex++;
  38:              }            
  39:              return (T)destination;
  40:          }     

Se trata solo de un ejemplo, no esta completo, ya que solo clono propiedades, y ademas solo trato colecciones que implementen IList. Afortunadamente rebuscando un poco pueden encontrarse implementaciones completas.

El algoritmo es sencillo:
  • Creamos el objeto destino
  • Leo mediante relection todas las propiedades
  • Para cada propiedad
                - Si es una lista, clono cada elemento de la misma (llamada recursiva)
                - Si es un tipo que implementa ICloneable, lo clonamos (estamos de suerte!)
                - Si es un tipo por valor, podemos copiarlo sin más (tipos básicos)
                - En caso contrario, será un tipo por referencia, tenemos que clonarlo (llamada recursiva)

Rendimiento:

No vamos a encontrar muchas sorpresas: el clonado manual es sin duda el más rápido. En cuanto al automático, se suele decir que reflection rinde un poco mejor que el serializado. ¿Será cierto? Vamos a comprobarlo.

He clonado una serie de objetos con los 2 métodos: un garage, que tienes varios vehículos,   1000 , 10000, 25000 y finalmente 50000. Estos son los resultados:



Nota: Se han realizado varias pruebas consecutivas y se han tomado tiempos medios.

Efectivamente, las cosas empiezan igualadas, pero según aumenta el número de objetos a clonar, reflection empieza a marcar diferencias.


Conclusiones:

Hay varias opciones, y escribiré una segunda entrada con algunas más, mientras tanto, yo me decantaría por lo siguiente:

  • Clonado manual siempre que podamos
  • Clonado basado en serialización si vamos a clonar pocos objetos
  • Clonado basado en reflection para el resto de casos

Aqui os dejo todo el código: los clonadores, las clases a clonar y las mediciones de tiempo:

http://sdrv.ms/10zGTin


Continuará...

 

No hay comentarios:

Publicar un comentario