Una de las cuestiones de concepto más comunes entre los alumnos de cursos de fundamentos de programación con .NET es la diferencia que existe entre clases y estructuras.
Al fin y al cabo se parecen mucho: ambas se pueden definir de manera similar, disponen de métodos y propiedades, ambas se pueden instanciar...
Por ejemplo, si definimos en C# una clase de nombre “Punto” como esta:
public class Punto
{
public int x,y;
public Punto(int X, int Y)
{
this.x = X;
this.y = Y;
}
public override string ToString()
{
return string.Format("Coordenadas del punto ({0},{1})", this.x, this.y);
}
}
Podemos muy fácilmente convertirla en una estructura cambiando la palabra clave class por struct:
public struct Punto
{
public int x,y;
public Punto(int X, int Y)
{
this.x = X;
this.y = Y;
}
public override string ToString()
{
return string.Format("Coordenadas del punto ({0},{1})", this.x, this.y);
}
}
Es exactamente igual salvo esa palabra clave: tiene un par de campos (x e y), un constructor y redefine el método ToString de la clase base Object.
La definamos como clase o como estructura la manera de usarla es exactamente la misma:
Punto punto1 = new Punto(5,10);
Console.WriteLine(punto1);
Es decir, se instancia con dos valores por defecto, y se muestra por pantalla (en este caso en la línea de comandos), obteniendo esto:
Entonces, si aparentemente funcionan igual ¿cuál es la principal diferencia entre una estructura y una clase?.
La principal diferencia entre una estructura y un clase en .NET es que las primeras son tipos por valor y las otras tipos por referencia. Es decir, aunque las estructuras pueden trabajar como clases, realmente son valores ubicados en la pila directamente, y no referencias a la información en memoria.
Esto, tan teórico, significa que las estructuras se pueden gestionar más eficientemente al instanciarlas (se hace más rápido), sobre todo en grandes cantidades, como una matriz. Al crear una matriz de estructuras, éstas se crean consecutivamente en memoria y no es necesario instanciar una a una y guardar sus referencias en la matriz, por lo que es mucho más rápido su uso.
Además, si pasas a un método una estructura, al hacerlo se crea una copia de la misma en lugar de pasar una referencia y por lo tanto los cambios aplicados sobre ella se pierden al terminar de ejecutarse la función, es decir, si cambias el valor de una propiedad dentro del código de la función, este cambio no se verá desde el código que llama a la función. Por ejemplo, si definimos esta función que toma un punto y le suma una determinada cantidad a sus coordenadas:
public void SumaCoordenadas(Punto p, int incremento)
{
p.x += incremento;
p.y += incremento;
}
Al llamarlo de esta manera:
Punto punto1 = new Punto(5,10);
SumaCoordenadas(punto1, 5);
Console.WriteLine(punto1);
Lo que obtenemos por pantalla cambia totalmente si “Punto” es una clase o una estructura. Si se trata de una clase, lo que recibe la función es una referencia a la instancia de la clase, es decir, se obtiene acceso a la verdadera clase y la información que maneja. Por ello, cuando dentro de la función se cambian los valores de x e y, se están cambiando los verdaderos valores de esas propiedades. Es decir, dentro de la función el parámetro “p” apunta a la mismo “almacén “ de datos en memoria que “punto1”. Por lo tanto, al mostrar por consola sus valores tras llamar a la función SumaCoordenadas lo que obtenemos es:
O sea, se ha modificado los valores de x e y en el objeto punto1.
Sin embargo ese mismo código exactamente aplicado sobre Punto cuando lo hemos definido como una estructura funciona de manera totalmente diferente. En se caso lo que obtenemos en “p” dentro de la función no es una referencia a la misma memoria a la que apunta “punto1”, sino que obtenemos una copia de la información. Por ello, aunque modifiquemos “x” e “y” dentro de la función, al terminar de ejecutarse ésta esos valores se pierden, puesto que ambas variables apuntan a objetos diferentes en la memoria. La ejecución del código anterior cuando “Punto” es una estructura devuelve lo mismo que en la primera figura:
Coordenadas del punto (5,10)
Además el rendimiento será menor en este caso en particular pues hay que copiar los datos.
Lo mismo pasa al aplicar ciertas funciones de objetos a estructuras en las que hay que hacer Boxing y Unboxing y eso merma el rendimiento.
¿Cuándo es interesante usar una estructura en lugar de una clase?
Lo habitual es no usar estructuras casi nunca, salvo para realizar optimizaciones.
Un caso concreto de uso de una estructura sería si, por ejemplo, quieres trabajar con números complejos, que como seguramente sabrás están representados por dos cifras: una parte real y una imaginaria. Si quieres tratarlos como un número más, es decir, que sean un tipo por valor, lo lógico es que los hagas estructuras para no tener que instanciarlos y para que cuando llames a métodos pasándole números complejos éstos se pasen por valor, y no se modifiquen con lo que hagas dentro del método al que los pasas. A todos los efectos se comportarían como si fueran otro tipo por valor, como un entero o un double.
Otro ejemplo, más común, es cuando debes trabajar con una cantidad muy elevada de objetos en memoria, uno tras otro. Por ejemplo imagínate que tienes que hacer una transformación sobre puntos tridimensionales en el espacio. Te puedes crear una clase que los represente, pero si debes ir creando y procesando miles de ellos será mucho más eficiente crearlos usando estructuras porque de este modo se crean en la pila y se libera su espacio en cuanto dejas de necesitarlos (los objetos quedarían en memoria hasta la próxima vez que los necesites).
También hay escenarios de interoperabilidad, cuando se llama a código nativo usando PInvoke, o a objetos COM usando COM/Interop, donde es necesario pasar estructuras y no clases. Pero vamos, como resumen quédate con que, salvo casos raros, lo habitual es que utilices clases en tu código.
Puedes conocer algunas diferencias más entre clases y estructuras, a la hora de usarlas en matrices, en este artículo.