sábado, 21 de junio de 2014

Pregunta C# (1 de 20): ¿Cuándo Usar una Estructura en Lugar de una Clase?

Índice

0. Introducción
1. ¿Qués es una Estructura?
2. Semántica de los Tipo por Valor
3. Herencia
4. Asignación
5. Valor por Defecto
6. Boxing y Unboxing
7. Uso de la Palabra Clave this
8. Inicializadores de Campos
9. Constructores
10. Destructores
11. Respuesta
12. Conclusiones
13. Glosario
14. Literatura & Enlaces

0. Introducción

Empiezo una serie de artículos en los que dedicaremos tiempo en afianzar y conocer nuevos conceptos a través de un enfoque muy entretenido y formativo: pregunta y respuesta. Esta idea se me ocurrió en un curso de la Microsoft Virtual Academy que empecé hoy mismo. Varias de las preguntas tienen de cierta manera ya su respuesta, sin embargo, quiero extenderme en detalles en cada una de ellas para sacar el máximo provecho de la oportunidad que ofrece Microsoft a  través de la MVA. Así que tendremos espacio para dar respuesta a 20 preguntas. En esta primera pregunta nos centraremos en profundizar en los detalles que diferencian una estructura (struct) de una clase, y además, en qué circunstancias es más apropiado usar una estructura que una clase. ¡Bienvenidos!

1. ¿Qué es una Estructura?

Un estructura es una construcción que representa un tipo por valor, y normalmente se usan para encapsular conjuntos de variables con una relación en particular. Ejemplos de uso de structs:
  • Coordenadas de un punto en un plano cartesiano.
  • Características de un elemento en un inventario.
En general podemos decir que una estructura es un útil en ambientes en donde es necesario las operaciones y estados que requieren los tipos por valor (los numéricos, por ejemplo).

Por otro lado, nosotros ya conocemos operaciones de asignación sobre tipos numéricos como:

int x = 5;
double y;
y = 3.14159;
char character = 'j';

Es evidente que la asignación de memoria (como en el caso de los objetos) no es necesaria para este tipo de estructura, sólo nos basta asignar un valor literal (e.g., 53.14159, o 'j') al identificador: en pocas palabras la instanciación no es necesaria (se puede omitir el uso de new [4]).

[Nota: En el artículo Structs en C# podrá encontrar más detalles acerca de las estructuras en C#.]

A continuación describiré varios de los puntos importantes que diferencian una estructura de una clase. Al final cuando ya tengamos claros estos puntos, responderemos la pregunta: ¿cuándo usar una estructura en lugar de una clase?

2. Semántica de los Tipos por Valor

Todas las estructuras tanto las definidas en la biblioteca base de clases de Microsoft .NET Framework e las propias definidas por el programador se clasifican como tipos por valor. Esta razón hace que las estructuras posean la semántica de valores. Por el contrario, las clases poseen semántica referencial (característica de los tipos por referencia).

Aquí se introduce una de las primeras diferencias entre una estructura y una clase, debido a que los una estructura almacena o contiene los valores asignados directamente en la variable del tipo de la estructura, mientras que una variable de una clase sólo contiene una referencia a la ubicación de memoria en donde realmente se hallan contenidos los datos del objeto. Podemos generalizar de la siguiente manera en cuanto a las estructuras de almacenamiento en donde se localizan cada tipo de estos tipos de datos:
  1. Una estructura (como todos los tipos de datos numéricos) localiza sus valores en la memoria stack
  2. mientras que una clase almacena los datos de un objeto en la memoria heap.
[Nota: Para conocer más acerca de estos tipos de memoria, recomiendo la lectura del artículo Variables y Parámetros.]

3. Herencia

Tanto las estructuras en la biblioteca base de clases de .NET Framework como las propias definidas por el programador heredan de la clase System.ValueType [8] (y esta de object). La herencia en las estructuras no está permitida, sin embargo, una estructura puede implementar una o más interfaces. Esta característica implica que una estructura posee el marcador sealed (no admisible para herencia), y al mismo tiempo no se admite la declaración de miembros abstractos.

De los modificadores de acceso disponibles para C# (Modificadores de Acceso), los únicos que no pueden ser utilizados para modificar la visibilidad de miembros de una estructura se hallan protected, y protected internal.

Por otra parte los miembros de una estructura no pueden utilizar el modificar virtual (debido a la restricción de herencia); y en las únicas situaciones en donde se permite la especificación de sobreescritura con el modificador override para un miembro es la operación de implementación de una interfaz por parte de una estructura.

4. Asignación

Debido a la naturaleza de tipo por valor de las estructuras, el hecho de asignar una variable de un tipo de estructura a otra variable (del mismo tipo) crea una copia de los valores que la estructura posee. Veamos esto con un ejemplo:

Archivo de código fuente Punto.cs [enlace alternativo]:
En las líneas 5-15 se declara la estructura Punto que representa un punto geométrico. En el código cliente (líneas 19-28) se declaran dos variables de esta estructura: sobre la línea 21 crea una instancia y le pasamos dos valores enteros a los argumentos del constructor de la estructura Punto; mientras que en la línea 22 asignamos a la variable b una copia del contenido de la variable a. Enseguida (línea 24), cambiamos el valor de la ordenada x de la variable a por 19. Este cambio sólo afecta a esta última variable, y no a la copia que creamos en la línea 24. El efecto de este cambio se aprecia en la salida que genera la ejecución de la línea 27.

> Prueba de ejecución.

Esto no sucede con las instancias de una clase, debido a que actúan como tipos por referencia. Es decir, si cambiamos el valor de cualquiera de los campos o propiedades de instancia, el cambio se ve reflejado sobre todas las variables que contengan la misma referencia.

5. Valores por Defecto

Cuando se crea una instancia de un tipo de estructura, los miembros (propiedades, o campos) se inicializan con su valor por defecto: los miembros de tipo por valor con su valor por defectos, y, los miembros de tipo por referencia con null. Asumamos que creamos un arreglo con 10 elementos de la estructura Punto (ver sección 4):

Punto[] puntos = new Punto[10];

Cada elemento Punto en el arreglo puntos será inicializado con los valores por defecto de los tipos de los miembros; en este caso, las coordenadas x-y en cero.

Es necesario agregar, de acuerdo con [12], que debido a que en una estructura no se puede declarar un constructor con cero parámetros. Lo anterior a razón de la declaración implícita de un constructor de esa naturaleza; este constructor se encargará de inicializar cada miembro con el valor por defecto de su tipo.

6. Boxing y Unboxing

La operación de conversión de un tipo de una estructura al tipo por referencia object (o una interfaz implementada por la estructura) se conoce como boxing. La operación contraria, unboxingobject (o una interfaz implementada por la estructura) a un tipo de una estructura.

En cambio, en los tipos de datos por referencia (e.g., una clase o una interfaz) la conversión, por ejemplo, de una clase a object o a una interfaz implementada por la clase, sólo permite un tratamiento distinto del tipo en tiempo de compilación; ahora al pasar de object a un tipo de clase, simplemente se recupera la referencia al objeto (sin embargo, en tiempo de ejecución se valida el casting [conversión]).

[Nota: En el artículo Boxing y Unboxing podrán encontrar otros detalles importantes acerca de las operaciones de boxing y unboxing.]

7. Uso de la Palabra Clave this

En una tipo de clase, la palabra clave this se clasifica como un valor cuando es usado dentro de un constructor; y dentro de miembros función de la clase se refiere a la instancia que invocó el miembro. El hecho de intentar asignar una nueva referencia a this ocasiona el error de compilación CS1604 [15] que consiste en:

Cannot assign to 'variable' because it is read-only

Archivo de código fuente thisUsoEnClase.cs [enlace alternativo]:

El intentar compilar este código se generá el error CS1604 (comentado inmediatamente arriba) en la línea 18, dado que se intenta asignar una nueva referencia a this, el cual es de solo lectura.

> Prueba de compilación.

Mensaje de error:
Intento de asignación referencia a this
Figura 1. Intento de asignación referencia a this.

En lo que se refiere a estructuras, this en un constructor se concuerda con un parámetro out de tipo de la estructura. Mientras que en un miembro de una función, this concuerda con un parámetro de tipo de la estructura. Todo lo anterior permite que this sea tratado como una variable, Veamos:

Archivo de código fuente thisUsoEnEstructura.cs [enlace alterantivo]:

En la línea 27 creamos una instancia de la estructura UsoThisEnEstructura y pasamos como argumento el valor 3. Más adelante invocamos al método Metodo con el argumento 4. Este último valor cambia el estado del campo campo (línea 16) y luego muestra su valor actual (línea 18). La parte en donde nos queremos concentrar aparece en la línea 20: notemos como creamos una nueva instancia de UsoThisEnEstructura pasando como argumento el valor 9. Esto toma efecto, y lo comprobamos en la salida que genera la ejecución de la sentencia ubicada en la línea 22.

> Prueba de ejecución.

Resultado:
4
9

8. Inicialización de Campos

Debido a que los campos de una estructura son inicializados con su valor por defecto (ver sección 4), la inicialización manual no está permitida. El intentar hacer esto:

public struct Punto
{
public int x = 7;
public int y = 5;
}

y tratar de compilarlo:

  1. csc /target:exe Punto.cs


Error:

stucts cannot have instance field initializers
prog.cs(14,16): error CS0573: `Punto.y': Structs cannot have instance field initializers

9. Constructores

En la sección 5 mencionamos que en una estructura, a diferencia de una clase, no es posible declarar explícitamente un constructor con cero parámetros. Implícitamente este constructor se crea para inicializar los valores por defecto de los campos. Sin embargo, los constructores con uno o más parámetros si están permitidos (inclusive la sobrecarga de constructores).

Ejemplo:

public Punto (int x, int y)
{
this.x = x;
this.y = y;
}

Por otro lado, en el cuerpo de un constructor de estructura no se puede invocar el constructor de inicialización de clase base, es decir:

base(...);

debido a que la herencia en las estructuras no está permitida [17].

10. Destructores

La declaración de destructores no está permitida en una estructura [18].

11. Respuesta

De acuerdo con lo que hemos visto en las 9 secciones anteriores, el diseño de una estructura es apropiado o preferible sobre una clase, en las siguientes circunstancias:
  • La semántica de una estructura sigue la de los tipos por valor.
  • Una estructura puede poseer miembros de estado y comportamiento.
  • Las estructuras no requieren de manejo sobre la memoria heap (a pesar de que los constructores se invoque con el operador new).
  • Las estructuras son útiles y apropiadas para estructuras de datos pequeñas.
  • Importante: las estructuras degradan el desempeño de una aplicación.
  • La invocación del constructor retorna la misma estructura con los campos miembro inicializados.
  • No se requiere de herencia.
  • Se requiere el manejo de valores inmutables (como los valores de los tipos numéricos.)
Veamos este ejemplo completo con la entidad Punto:

Archivo de código EstructuraVsClase.cs [enlace alternativo]:
Con los métodos ModificacionEstructuraPunto y ModificacionClasePunto se demuestra la diferencia entre una estructura y una clase:

  • ModificacionClasePunto (líneas 57-64):
    • El parámetro de este método recibe una copia de la variable estructuraPunto definida en la línea 35. Los cambios (asignación de nuevos valores a las variables x e y) sólo afectan al parámetro, y no a la variable pasada como argumento (línea 41).
  • ModificacionEstructuraPunto (líneas 66-73):
    • El parámetro de este método recibe una referencia, por lo tanto cualquier cambio que se haga a los campos del parámetro, también se verán reflejados en la instancia de tipo ClasePunto definida en la línea 47.
Compilación:

  1. csc /target:exe EstructuraVsClase.cs

Ejecución:

  1. .\EstructuraVsClase.exe


Resultado:
Prueba ejecución assembly EstructuraVsClase.exe
Figura 2. Prueba ejecución assembly EstructuraVsClase.exe.

12. Conclusiones

Se definieron varios de los aspectos inherentes a las estructuras. Esto permitió distintiguir varios rasgos distintivos de una estructura frente a una clase: una estructura es un tipo por valor, mientras que una clase es un tipo por referencia; la estructuras no soportan la herencia, las clases sí; una estructura es apta para representar estructuras de datos pequeñas (e.g., punto geométrico, registro de un partida un juego de mesa), entre otras diferencias destacables. En la sección de respuesta se presentó un listado con guías básicas para el diseño de una estructura sobre una clase; además de un ejemplo que destaca la diferencia de pasar una estructura (por valor), y una clase (por referencia) a un método.

13. Glosario

  • Asignación
  • Boxing
  • Clase
  • Constructor
  • Destructor
  • Estructura
  • Herencia
  • Iniciliazación
  • this
  • Tipo por referencia
  • Tipo por valor
  • Unboxing

14. Literatura & Enlaces

[1]: Microsoft Virtual Academy - http://www.microsoftvirtualacademy.com
[2]: Classes and Structs (C# Programming Guide) - http://msdn.microsoft.com/en-us/library/ms173109.aspx
[3]: 11.3 Class and struct differences (C#) - http://msdn.microsoft.com/en-us/library/aa664471(v=vs.71).aspx
[4]: ValueType Class (System) - http://msdn.microsoft.com/en-us/library/system.valuetype%28v=vs.110%29.aspx
[5]: Structs en C# | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/04/structs-en-c.html
[6]: Variables y Parámetros en C# - Parte 1 | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2013/09/variables-y-parametros-en-c-parte-1.html
[7]: 11.3.1 Value semantics (C#) - http://msdn.microsoft.com/en-us/library/aa664472(v=vs.71).aspx
[8]: ValueType Class (System) - http://msdn.microsoft.com/en-us/library/system.valuetype%28v=vs.110%29.aspx
[9]: Modificadores de Acceso en C# | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/04/modificadores-de-acceso-en-c.html
[10]: 11.3.2 Inheritance (C#) - http://msdn.microsoft.com/en-us/library/aa664473(v=vs.71).aspx
[11]: 11.3.3 Assignment (C#) - http://msdn.microsoft.com/en-us/library/aa664474(v=vs.71).aspx
[12]: 11.3.4 Default values (C#) - http://msdn.microsoft.com/en-us/library/aa664475(v=vs.71).aspx
[13]: Boxing y Unboxing en C# | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/03/boxing-y-unboxing-en-c.html
[14]: 11.3.5 Boxing and unboxing (C#) - http://msdn.microsoft.com/en-us/library/aa664476(v=vs.71).aspx
[15]: Compiler Error CS1604 - http://msdn.microsoft.com/en-us/library/fkcb07fa(v=vs.90).aspx
[16]: 11.3.7 Field initializers (C#) - http://msdn.microsoft.com/en-us/library/aa664478(v=vs.71).aspx
[17]: 11.3.8 Constructors (C#) - http://msdn.microsoft.com/en-us/library/aa664479(v=vs.71).aspx


J

No hay comentarios:

Publicar un comentario

Envíe sus comentarios, dudas, sugerencias, críticas. Gracias.