sábado, 7 de octubre de 2017

Curso de C#: conversiones y estructuras


Conversiones definidas

Con la sobrecarga de operadores es posible sumar objetos de clases distintas e incompatibles. Las conversiones definidas vienen a abundar un poco más sobre estos conceptos, permitiendo hacer compatibles tipos que antes no lo eran.
El fragmento de código a continuación es extraño al tener que usar constructores y propiedades:

Metros m=new Metros(10);
Centimetros c=new Centimetros(10);

Metros SumaMetros=m+c;
Centimetros SumaCentimetros=c+m;

Console.WriteLine(SumaMetros.Cantidad);
Console.WriteLine(SumaCentimetros.Cantidad);
Las conversiones definidas permiten manejarlo de un modo más natural:
Metros m=(Metros) 10;
Centimetros c=(Centimetros) 10;

Metros SumaMetros=m+c;
Centimetros SumaCentimetros=c+m;

Console.WriteLine((double) SumaMetros);
Console.WriteLine((double) SumaCentimetros);

Curso de C#: conversiones y estructuras


Ya no es necesario complicar el código con constructores ni con la propiedad Cantidad para escribir su valor con WriteLine, pues se han creado conversiones definidas que han hecho compatibles esas clases con el tipo double. Gracias a esto es posible convertir a metros un número (primera línea) y también se puede convertir a double un objeto de la clase Metros, dentro de WriteLine. A continuación el código de estas conversiones definidas en la clase Metros:

public static explicit operator Metros(double cant)
{
    Metros retValue=new Metros(cant);

    return retValue;
}

public static explicit operator double(Metros m)
{
    return m.Cantidad;
}

En la primera conversión definida, la conversión que es Metros, de modo que  es posible convertir en objetos de esta clase cualquier número de tipo double con la sintaxis (Metros) numero. En la segunda se hace justo lo contrario, se define la conversión double para convertir en datos de este tipo objetos de la clase Metro, mediante la sintaxis (double) ObjetoMetros. En la primera conversión definida el argumento es de tipo double, y en la segunda es de tipo Metros.
La palabra explicit hace que sea obligatorio usar el operador de conversión para convertir entre estos dos tipos. No se puede asignar un valor de tipo double a un objeto de la clase Metros sin usar el operador de conversión (Metros). También es posible definir estas conversiones como implícitas, de modo que el operador de conversión no fuera necesario. Para ello, bastaría con poner la palabra implicit en lugar de explicit, esto es, así:

public static implicit operator Metros(double cant)
{
    Metros retValue=new Metros(cant);

    return retValue;
}

public static implicit operator double(Metros m)
{
    return m.Cantidad;
}

El resultado todavía es mucho más manejable. El código cliente de las clases Metros y Centimetros quedaría así.

Metros m=10;
Centimetros c=10;

Metros SumaMetros=m+c;
Centimetros SumaCentimetros=c+m;

Console.WriteLine(SumaMetros);
Console.WriteLine(SumaCentimetros);

Ni siquiera se ha necesitado hacer las conversiones explícitas, ya que las conversiones definidas en ambas clases se han hecho implícitas con la palabra clave implicit en lugar de explicit. Ahora es posible asignar  el número 10, que es de tipo int al no tener el sufijo D, aunque la conversión definida está creada solamente para el tipo double, esto es así, porque el tipo double es compatible con el tipo int, de modo que basta con hacer la conversión definida con el tipo double para que sirva con todos los tipos numéricos.
Si se intenta asignar un objeto de la clase centímetros a otro de la clase metros y viceversa:

Metros cEnMetros=c;
Centimetros mEnCentimetros=m;

El compilador genera otro error. Antes de hacer eso es necesario crear también las conversiones definidas en Metros y Centímetros para hacerlas compatibles y, también, para que se asignen los valores adecuadamente:
En la clase Metros.

public static implicit operator Metros(Centimetros c)
{
    Metros retValue=new Metros();
    retValue.Cantidad=c.Cantidad/100;

    return retValue;
}

En la clase Centímetros:

public static implicit operator Centimetros(Metros m)
{
    Centimetros retValue=new Centimetros();
    retValue.Cantidad=m.Cantidad*100;

    return retValue;
}

No basta con escribir las conversiones para hacerlas compatibles, sino que también hay que escribirlas correctamente para que se asignen valores adecuados, es decir, si a "m" (que es de tipo Metros) le asignamos 10 centímetros, "m" tiene que valer, necesariamente, 0.1 metros.
Otro detalle, sería reescribir el método ToString (este método se hereda siempre de System.Object). Dicho método tendría que definirse así tanto en la clase Metros como en la clase Centímetros:

public override string ToString()
{
    return this.cantidad.ToString();
}

La sobrecarga de operadores y las conversiones definidas se deben utilizar únicamente cuando el resultado final sea más natural e intuitivo que sin utilizarlas.
Si a través de la sobrecarga de operadores y conversiones definidas se convierten dos tipos como compatibles, se debe intentar que sean compatibles a todos los niveles posibles. Como se acaba de ver, en las clases Metros y Centimetros, habría que sobrecargar también los operadores /, *, %, >, >=, <, <=, == y != para hacer posibles las divisiones, multiplicaciones, el cálculo de restos y las comparaciones (es decir, la comparación 1 metro == 100 centímetros debería retornar true).

Estructuras

Las clases representan tipos referencia, cuando se instancia un objeto de una determinada clase, lo que hace, es crear un objeto en la memoria, y la variable devuelve internamente un puntero al comienzo de dicho objeto. Para crear un tipo valor, se hace con las estructuras.
Usar clases para almacenar algo que realmente son valores va a restar eficiencia y también funcionará de un modo distinto y peor.
Se ha diseñado una clase Moneda. Para hacer una asignación para crear una copia de un objeto de esta clase hacemos esto:

Moneda m
m=new Moneda(10);
Moneda m2=m.Clone();

Resulta bastante raro, no por invocar el método Clone, sino por tener que invocarlo para crear una simple copia de su valor en otra variable, puesto que la clase Moneda puede ser considerada, como un valor.

Moneda m
m=10;
Moneda m2=m;

Este último código es más fácil y natural, para ello se ha utilizado una estructura en lugar de una clase:

public struct Moneda
{
    private double valor;

    public static implicit operator Moneda(double d)
    {
        Moneda m;
        m.valor=Math.Round(d,2);

        return m;
    }

    public static implicit operator double(Moneda m)
    {
        return m.valor;
    }

    public static Moneda operator++(Moneda m)
    {
        m.valor++;
        return m;
    }

    public static Moneda operator--(Moneda m)
    {
        m.valor--;
        return m;
    }

    public override string ToString()
    {
        return this.valor.ToString();
    }
}

Se parece mucho a una clase, pero aquí se ha utilizado la palabra struct en lugar de class. La mayoría de los conceptos de las clases sirven también para las estructuras. Pero, no todos, pues hay cosas habituales en las clases que no se pueden hacer con las estructuras:
Se pueden escribir constructores, pero estos han de tener argumentos. No se puede escribir un constructor sin argumentos para una estructura.
Al tratarse de un tipo valor que se almacena en la pila, las estructuras no admiten destructores.
No soportan herencia, por lo que no pueden ser utilizadas como clases base ni como clases derivadas. Pero sí pueden implementar interfaces, y lo hacen igual que las clases.
Los campos de una estructura no pueden ser inicializados en la declaración. Por ejemplo, no es posible decir int a=10; porque daría un error de compilación.
Las estructuras, si no se especifican los modificadores ref o out, se pasan por valor, mientras que las clases se pasan siempre por referencia.

No es necesario instanciar una estructura para poder usar sus miembros, en el segundo fragmento de código del ejemplo no se ha instanciado el objeto m.

No hay comentarios:

Publicar un comentario