martes, 27 de diciembre de 2016

Clonando Objectos en .NET Framework



En esta entrada vamos a tratar un par de formas o estrategias para realizar clonación de objetos en .NET Framework. Analizaremos los pros y las contras por cada uno de los métodos, ya que a día de hoy no existe una fórmula infalible para realizar esta tarea.


Todo lo que vamos a ver está dirigido al uso de clases, ósea a tipos por referencia que no sean clases inmutables (stringsdelegadosstructuras, etc), ya que éstas tienen un tratamiento diferente en memoria que se queda fuera del objetivo de este post.


Dentro de los métodos de clonación, vamos a abordar 2 modos diferentes de uso: Mediante la Interfaz ICloneable y mediante Métodos Extensores, también veremos cual son los puntos fuertes y las debilidades de cada uno.





Clase de ejemplo

La siguiente clase es la que utilizaremos para nuestros ejemplos:


public class Customer : ICloneable
{
    public    int                ID        { get; set; }
    public    string             Name      { get; set; }
    public    decimal            Sales     { get; set; }
    public    DateTime           EntryDate { get; set; }
    public    Address            Adress    { get; set; }
    public    Collection<string> Mails     { get; set; }
 
    protected string             Data1     { get; set; }
    private   string             Data2     { get; set; }
 
 
    public Customer()
    {
        Data1 = "data1";
        Data2 = "Data2";
    }
 
 
 
    public virtual object Clone() { }
 
}



La necesidad de clonar

Este es un concepto muy simple y básico dentro de la programación, así que si no eres un principiante en esto puedes pasar al siguiente bloque.


La acción de clonar puede llegar a ser un aspecto muy necesario dentro los lenguajes de alto nivel (C#, java, C++, etc), porque de forma natural, cuando asignamos un objeto a otro, en realidad lo que estamos haciendo es asignando los dos a la misma referencia:


Customer customer1 = new Customer { ID = 1, Name = "Test", City = "City", Sales = 1000m };
 
Customer customer2 = customer1;






Customer1 y Customer2  están enlazados y cualquier modificación en cualquiera de ellos se verá reflejada en el otro, por lo que no son independientes.

Clonar objetos es completamente necesario para lograr que ambos objetos, el original y el clonado sean objetos completamente independientes.



Customer customer2 = (Customer)customer1.Clone();





















ICloneable
Es la interfaz oficial de .NET Framework destinada a la clonación de objetos. Es muy simple ya que solo consta del método Clone.

Como cualquier otra interfaz, carece de implementación y deja completa libertada al desarrollador para realizar sus métodos de clonación y el nivel de profundidad que quiera aplicar en ellos.


public interface ICloneable
{
    object Clone();
}


Una de las principales carencias de este método es el tipo de datos que devuelve (object), ya que esto nos obliga a tener que realizar un casting al tipo de nuestro objeto principal cada vez que hacemos uso de ella y con esto a sufrir una pérdida de rendimiento derivada de boxing/unboxing.


Customer customer2 = (Customer)customer1.Clone();

Otro aspecto negativo es que nos obliga escribir código especializado para cada una de las clases que queramos clonar.



Método Extensor

Otro mecanismo de creación de objetos es mediante los Métodos de Extensión. Éstos nos ofrecen la posibilidad de trabajar con tipos genéricos y devolver objetos diferentes a object, librándonos de hacer castings innecesarios y de los problemas de rendimiento derivados del boxing/unboxing. Los Métodos de Extensión, solo tendremos que escribirlos una vez y podremos usarlos en cualquier parte de nuestro código.


public static class MyExtensions
{
    public static T CloneObject<T>(this object source)
    {
        T result = Activator.CreateInstance<T>();
 
        //// **** hacer más cosas
        return result;
    }
}


Llamada:


Customer Customer2 = customer1.CloneObject();


También podemos usar nuestro método extensor en conjunción con ICloneable:


public class Customer : ICloneable
{
   // Propiedades ...

    public virtual object Clone()
    {
        return this.CloneObject();
    }
}




Object.MemberWiseClone

MemberWiseClone es un método protegido (protected) de la clase object. Este método crea una copia superficial del objeto actual en nuevo objeto.

Según el tipo de la propiedad, tipos por valor (structs) o por referencia (class), MemberWiseClone, realizará la tarea de clonado de modo diferente:
  • Structs .- Copiará bit a bit el valor de la propiedad.
  • Class . – Copiará la referencia de la propiedad, por lo que la propiedad del objeto original y la de la copia se mantendrán enlazadas. Este es uno de los puntos flacos de este método.

Al ser MemberWiseClone un método protegido, normalmente lo utilizaremos junto con la interfaz ICloneable, ya que la llamada a MemberWiseClone, la tendremos que realizar en el interior de la declaración de la clase.   

MemberWiseClone ejemplo:


public class Customer : ICloneable
{
    public    int                ID        { get; set; }
    public    string             Name      { get; set; }
    public    decimal            Sales     { get; set; }
    public    DateTime           EntryDate { get; set; }
    public    Address            Adress    { get; set; }
    public    Collection<string> Mails     { get; set; }
 
    protected string             Data1     { get; set; }
    private   string             Data2     { get; set; }
 
 
    public Customer()
    {
        Data1 = "data1";
        Data2 = "Data2";
    }

    public virtual object Clone()
    {
        return this.MemberwiseClone();
    }
}


Pros:
  • Sencillo de desarrollar.
  • Muy poco código.
  • Fácil de entender.
  • Copia cualquier elemento del tipo que sea (simples, colecciones, clases, etc).
  • No necesita ser marcado con ningún tipo de atributo especial.


Contras:
  • Solo puede ser llamado desde el interior del método (this).
  • Debe implementarse el código en todas y cada una de las clases que lo utilicen.
  • Las propiedades de tipo referencia, son enlazadas, por lo que no se realiza una copia real.
  • Este método devuelve object, por lo que es obligatorio realizar un casting cada vez que su usa.


En caso de querer hacer una copia en profundidad, tendremos que añadir código personalizado según el tipo, realizando copias reales de nuestras propiedades de tipo referencia, este sería el ejemplo para la clase Customer:


public virtual object Clone()
{
    var result = this.MemberwiseClone();
 
    // Asinaciones Manuales 
 
    result.Adress = new Address
    {
        City    = this.Adress.City,
        Street  = this.Adress.Street,
        ZipCode = this.Adress.ZipCode
    };
 
    result.Mails = new Collection<string>();
 
    this.Mails.ToList().ForEach(a => result.Mails.Add(a));
 
    return result;
}



Stream - Formatters

Este modelo de clonación utiliza la serialización para procesar las copias de los objetos. Las copias de resultado son muy completas, ya que realiza un copiado en profundidad, tanto de las propiedades de tipo valor, como de las de tipo referencia.

Su talón de Aquiles se materializa en la necesidad de tener que marcar la definición de nuestras clases cloneables con cualquier tipo de atributo de serialización.

En la red hay diferentes ejemplos de clonación mediante serialización, yo tomaré el ejemplo del compañero Surajit Datta Article, que me parece claro y sencillo para lo que vamos a exponer.

Para facilitar su empleo, concretaremos el ejemplo en un Método Extensor que nos proporcionara funcionalidad y compatibilidad con todos los objetos de tipo object.


public static T CloneObjectSerializable<T>(this T obj) where T : class
{
    MemoryStream    ms = new MemoryStream();
    BinaryFormatter bf = new BinaryFormatter();
    bf.Serialize(ms, obj);
    ms.Position = 0;
    object result = bf.Deserialize(ms);
    ms.Close();
    return (T) result;
}


Llamada:

Si ejecutáramos el código tal y como lo mostramos en la llamada, el compilador nos lanzaría una excepción de tipo SerializationException:























Esto es debido a que es obligatorio marcar nuestra clase con un Atributo de Serialización:


[Serializable]
public class Customer


Pros:
  • Sencillo de escribir (Según mi criterio claro).
  • Fácil de pasar a método extensor, por lo que solo tendremos que escribirlo una vez.
  • Copia cualquier elemento del tipo que sea (simples, colecciones, clases, etc).
  • Proporciona copia profunda, realizando copia de cualquier tipo de propiedad.
  • No es necesario ser llamado dentro de la definición de la clase.
  • Retorna un tipo genérico, por lo que no hay necesidad de hacer casting y libra de los problemas de boxing / unboxing.

Contras:
  • Es necesario marcar nuestras clases con un atributo de serialización.
  • Para su implementación necesitas una lógica un poco más compleja y algo más de código.



Este modo es completamente compatible con la interfaz ICloneable y mantiene mucha de sus virtudes.


public virtual object Clone()
{
    return this.CloneObjectSerializable();
}





Conclusiones

En el mundo del desarrollo es muy necesario tener claro el concepto de clonación, tener una idea equivocada sobre ello, nos puede llevar a un conjunto de errores y comportamiento no esperados en nuestros programas. Recordar que estos son los más perjudiciales en el desarrollo y los menos deseados, ya que son transparentes para el compilador y para la ejecución y solo son distinguibles en resultados de salida no exactos.