Sobre interfaces, patrón NullObject y estructuras en C#

Voy a intentar explicar algunas curiosidades que en C# tienen las estructuras al respecto de la implementación de interfaces, y de paso también algunas limitaciones con las que me he encontrado al intentar implementar un patrón NullObject con las mismas. Empecemos.

En C# es posible implementar interfaces en structs. Pero tiene algunos efectos colaterales inesperados, debido a que cuando se almacena en una variable del tipo interfaz, se realiza una conversión a tipo por referencia, obteniendo así una copia de la instancia original. Los cambios que realicemos en la copia no se verán reflejados en el original.

Supongamos que tenemos la siguiente estructura, compuesta por un Id que no se puede cambiar, un miembro Amount que si puede variar llamando al método AddAmount, y los operadores de igualdad sobreescritos para implementar adecuadamente la comparación (en este caso, para que no tenga en cuenta Amount).

    public struct Asignable : IAsignable, IEquatable<Asignable>
    {
        private readonly int _Id;
        private int _Amount;

        public Asignable (int id)
        {
            _Id = id;
            _Amount = 0;
        }

        public bool Equals (IAsignable other)
        {
            if (ReferenceEquals (null, other))
            {
                return false;
            }
            return other.Equals (this);
        }

        public int Id
        {
            get
            {
                return _Id;
            }
        }

        public int Amount
        {
            get
            {
                return _Amount;
            }
        }

        public bool Equals (Asignable other)
        {
            return _Id == other._Id;
        }

        public void AddAmount ()
        {
            _Amount ++;
        }

        public override bool Equals (object obj)
        {
            if (ReferenceEquals (null, obj))
            {
                return false;
            }
            return obj is Asignable && Equals ((Asignable) obj);
        }

        public override int GetHashCode ()
        {
            return _Id;
        }

        public static bool operator == (Asignable left, Asignable right)
        {
            return left.Equals (right);
        }

        public static bool operator != (Asignable left, Asignable right)
        {
            return !left.Equals (right);
        }
    }

Nuestro primer experimento es comprobar cómo las instancias son distintas al convertir al interface:

Asignable c = new Asignable (2);
IAsignable c_interface = c;
c_interface.AddAmount ();

Podríamos pensar que AddAmount () afectase al objeto original, Sin embargo no es cierto, y la instrucción afecta a c_interface pero no a la instancia almacenada en c
.

Debug.WriteLine ("c.amount: {0}", c.Amount);
Debug.WriteLine ("c_interface.amount: {0}", c_interface.Amount);
c.amount: 0
c_interface.amount: 1

Un segundo experimento nos muestra que aunque nuestra estructura sea inmutable, también debemos tener cuidado. Nuestra estructura consiste en un simple Id que identifica la instancia.

 Asignable a = new Asignable (2);
 Asignable b = new Asignable (2);

Debug.WriteLine (a == b);
IAsignable a_interface = a;
IAsignable b_interface = b;
Debug.WriteLine ("a_interface == b_interface: {0}", a_interface == b_interface);

El resultado es:

a_interface == b_interface: False

Dejando a parte las interfaces, otra característica de las estructuras es que no pueden contener campos de su mismo tipo, es decir, no pueden incluirse dentro de si mismos de modo recursivo. Tampoco es posible hacerlo indirectamente (A contiene a B que contiene a A, o cualquier otro ciclo similar). Si lo intentamos obtendremos un bonito error Struct member 'info' causes a cycle in the struct layout‘ en tiempo de compilación. Esto es debido a que las estructuras tienen un tamaño máximo fijo. Si hay una autorreferencia, el tamaño máximo es… infinito. Véase esta respuesta de Stack Overflow.

Hay un problema derivado de está última limitación. Imaginemos que queremos implementar un patrón  NullObject con un struct (de esa necesidad nace precisamente este post). La idea es sustituir los típicos Nullable<> por algo que no exija permanentemente comprobar si tiene valor para acceder a sus miembros, a la vez que permite seguir comparando instancias por valor. Dado que la instancia que representa un null es siempre igual (algo similar a String.Empty, que simpre devuelve “”), no es mala idea hacer que dicha instancia se construya sólo una vez. (véase las notas de está página)

Podríamos hacerlo parecido a:

         public static IAsignable? _Empty;

        public static IAsignable Empty
        {
            get
            {
                if (!_Empty.HasValue)
                    _Empty = new IAsignable ();
                return _Empty.Value;
            }
        }

pero obtendremos un error. Así que no nos queda otro remedio que: a) usar una clase, a la que también se puede forzar por valor, b) poner la propiedad estática Empty en otra clase, cosa que yo no haría por coherencia con otros diseños como String.Empty, c) no hacer que sea un singleton, en cuyo caso el recolector de basura tendrá más trabajo debido a la creación redundante de objetos Null. Como siempre, la solución depende del contexto.

Anuncios
Tagged with: , ,
Publicado en Programacion

Deixa a túa opinión

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. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: