Algunas consideraciones sobre las structs

 ·  ☕ 7 min  ·  ✍️ eiximenis

    Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí

    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:

    Si quieres, puedes invitarme a un café xD

    eiximenis
    ESCRITO POR
    eiximenis
    Compulsive Developer