C#: Implementar la Interface ICloneable

No será la primera vez que me he encontrado en la tesitura de tener que duplicar todos los datos de una clase. Quizás, el ejemplo más claro lo encontremos cuando tenemos un grid creado y el usuario te sugiere la posibilidad de crear una nueva función que permita copiar una fila ya existente, con todos sus datos. De esta manera, modificando sólo los datos que varían, la creación de la nueva fila es mucho más sencilla. Reconozco que en alguna ocasión he llegado a implementar la Interface ICloneable de un modo tan rupestre como es la creación de una nueva clase y, propiedad a propiedad, ir haciendo una copia de los valores de la misma. Si hablamos de clases con pocas propiedades y con la premisa que nos caracteriza a casi todos los programadores (¡hay prisa en entregar!), esta acción, sin llegar a ser muy ortodoxa, tiene una justificación, pero cuando la clase ya tiene un número considerable de propiedades o es una clase que varía constantemente según las “exigencias del guión”, no dejará de ser un “coladero de errores”.

Vamos a intentar, en esta ocasión, aprender un poco más de la implementación de la interface ICloneable con el objetivo de poder obtener un rendimiento mejorado.

De hecho, si nuestro objetivo es obtener una copia superficial de una clase, no haría falta la implementación de la interface mencionada, puesto que la clase object ya contiene un método protegido que nos proporciona una “copia” de la clase. Por ejemplo:

using System;

namespace JnSoftware.Test
{
    class Coche
    {
        /// <summary>Obtiene la matrícula de un coche</summary>
        public string Matricula { get; private set; }

        /// <summary>Constructor de la clase.</summary>
        /// <param name="matricula">Matrícula del vehículo</param>
        public Coche(string matricula)
        {
            Matricula = matricula;
        }

        /// <summary>
        /// Ejecución del ejemplo
        /// </summary>
        public static void Main()
        {
            Coche c1 = new Coche("0000AAA");
            Coche c2 = (Coche)c1.MemberwiseClone();

            Console.WriteLine(c1.Matricula);
            Console.WriteLine(c2.Matricula);

            Console.ReadKey();
        }

    }
}

Debemos fijarnos en el método MemberwiseClone(), el cual realiza la función que andábamos buscando. ¿Dónde está entonces el problema?. Es muy sencillo. A la clase que estamos usando de ejemplo, vamos a añadirle algún tipo de funcionalidad extra, de tal manera que se asemeje más a un ejemplo de la vida real que no a un ejercicio básico de programación. En este caso, vamos a incluir las multas que pueda llegar a tener un coche.

Clase: Multas

using System;

namespace JnSoftware.Test
{

    /// <summary>
    /// Clase que representa la multa que tiene un coche determinado.
    /// </summary>
    internal class Multa
    {
        public DateTime Fecha { get; set; }
        public decimal Importe { get; set; }

    }
}

Clase: Coche

using System;
using System.Collections.Generic;
using System.Linq;

namespace JnSoftware.Test
{
    public class Coche
    {
        /// <summary>Obtiene la matrícula de un coche</summary>
        public string Matricula { get; private set; }

        private List<Multa> listaMultas;

        /// <summary>Constructor de la clase.</summary>
        /// <param name="matricula">Matrícula del vehículo</param>
        public Coche(string matricula)
        {
            listaMultas = new List<Multa>();
            Matricula = matricula;
        }

        /// <summary>
        /// Añade una multa
        /// </summary>
        public void AddMulta(DateTime fechaMulta, decimal importe)
        {
            listaMultas.Add(new Multa() { Fecha = fechaMulta, Importe = importe });
        }

        /// <summary>
        /// Obtiene el total de multas de un coche
        /// </summary>
        public decimal GetImporteMultas()
        {
            return listaMultas.Sum(m => m.Importe);
        }

        /// <summary>
        /// Ejecución del ejemplo
        /// </summary>
        public static void Main()
        {
            Coche c1 = new Coche("0000AAA");
            c1.AddMulta(DateTime.Today, 1000);
            
            Coche c2 = (Coche)c1.MemberwiseClone();
            c2.Matricula = "1111BBB";
            c2.AddMulta(DateTime.Today, 1000);


            Console.WriteLine("Coche: {0}tMultas:{1}",c1.Matricula,c1.GetImporteMultas());
            Console.WriteLine("Coche: {0}tMultas:{1}", c2.Matricula, c2.GetImporteMultas());

            Console.ReadKey();
        }
    }
}

Si ejecutamos el ejemplo nos llevaremos una sorpresa. Hemos incluido una multa en el coche c1, con un importe de 1000. En la instancia que hemos duplicado, vemos que la segunda multa, también de 1000 se ha añadido al coche c2, en cambio, a la hora de obtener el resultado del importe de las multas que han tenido ambos vehículos, vemos que los dos tienen 2000 como suma del importe de sus multas. ¿Qué ha pasado?; sencillamente, que no hemos leído en profundidad la ayuda del método MemberwiseClone(), sobre todo, cuando hablaba de una “copia superficial”. Así pues, ¿qué es una “copia superficial”?:

Si un campo es de un tipo de valor, se realiza una copia bit a bit de él.Si un campo es un tipo de referencia, se copia la referencia pero no el objeto al que se hace referencia; por consiguiente, el objeto original y su copia hacen referencia al mismo objeto

    Al haber utilizado, por ejemplo, una List<Multas>, que es un objeto por referencia, vemos que no se ha duplicado su contenido sino que se ha hecho uso de la referencia. Es por eso que el resultado de c1.GetImporteMultas() es el mismo que c2.GetImporteMultas(), aunque la multa de c2 se haya introducido después de haber hecho el “duplicado”.

    Ahora ya conocemos el problema. Vamos a ver cómo podemos resolverlo. Una opción pasaría por hacer una clonación superficial de la clase y, para aquellos elementos que precisen de una intervención manual, podemos, por ejemplo, crear una serie de extensores que permitan la creación de elementos nuevos. En el caso del campo listaMultas, por ejemplo, podríamos crear un método extensor de la clase List<T>:

    using System.Collections.Generic;
    using System.Linq;
    
    namespace JnSoftware.Extensions
    {
        public static class MetodosExtensores
        {
            
            /// <summary>
            /// Clona una clase List
            /// </summary>
            public static List<T> Clone<T>(this List<T> lista) 
            {
                return lista.ToList();
            }
    
        }
    }
     

    De esta manera, una vez referenciado el namespace JnSoftware.Extensions en la clase Coche, bastará con modificar el método Clone de la misma:

    public object Clone()
    {
        // Obtenemos una copia superficial de la clase
        Coche nuevo = (Coche)this.MemberwiseClone();
       
        // Clonación manual de campos
        nuevo.listaMultas = this.listaMultas.Clone();
        return nuevo;
    }

    Claro, visto así, no es que sea una clonación muy “profunda” de los elementos de la clase. Cuanto menos, nada automática ni generalizable, sobre todo si nuestra aplicación hace uso de Colecciones, Listas, Diccionarios y demás contenedores por doquier.

    A partir de ahora, reconozco que hubiese sido incapaz de ver una solución más o menos práctica del problema, sobre todo, cuando mis derroteros iban rascando las clases del namespace System.Reflection, con las propiedades y métodos de la clase Type. Menos mal que San Google nos ayuda en nuestros momentos de extrema depresión. Así, una visita a la página http://es.debugmodeon.com/articulo/clonar-objetos-de-estructura-compleja me ha permitido ver cómo cualquier clase Serializable puede clonarse serializando primero en memoria y deserializando después. Por tanto, es cuestión de crear una clase auxiliar que nos permita tener un método útil para poder clonar clases con estructuras complejas:

    using System;
    using System.IO;
    using System.Runtime.Serialization;
    using System.Runtime.Serialization.Formatters.Binary;
    
    namespace JnSoftware
    {
        public static class Utiles
        {
            /// <summary>
            /// Permite una clonación en profundidad de origen
            /// </summary>
            /// <param name="origen">Objeto serializable</param>
            /// <exception cref="ArgumentExcepcion">
            /// Se produce cuando el objeto no es serializable.
            /// </exception>
            /// <remarks>Extraido de 
            /// http://es.debugmodeon.com/articulo/clonar-objetos-de-estructura-compleja
            /// </remarks>
            public static T Copia<T>(T origen)
            {
                // Verificamos que sea serializable antes de hacer la copia            
                if (!typeof(T).IsSerializable)
                    throw new ArgumentException("La clase " + typeof(T).ToString() + " no es serializable");
                
                // En caso de ser nulo el objeto, se devuelve tal cual
                if (Object.ReferenceEquals(origen, null))
                    return default(T);
                
                //Creamos un stream en memoria            
                IFormatter formatter = new BinaryFormatter();
                Stream stream = new MemoryStream();
                using (stream)
                {
                    try
                    {
                        formatter.Serialize(stream, origen);
                        stream.Seek(0, SeekOrigin.Begin);
                        //Deserializamos la porcón de memoria en el nuevo objeto                
                        return (T)formatter.Deserialize(stream);
                    }
                    catch (SerializationException ex)
                    { throw new ArgumentException(ex.Message, ex); }
                    catch { throw; }
                }
            }
        }
    }
     

    Eso sí, para todas aquellas clases que queramos Clonar con este método, deberemos añadir el atributo Serializable:

    [Serializable()]
    public class Coche : ICloneable

    Ahora, el método Clone de cualquiera de las clases que queramos “clonar”, será tan sencillo de escribir como el método MemberwiseClone()  que usamos al principio:

    public object Clone()
    {
        return JnSoftware.Utiles.Copia(this);
    }

    Confiemos que ya no tenga que ir copiando más las propiedades de una clase…

    2 thoughts on “C#: Implementar la Interface ICloneable

    Deja un comentario

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *