3. METHODOLOGY
3.5. DATA ANALYSIS
Dado que toda estructura deriva de System.Object, ha de ser posible a través del polimorfismo almacenar objetos de estos tipos en objetos object. Sin embargo, esto no puede hacerse directamente debido a las diferencias semánticas y de almacenamiento que existen entre clases y estructuras: un object siempre ha de almacenar una referencia a un objeto en memoria dinámica y una estructura no tiene porqué estarlo. Por ello ha de realizársele antes al objeto de tipo valor una conversión conocida como boxing. Recíprocamente, al proceso de conversión de un object que contenga un objeto de un tipo valor al tipo valor original se le denomina unboxing.
El proceso de boxing es muy sencillo. Consiste en envolver el objeto de tipo valor en un objeto de un tipo referencia creado específicamente para ello. Por ejemplo, para un objeto de un tipo valor T, el tipo referencia creado sería de la forma:
class T_Box {
El lenguaje de programación C# Tema 13: Estructuras T_Box(T t) { value = t; } }
En realidad todo esto ocurre de forma transparente al programador, el cual simplemente asigna el objeto de tipo valor a un objeto de tipo referencia como si de cualquier asignación polimórfica se tratase. Por ejemplo:
int p = new Punto(10,10);
object o = p; // boxing. Es equivalente a object o = new Punto_Box(p);
En realidad la clase envoltorio arriba escrita no se crea nunca, pero conceptualmente es como si se crease. Esto se puede comprobar viendo a través del siguiente código que el verdadero tipo del objeto o del ejemplo anterior sigue siendo Punto (y no Punto_Box):
Console.WriteLine((p is Punto));
La salida por pantalla de este código es True, lo que confirma que se sigue considerando que en realidad o almacena un Punto (recuérdese que el operador is sólo devuelve true si el objeto que se le pasa como operando izquierdo es del tipo que se le indica como operando derecho)
El proceso de unboxing es también transparente al programador. Por ejemplo, para recuperar como Punto el valor de tipo Punto almacenado en el objeto o anterior se haría:
p = (Punto) o; // Es equivalente a ((Punto_Box) o).value
Obviamente durante el unboxing se hará una comprobación de tipo para asegurar que el objeto almacenado en o es realmente de tipo Punto. Esta comprobación es tan estricta que se ha de cumplir que el tipo especificado sea exactamente el mismo que el tipo original del objeto, no vale que sea un compatible. Por tanto, este código es inválido:
int i = 123; object o = i;
long l = (long) o // Error: o contiene un int, no un long Sin embargo, lo que si sería válido es hacer:
long l = (long) (int) o;
Como se puede apreciar en el constructor del tipo envoltorio creado, durante el boxing el envoltorio que se crea recibe una copia del valor del objeto a convertir, por lo que los cambios que se le hagan no afectarán al objeto original. Por ello, la salida del siguiente código será 10:
Punto p = new Punto(10,10);
object o = p; // boxing p.X = 100;
Sin embargo, si Punto se hubiese definido como una clase entonces sí que se mostraría por pantalla un 100 ya que entonces no se haría boxing en la asignación de p a o sino que se aplicaría el mecanismo de polimorfismo normal, que consiste en tratar p a través de o como si fuese de tipo object pero sin realizarse ninguna conversión.
El problema del boxing y el unboxing es que son procesos lentos, ya que implican la creación y destrucción de objetos envoltorio. Por ello puede interesar evitarlos en aquellas situaciones donde la velocidad de ejecución de la aplicación sea crítica, y para ello se proponen varias técnicas:
• Si el problema se debe al paso de estructuras como parámetros de métodos genéricos que tomen parámetros de tipo object, puede convenir definir sobrecargas de esos métodos que en lugar de tomar objects tomen objetos de los tipos estructura que en concreto la aplicación utiliza
• Siguiendo en la línea de lo anterior, puede que interese usar plantillas en lugar de tipos genéricos. Éstas no son más que definiciones de tipos de datos en las que no se indica cuál es el tipo exacto de ciertas variables sino que se deja en función de parámetros a los que puede dárseles distintos valores al crear cada objeto de ese tipo. Así, en vez de crearse siempre objetos con métodos que tomen parámetros object, se podrían ir creando diferentes versiones del tipo según el tipo de estructura con la que se vaya a trabajar.
Actuálmente el CLR puede trabajar con plantillas, pero se espera que en versiones futuras del .NET Framework lo haga y el lenguaje C# las incluya.
• Muchas veces conviene hacer unboxing para poder acceder a miembros específicos de ciertas estructuras almacenadas en objects, aunque a continuación vuelva a necesitarse realmacenar la estructura en un object. Para evitar esto una posibilidad sería almacenar en el objecto no directamente la estructura sino un objeto de una clase envolvente creada a medida por el programador y que incluya los miembros necesarios para hacer las operaciones anteriores. Así se evitaría tener que hacer unboxing, pues se convertiría de object a esa clase, que no es un tipo valor y por tanto no implica unboxing.
• Con la misma idea, otra posiibilidad sería que el tipo estructura implementase ciertas interfaces mediante las que se pudiese hacer las operaciones antes comentadas. Aunque las interfaces no se tratarán hasta el Tema 15: Interfaces, por ahora basta saber que las interfaces son también tipos referencia y por tanto convertir de object a un tipo interfaz no implica unboxing.
Constructores
Los constructores de las estructuras se comportan de una forma distinta a los de las clases. Por un lado, no pueden incluir ningún inicializador base debido a que como no puede haber herencia el compilador siempre sabe que ha de llamar al constructor sin parámetros de System.ValueType. Por otro, dentro de su cuerpo no se puede acceder a sus miembros hasta inicializarlos, pues para ahorrar tiempo no se les da ningún valor inicial antes de llamar al constructor.
El lenguaje de programación C# Tema 13: Estructuras
Sin embargo, la diferencia más importante entre los constructores de ambos tipos se encuentra en la implementación del constructor sin parámetros: como los objetos estructura no puede almacenar el valor por defecto null cuando se declaran sin usar constructor ya que ese valor indica referencia a posición de memoria dinámica indeterminada y los objetos estructura no almacenan referencias, toda estructura siempre tiene definido un constructor sin parámetros que lo que hace es darle en esos casos un valor por defecto a los objetos declarados. Ese valor consiste en poner a cero toda la memoria ocupada por el objeto, lo que tiene el efecto de dar como valor a cada campo el cero de su tipo12. Por ejemplo, el siguiente código imprime un 0 en pantalla:
Punto p = new Punto();
Console.WriteLine(p.X); Y el siguiente también:
using System; struct Punto
{
public int X,Y;
}
class EjemploConstructorDefecto {
Punto p;
public static void Main() {
Console.WriteLine(p.X); }
}
Sin embargo, el hecho de que este constructor por defecto se aplique no implica que se pueda acceder a las variables locales sin antes inicializarlas con otro valor. Por ejemplo, el siguiente fragmente de código de un método sería incorrecto:
Punto p;
Console.WriteLine(p.X); // X no inicializada
Sin embrago, como a las estructuras declaradas sin constructor no se les da el valor por defecto null, sí que sería válido:
Punto p;
p.X = 2;
Console.WriteLine(p.X);
Para asegurar un valor por defecto común a todos los objetos estructura, se prohíbe al programador dar una definición propia de su constructor sin parámetros. Mientras que en las clases es opcional implementarlo y si no se hace el compilador introduce uno por defecto, en las estructuras no es válido hacerlo. Además, aún en el caso de que se definan otros constructores, el constructor sin parámetros seguirá siendo introducido
automáticamente por el compilador a diferencia de cómo ocurría con las clases donde en ese caso el compilador no lo introducía.
Por otro lado, para conseguir que el valor por defecto de todos los objetos estructuras sea el mismo, se prohíbe darles una valor inicial a sus campos en el momento de declararlos, pues si no el constructor por defecto habría de tenerlos en cuenta y su ejecución sería más ineficiente. Por esta razón, los constructores definidos por el programador para una estructura han de inicializar todos sus miembros no estáticos en tanto que antes de llamarlos no se les da ningún valor inicial.
Nótese que debido a la existencia de un constructor por defecto cuya implementación escapa de manos del programador, el código de los métodos de una estructura puede tener que considerar la posibilidad de que se acceda a ellos con los valores resultantes de una inicialización con ese constructor. Por ejemplo, dado:
struct A {
public readonly string S; public A(string s)
{
if (s==null)
throw (new ArgumentNullException()); this.s = S;
} }
Nada asegura que en este código los objetos de clase A siempre se inicialicen con un valor distinto de null en su campo S, pues aunque el constructor definido para A comprueba que eso no ocurra lanzando una excepción en caso de que se le pase una cadena que valga null, si el programador usa el constructor por defecto creará un objeto en el que S valga null. Además, ni siquiera es válido especificar un valor inicial a S en su definición, ya que para inicializar rápidamente las estructuras sus campos no estáticos no pueden tener valores iniciales.
El lenguaje de programación C# Tema 14: Enumeraciones
TEMA 14: Enumeraciones
Concepto de enumeración
Una enumeración o tipo enumerado es un tipo especial de estructura en la que los literales de los valores que pueden tomar sus objetos se indican explícitamente al definirla. Por ejemplo, una enumeración de nombre Tamaño cuyos objetos pudiesen tomar los valores literales Pequeño, Mediano o Grande se definiría así:
enum Tamaño { Pequeño, Mediano, Grande }
Para entender bien la principal utilidad de las enumeraciones vamos a ver antes un problema muy típico en programación: si queremos definir un método que pueda imprimir por pantalla un cierto texto con diferentes tamaños, una primera posibilidad sería dotarlo de un parámetro de algún tipo entero que indique el tamaño con el que se desea mostrar el texto. A estos números que los métodos interpretan con significados específicos se les suele denominar números mágicos, y su utilización tiene los inconvenientes de que dificulta la legibilidad del código (hay que recordar que significa para el método cada valor del número) y su escritura (hay que recordar qué número ha pasársele al método para que que funcione de una cierta forma)
Una alternativa mejor para el método anterior consiste en definirlo de modo que tome un parámetro de tipo Tamaño para que así el programador usuario no tenga que recordar la correspondencia entre tamaños y números. Véase así como la llamada (2) del ejemplo que sigue es mucho más legible que la (1):
obj.MuestraTexto(2); // (1) obj.MuestraTexto(Tamaño.Mediano); // (2)
Además, estos literales no sólo facilitan la escritura y lectura del código sino que también pueden ser usados por herramientas de documentación, depuradores u otras aplicaciones para sustituir números mágicos y mostrar textos muchos más legibles. Por otro lado, usar enumeraciones también facilita el mantenimiento del código. Por ejemplo, si el método (1) anterior se hubiese definido de forma que 1 significase tamaño pequeño, 2 mediano y 3 grande, cuando se quisiese incluir un nuevo tamaño intermedio entre pequeño y mediano habría que darle un valor superior a 3 o inferior a 1 ya que los demás estarían cogidos, lo que rompería el orden de menor a mayor entre números y tamaños asociados. Sin embargo, usando una enumeración no importaría mantener el orden relativo y bastaría añadirle un nuevo literal.
Otra ventaja de usar enumeraciones frente a números mágicos es que éstas participan en el mecanismo de comprobación de tipos de C# y el CLR. Así, si un método espera un objeto y se le pasa uno de otro tipo enumerado se producirá, según cuando se
detecte la incoherencia, un error en compilación o una excepción en ejecución. Sin embargo, si se hubiesen usado números mágicos del mismo tipo en vez de enumeraciones no se habría detectado nada, pues en ambos casos para el compilador y el CLR serían simples números sin ningún significado especial asociado.