Algunas consideraciones sobre las structs

El otro día un tweet de Juan Quijano, animó una pequeña discusión sobre la diferencia entre clases y estructuras en .NET. Este no es el primer post que escribo al respecto, pero bueno, aprovechando la coyuntura vamos a comentar algunas de las cosas que se mencionaron en el pequeño debate que generó el tweet de Juan.

Seguramente todos ya sabréis que la diferencia fundamental entre clases y estructuras en .NET es que las primeras son tipos por referencia y las segundas tipos por valor. Es decir cuando trabajamos con una clase lo hacemos siempre a través de una referencia, que “apunta” al objeto real. Por otro lado cuando trabajamos con una estructura podemos asumir que no hay referencia  y que trabajamos con el “objeto real”, sin intermediarios visibles.

De esta diferencia se deduce que si no hay referencias es imposible que dos variables distintas apunten a la misma estructura. Por lo tanto, siendo S una estructura, el código “S a = b;” clona la estructura contenida en la variable b en la variable a. Por lo que después de la asignación tenemos dos estructuras idénticas. Pero tenemos dos. Eso no ocurre en las clases donde el mismo código (si S fuese una clase) haría que tuviesémos dos referencias (a y b) apuntando a un único objeto.

De esto mismo se deduce que el valor “null” no es válido en una estructura. El valor null es un valor de referencia, no de objeto. Es decir, un objeto no puede ser null, es la referencia que vale null para indicar que no apunta a ningún objeto. En el caso de las estructuras, dado que no hay referencia, no puede haber el valor null: todas las variables de tipo struct contienen su propio y único objeto.

Igualmente cuando pasamos una estructura como parámetro de una función, lo que recibe la función no es la estructura original si no una copia. Y por supuesto lo mismo ocurre cuando devolvemos una estructura como valor de retorno. Son casos análogos al de la asignación que veíamos antes.

Para inicializar una variable de tipo struct se usa una sintaxis unificada con la de las clases:

  1. var p = new Point();

De hecho leyendo este código no sabemos si “Point” es una estructura o una clase: el lenguaje unifica la inicialización de ambos tipos para una mayor coherencia. Eso es  bueno, porque tenemos una sola forma de inicializar objetos (con new) pero a cambio nos puede confundir y hacernos creer que ambos tipos (clases y estructuras) son más parecidos de lo que realmente son.

Un tema interesante es que las estructuras siempre tienen el constructor por defecto. No se puede evitar, ni aún declarando otro constructor. Así dada la siguiente estructura:

  1. struct Point
  2. {
  3.     public int X;
  4.     public int Y;
  5.     public Point(int x, int y)
  6.     {
  7.         X = x;
  8.         Y= y;
  9.     }
  10. }

Se pueden crear objetos Point usando new Point(). Observa que eso no es cierto en una clase: si se declara un constructor en una clase, el constructor por defecto deja de existir.

Por supuesto, si el constructor por defecto existe siempre, es lícito preguntarnos qué hace: Pues inicializa todos los miembros de la struct a su valor por defecto. En este caso “new Point()” nos devolverá un punto en el que el miembro X y el miembro Y tienen el valor default(int) que es 0. Es posible que este comportamiento no te guste o lo quieras cambiar. Pues mala suerte: C# no permite redefinir el constructor por defecto de una estructura. El siguiente código no compila:

  1. struct Point
  2. {
  3.     public int X;
  4.     public int Y;
  5.     public Point(int x, int y)
  6.     {
  7.         X = x;
  8.         Y= y;
  9.     }
  10.     public Point()
  11.     {
  12.         X = 0;
  13.         Y = 0;
  14.     }
  15. }

El error, además, es muy claro y no deja lugar a dudas: “CS0568 Structs cannot contain explicit parameterless constructors”. Una de las novedades que se preveyeron para C#6 fue precisamente que se pueda definir el constructor por defecto de las structs (y estuvo presente en algunas RCs), pero al final se descartó. Así que de momento no es posible.

Esta explicación debería ya resolver la duda de Juan. Él se preguntaba porque new Guid() devolvía un Guid vacío (todo ceros) y para obtener un Guid único era necesario hacer Guid.NewGuid(). Pues la razón es, obviamente, que Guid es una struct. Por lo tanto “new Guid()” invoca al constructor por defecto que tienen todas las estructuras y que inicializa todos los miembros a su valor por defecto y que no podemos cambiar (una limitación a mi parecer un poco ridícula, pero es lo que hay).

Otro de los comentarios típicos que se hacen sobre las estructuras es que estas residen en la pila, mientras que las clases residen en el heap. Por supuesto los objetos que son instancias de clases residen en el heap. Es obvio ya que son, por definición, “long-lived objects”, es decir la vida del objeto está desligada del ámbito de la referencia que lo contiene. Así dado el siguiente código y asumiendo que “Rect” es una clase:

  1. Rect Foo()
  2. {
  3.     var obj = new Rect();
  4.     // Do something
  5. }

El objeto asignado en la referencia “obj” sigue existiendo incluso después de salir de la función Foo. Quizá en este caso se podría pensar que el compilador podría ser lo suficientemente inteligente como para detectar que la referencia “obj” no se copia hacia ningún sitio de la función y podría eliminar el objeto creado al salir de Foo, pero… ¿para qué complicar el compilador teniendo ya un elemento que se encarga de eliminar objetos inaccesibles? Al salir de Foo, el objeto Rect es inaccesible (no hay referencia alguna que apunte a él) y el garbage collector ya lo eliminará.

Observa que es imprescindible que los objetos instancias de clases tengan un ciclo de vida independiente del de las referencias que las contienen. Dado este código:

  1. Rect Foo()
  2. {
  3.     var obj = new Rect();
  4.     // Do something
  5.     return obj;
  6. }

El objeto Rect creado no puede ser eliminado al salir de la función Foo porque en este caso dado un código “var f = Foo()” la referencia “f” terminaría apuntando a un objeto eliminado.

Eso misma en una structura no es necesario, porque en este caso el objeto creado dentro de la función se copia cuando devolvemos de la función, por lo que el original puede ser destruído sin miedo.

Vale, queda claro que unos (clases) son objetos “long-lived” y otros (structs) no tienen porque serlo. ¿Pero eso implica que las structs deban guardarse en la pila y las clases en el heap?. No es del todo cierto… la realidad es que las clases sí que deben guardarse en el heap pero las estructuras no tienen por qué guardarse en la pila. Ojo, se puede ¿eh? Pero no es imprescindible. La especificación de .NET no obliga a que los objetos struct se guarden en la pila. Es algo que depende de la implementación. De hecho, realmente a veces puede ocurrir que objetos estructura estén en el heap. Y la realidad es que, como desarrollador, debe darte igual si un objeto estructura está en la pila o el heap. Esto es un detalle de la implementación y no es importante. Lo que debes entender es la semántica de valor de las estructuras.

La asignación de this

Como comenté en el hilo que se generó a raíz del tweet de Juan, las stucts tienen otras curiosidades, y una de ellas es la asignación de this. Básicamente, en una struct, this no es una constante si no una variable, y por lo tanto puede asignarse. Así, el siguiente código es válido:

  1. struct Point
  2. {
  3.     public int X;
  4.     public int Y;
  5.     public Point(int x, int y)
  6.     {
  7.         X = x;
  8.         Y= y;
  9.     }
  10.  
  11.     public void CloneInc(Point p) {
  12.         this = p;
  13.         X++;
  14.         Y++;
  15.     }
  16. }

Observa como el método “CloneInc” asigna a this el valor del parámetro p. Recuerda que hablamos de estructuras, por lo que eso asigna a this una copia del objeto estructura contenido en p. Por eso tiene sentido esa asignación. En una clase eso no es posible, porque esa asignación implicaría que la referencia “this” se modifica, y que pasa a apuntar a otro objeto. En cambio en una estructura eso no es así. Simplemente significa que se modifica el valor del objeto contenido en this. Por eso es legal y tiene sentido poder hacer eso.

Y nada más, os dejo con otros posts sobre structs con más información:

Un saludo!

4 comentarios sobre “Algunas consideraciones sobre las structs”

  1. Hola Eduard,
    Magnífico post, como siempre (esto no es nuevo) 🙂
    Mi pregunta es ¿Cuándo usar una estructura en vez de una clase?
    Yo básicamente las uso poco (por no decir nada). Uso las del framework pero no creo nuevas estructuras. La teoría (creo) es que si vas a crear un nuevo tipo y ninguna de las limitaciones que tienen las estructuras te afectan, pues adelante (
    al respecto de limitaciones agrego, porque creo no esta en el post, que no pueden heredar pero sí implementar interfaces).
    Es decir, si no tengo el ejemplo canónico Point, ¿Cuándo crear una estructura?
    Yo reconozco (de nuevo) que al final son tantos los puntos a tener en cuenta en la batalla Struct vs Class, que termino siempre creando clases. Sólo si tuviera que crear «chorrocientosmil» objetos y por el tema de que son «lightweight» me pensaría hacer una estructura, pero vamos… lo dicho, al final tiro por clases.
    Revisando este enlace http://stackoverflow.com/questions/85553/when-should-i-use-a-struct-instead-of-a-class me gusta lo que dice uno que es «que la estructura no tienen identidad, 2 iguales son la misma», eso podría ser otra razón 🙂
    Un saludo!

    1. Buenas Sergio! 😉

      Pues en general las estructuras deberían usarse para pequeños tipos de datos, tales como los que vienen en el framework (DateTime, Guid) o los «clásicos» ejemplos (Complex, Point, Rect, Vector2D, Vector3D y similares).
      La idea es que al ser short-lived objects una buena implementación (como la de MS) será muy eficiente destruyendo esos objetos. A pesar del coste de «copiarlos» cada vez, muchas veces vale la pena (a pesar de que la especificación NO obliga a usar la pila, una especificación que usase SIEMPRE el heap para guardar estructuras no sería muy «inteligente»).
      Al no tener identidad se pueden «memoizar» sin ningún temor que alguien acceda a la versión «memoizada» y la modifique. Personalmente preferiría que las estructuras fuesen inmutables (a pesar de que una asignación a una propiedad implicase una copia entera de la estructura), y así siempre intento construirlas.

      De todos modos, tampoco te creas que mis proyectos están llenos de ellas… 😀

  2. A partir del post, me surge una duda puntual. En cuáles casos sería idóneo emplear clases y en cuáles structs? Gracias.

Responder a Sergio León Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *