Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

¿Qué diferencia hay entre usar o no "New" al declarar una variable cuyo tipo es una estructura?

NET-framework

Seguimos a vueltas con algunas cuestiones de fundamentos relacionadas con la plataforma .NET...

Esta pregunta pregunta me la hizo hace poco un alumno del curso "Visual Studio desde cero". De hecho, dado que no es la primera vez que me lo preguntan, he pensado que podría ser útil a la comunidad, y por eso reproduzco aquí mi respuesta.

Consideremos por ejemplo esta estructura en C#:

public struct Punto 
{
    public int x, y;
}

O en VB:

Public Structure Punto
    Public x, y As Integer
End Structure

Puedes declarar una variable que las use de dos formas diferentes: usando New o no. Por ejemplo en VB:

Dim p As Punto
Dim p As New Punto

¿Cuál es la diferencia?

A priori parece no haber ninguna. Sin embargo sí que la hay, y tiene que ver con extender las estructuras con propiedades y métodos.

A las estructuras se les puede añadir métodos, propiedades, etc.. como si fueran clases. Por ejemplo, consideremos el siguiente código en C#:

public struct Punto
{
    int x, y;
   
    public int X
    {
        get { return x; }
        set { x = value; }
    }

    public int Y
    {
        get { return y; }
        set { y = value; }
    }
}

Si escribimos el siguiente código (el más habitual):

Punto p;
Console.WriteLine (p.X);

obtendremos un error que nos dice que la variable interna 'x' no está inicializada. Sin embargo, con este otro código:

Punto p = new Punto();
Console.WriteLine (p.x);

No se produciría error alguno.

Lo importante aquí es que, al ser declaradas por defecto (sin constructor), las estructuras no inicializan sus miembros internos, por lo tanto antes de usarlas debemos asignarles un valor.

Cada propiedad tiene un campo interno que no está inicializado cuando intentamos usarlo a través de la propiedad por primera vez, y obtendrías un error al hacerlo (no se sabe si tu propiedad antes de asignarlo intenta hacer algo con él y por lo tanto te obliga a tenerlo inicializado).

Si usas el constructor por defecto explícitamente (con la palabra clave new) lo que éste hace es inicializar todos los miembros internos a su valor por defecto, y a partir de ese momento ya te deja utilizarlos. Si no usas New no se llama al constructor por defecto y los campos están sin inicializar.

José M. Alarcón Aguín Fundador de campusMVP, es ingeniero industrial y especialista en consultoría de empresa. Ha escrito diversos libros, habiendo publicado hasta la fecha cientos de artículos sobre informática e ingeniería en publicaciones especializadas. Microsoft lo ha reconocido como MVP (Most Valuable Professional) en desarrollo web desde el año 2004 hasta la actualidad. Puedes seguirlo en Twitter en @jm_alarcon o leer sus blog técnico o personal. Ver todos los posts de José M. Alarcón Aguín
Archivado en: Lenguajes y plataformas

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Comentarios (14) -

Eso no es más que otra de las absurdideces (aunque yo lo llamo aberraciones) de diseño de C#, que tiene que ver con que una estructura herede de ValueType (que se asigna por valor), pero que a su vez hereda de Object (que se asigna por referencia), manteniendo una especie de absurda dualidad pila/montón...

Si no pones new se coloca en la pila pero no se llama al constructor. Absurdo.
Si pones new se coloca en la pila (o el montículo, no estoy seguro) pero sí se llama al constructor.

¿En qué quedamos? ¿Los constructores se llaman o no se llaman al crear un objeto?

Leyendo a Sutter nos damos cuenta de que es una estúpida limitación del motor net a la hora de colocar objetos en la pila... máxime cuando creo que en C++/CLI sí que se llama al constructor (no me acuerdo del todo bien, pero creo que sí lo hace)...

En fin, más de lo mismo.

Ah, por cierto, buena entrada y explicación del tema. :-)

Responder

José M. Alarcón
Spain José M. Alarcón

Hola Rafa,

Ya tardabas en criticar a C#, jajaja. Como sois los de C y C++ ;-)

Seguro que no te falta razón, pero alguna razón habrá para que Hejlsberg haya decidido tirar por ahí...

Hace unos días escribí la primera parte de este, donde hablo sobre la pila y todas esas cosas:

/recursos/post/Clases-y-estructuras-en-Net-cuando-usar-cual.aspx

Saludos y gracias por comentar.

Responder

Bueno, no lo digo yo, ojo, fue Sutter el que se lo dejó caer en su Rationale. Me refiero a lo de la limitación del .NET.

Y sobre lo otro (que es una aberración ya que muta un tipo de objeto en otro), me imagino el susto de Hejlsberg; todo iban a ser objetos, todo todo todo (como la niña del anuncio), y luego se dieron cuenta que no, que un int no puede ser un objeto todo el tiempo o se iban a comer los mocos... y salió eso. Podía haber salido otra cosa, podían haber implementado algo más natural, como lo intenta C++/CLI sin salirse del CLR, pero no, pegote de masa y a seguir.

:-)

Acabo de leer la otra entrada. Mola eso de no usar las estructuras excepto cuando sea un tema de optimización. Yo más bien diría de compatibilidad hacia atrás. El NET gestiona muy bien y rápido la memoria (aunque lo de rápido venga a costa de no liberar nada a no ser que lo hagas a mano), así que mejor dejarlo palomica suelta y cuando tengas que recuperar memoria sí o sí llamas a un collect.

Responder

Eduard Tomàs
Spain Eduard Tomàs

Ojo! En .NET las structs NO están siempre en la pila. Jon Skeet lo aclara en un artículo (que es un must read): http://jonskeet.uk/csharp/memory.html

Para mi la diferencia realmente importante entre structs y clases tiene que ver con la semántica de valor que ofrecen las primeras y que no tienen las segundas (en tanto que asignar una struct a otra es copiar los datos realmente).

En el resto coincido con @rfog: O el constructor se llama siempre o nunca. Ese comportamiento de "sí pero no" que ofrecen las structs no me convence para nada... pero bueno... es lo que hay ;-)

Saludos!

Responder

Eduard Tomàs
Spain Eduard Tomàs

Por cierto... que veo que el post es viejo... Pues me ha aparecido destacado hoy en facebook.

Que cosas... xD

Responder

José Manuel Alarcón
Spain José Manuel Alarcón

Sí, jejeje, es lo que se llama contenido "evergreen": que no caduca y que de vez en cuando viene bien volver a retuitear o compartir en redes sociales como recordatorio :-)

Responder

Con todo respeto es lo más estúpido que he leido este mes.

Gracias por compartir igual.

Responder

José Manuel Alarcón
Spain José Manuel Alarcón

Hola Raúl:

¿A qué te refieres exactamente? ¿Al post o algún comentario en particular?
Por que si te refieres al post... bueno, puede que estés o no de acuerdo en cómo se ha diseñado el comportamiento de las estructuras en .NET por parte de Microsoft, pero lo cierto es que funcionan así como describo en el contenido, y esa pequeña diferencia en la inicialización de las variables puede ser importante, y desde luego no todo el mundo lo sabe.

Si eres tan amable de aclararnos qué es lo que te parece estúpido y por qué, te lo agradeceré.

Saludos.

Responder

Como puse en Facebook (copio aquí):

Creo que el artículo está bastante mal ( al menos en C#, vb.net no lo conozco).

Por ejemplo la frase: "Lo importante aquí es que, al ser declaradas por defecto (sin constructor), las estructuras no inicializan sus miembros internos, por lo tanto antes de usarlas debemos asignarles un valor.", no tiene sentido...

Al usar "new" se inicializa el objeto que contiene la variable (en vuestro caso, "p"), si no se usa "new" (de hecho, para este caso y siendo una estructura, podíais usar igualmente "default(Punto)", no hace falta especificamente hacer "new"), la variable no apunta a ningún objeto y por eso da error de que no está inicializada (que el compilador te diga que lo que no está inicializado es el miembro al que quieres acceder es logico, porque es a lo que estás accediendo cuando detecta el error, pero lo que no está inicializado es la variable local "p")... no tiene absolutamente nada que ver con que "sus miembros internos estén inicializados" o no... de hecho, en esta estructura en concreto, se usan primitivas que -siempre- están inicializadas (obviamente cuando el objeto que las contiene está inicializado).

De hecho, si mirais la especificación de C# (punto 10.5.4), dice especificamente: "The initial value of a field, whether it be a static field or an instance field, is the default value (§5.2) of the field’s type. It is not possible to observe the value of a field before this default initialization has occurred, and a field is thus never “uninitialized”...

Lo que contradice de lleno prácticamente toda la redacción de vuestro artículo, que habla de la "falta de inicialización de miembros internos".

Estoy seguro que el concepto lo tenéis claro, pero la redacción es totalmente errónea.
--

Y añado: todo esto tiene que ver con la inicialización de "variables locales", no de estructuras, clases, o miembros internos. Una variable local en C# no está inicializada por defecto, y hay que asignarle algún valor... pero es sólo eso, las variables locales. Cuando una variable es un miembro de un objeto, su valor SIEMPRE está inicializado (a su default... si es value, al que toque, y si es de referencia, a null)... cuando es una variable local no, y por tanto hay que asignarle un valor (porque una variable no es un objeto: una variable APUNTA a un objeto... obviamente si el objeto no está creado, la variable no apunta a ninguna parte y no podemos acceder a él)

De nuevo, nada tiene que ver con struct o class... y aquí un ejemplo muy claro... puedo acceder a un miembro de una struct sin hacer "new" de esa struct en ningún sitio, sin errores, ni nada (y no es código precisamente ofuscado, ni un "edge case", ni nada parecido): https://dotnetfiddle.net/7WUzkk

Si hiciera el mismo ejemplo y "Punto" fuera de tipo "class" (https://dotnetfiddle.net/306UEP), tampoco el compilador me dice que está sin inicializar... da una NullReferenceException (en runtime) porque el miembro "p" apuntara a "null" (que es su default), pero está inicializado, sin hacer "new".

Me parece que en este artículo se mezclan churros con merinas y hay un batiburrillo de cosas, que como ya he puesto arriba, contradicen de pleno las especificaciones de C# (y si algo bueno tiene C# son sus especificaciones).

Como dije, vb.net no lo conozco demasiado, así que no opino, puede que funcione de manera diferente.

Responder

José Manuel Alarcón
José Manuel Alarcón

Hola:

Creo que a lo mejor el que no estás entendiendo correctamente el contexto del post eres tú, pero en cualquier caso te agradezco los comentarios. Siento si no me he explicado bien para los lectores más avanzados como tú.

Lo que estoy explicando en este post es que las estructuras son tipos por valor, y por lo tanto se comportan como tales en el contexto de una función. Por ello, si declaras una variable de tipo estructura pero no la inicializas (da igual que lo hagas con default o con new, pues son totalmente equivalentes), no podrás acceder tampoco a ninguno de sus miembros porque tampoco están inicializados. Ese sería el resumen.

Esto no puede ocurrir con un tipo por referencia como una clase. Y de hecho el ejemplo que tú pones efectivamente lo demuestra. El motivo es que, como bien dice la referencia del lenguaje C#, al inicializar una clase como la "PuntoClase" que has definido en tu ejemplo, los miembros del objeto siempre están inicializados, y por lo tanto puedes acceder al campo "p" (de tipo estructura "Punto") porque el runtime de .NET los ha inicializado antes por ti. Pero es una convención, algo que puedes dar por sentado en los miembros de una clase, pero no en otros lugares.

De hecho es muy fácil de comprobar.

Si compilamos el código de tu ejemplo a un .exe y lo abrimos con ILDASM.exe (el decompilador que viene con el SDK de .NET en Windows) lo que veremos será el siguiente código MSIL (pongo un enlace ya que en los comentarios no se soportan imágenes):

/recursos/image.axd?picture=newObj.png

La instrucción newobj de MSIL lo que hace, según documentación de Microsoft es:

weblogs.asp.net/.../introduction-to-msil-part-4-defining-type-members

"The newobj instruction allocates a new instance of the class associated with ctor and initializes all the fields in the new instance to 0 (of the proper type) or null references as appropriate. It then calls the constructor ctor with the given arguments along with the newly created instance. After the constructor has been called, the now initialized object reference (type O) is pushed on the stack."

Lo tienes también aquí: msdn.microsoft.com/.../...tion.emit.opcodes.newobj

Por eso, cuando forman parte de una clase se inicializan siempre de manera automática y los puedes utilizar sin instanciarlos con new o sin asignarles explícitamente el valor por defecto con default. Pero cuando no están como miembros de una clase, sino en una función o en un código general (que es de lo que yo hablo en el post), si no los inicializas no los puedes utilizar. De hecho el compilador no te dejará compilar la aplicación porque hay una variable por valor sin inicializar. En este caso además, como es una estructura (insisto: y por tanto un tipo por valor), cuando intentas acceder a su miembro "x" te da un error en ese miembro ya que solo es un atajo directo a dicho valor sin inicializar, que debería estar guardado en la pila y no lo está.

De hecho, si todavía no estás convencido, podemos ver el código cuando asignas valores directamente a la estructura, sin haberla inicializado antes. Por ejemplo, es te código tan simple dentro del método main del programa:

   Punto p;
   p.x = 1;

Si lo compilamos y lo desensamblamos con ILDASM veremos lo siguiente:

/recursos/image.axd?picture=InicializacionMiembroEstructura.png

Como puedes observar la estructura existe y lo que está sin inicializar son sus miembros. De hecho en este ejemplo el miembro 'y' sigue sin inicializar, pero el código ha compilado.

Todo esto es ya entrar a muy bajo nivel, pero dado que parece que no había quedado claro, lo explico sin problema.

Por cierto, todo esto es independiente del lenguaje que utilices ya que forma parte de los entresijos de .NET. Es idéntico en C#, en VB.NET o en cualquier otro lenguaje de la plataforma.

Espero que esto lo aclare y siento que no te haya quedado claro en el post original. La intención nunca fue entrar tan a bajo nivel como ahora, sino solamente explicar un concepto de la manera más simple posible a personas que están comenzando con .NET. En cualquier caso, no deja de ser una cuestión de cómo expresar un concepto con una u otra palabra, o sea, una cuestión semántica.

Saludos.

Responder

Si, lo entiendo perfectamente (y sé cómo funciona) pero creo que sin quererlo mezclaste precisamente eso, el bajo nivel de aquí (a eso me refería con churras con merinas) con el alto nivel con el que se ha querido expresar el post (o de nuevo, así lo entendí yo).

Se mezcla la inicialización de los espacios de memoria de un objeto con la inicialización de variables locales y de nuevo, "miembros internos", algo que nunca llega a siquiera pasar porque el compilador no compila código que lea de variables locales sin inicializar (con lo que ni se puede llegar al IL, porque no compila... y a eso me refería con la diferencia de vb.net y C#, es posible que vb.net tuviera otras especificaciones y lo compilara de forma diferente).

Y por eso indico que creo que el concepto lo tenías claro, pero que la redacción del artículo es errónea: o se habla de una cosa (del alto nivel, errores de compilador, inicialización de variables locales, etc., que es lo que entendí yo que quería expresarse aquí), o se habla de la inicialización de variables en memoria (que es completamente distinto, y mucho más complicado de lo que se expresa en este post, incluso de lo que se expresa en tu respuesta)... si no, se mezclan churras con merinas y se monta el batiburrillo que dije en mi respuesta :-)

Responder

José Manuel Alarcón
José Manuel Alarcón

Estamos de acuerdo en que no estamos de acuerdo...

Yo creo que estás hilando demasiado fino en la semántica, y además hay algunas cosas que dices que no son correctas tampoco.

En cualquier caso, no creo que tenga tanta importancia como para seguir dándole vueltas.

Saludos!

Responder

Eduard Tomàs
Eduard Tomàs

Anda! Que movida que se ha montado por aquí xDDDD

Para mi, intuyo, que estáis metidos en una discusión semántica... A ver, en C# hay 4 maneras de crear una struct...

S s = new S();
S s = default(S);
S s = new S(...);     // Solo si S define un constructor con params
S s;

Las dos primeras son equivalentes e inicializan la estructura y todos sus miembros internos al valor por defecto según el tipo de miembro.
La tercera, pues inicializa la estructura y da el valor a sus miembros según el código del constructor.
La cuarta es la que estamos discutiendo... :D

El tema está en que NO puedo hacer

struct S {
     public int x;
     public int Foo() {...}    // Da igual lo que haga Foo
}
S s;
s.Foo();

El valor que me da el compilador es que x NO está inicializada. El compilador habla explícitamente de x, no de s. La pregunta es si consideramos que s está inicializada si x no lo está.
Es interesante resaltar que SÍ que puedo hacer:
S s;
s.x = 100;
s.Foo();

Esto es válido. Eso significa que el ESPACIO necesario para almacenar un objeto de tipo S, ha sido reservado en el momento de la declaración de la variable s (lógico si tenemos en cuenta que S es un tipo por valor y siempre debe valer algo). Pero si sizeof(S) = xx, el compilador ya ha reservado esos xx bytes de espacio, en el momento en que declaramos la variable s;
Personalmente tiendo a considerar que la variable s tiene valor, ya que si s no tiene valor, entonces s.x  = 100 deberia ser incorrecto, en tanto s.x no tendría significado. Pero lo tiene. Y, al menos quien diseñó los mensajes de error de C# tiene el mismo concepto: la variable s tiene valor, pero a s.x NO se le ha establecido valor de forma explícita y por lo tanto eso es lo que dice el compilador.

A partir de ahí, si consideramos que "inicializar S" es lo mismo que "dar valores a todos sus miembros", entonces S s NO incializa la struct Pero si consideramos que "inicializar S" es "reservar el sizeof(S) bytes" entonces S s inicializa la struct y lo que no tenemos inicializados son sus miembros...

En todo caso... me encantan esas discusiones!!! :D

Thx!
edu

Responder

¿Habéis tenido en cuenta que una estructura es un tipo-valor? ¿Se crea como tipo-referencia cuando lo haces con new, y se boxea/unboxea (o al revés en el proceso)? ¿Puedes crear una struct virtual pura? Y en ese caso, ¿cómo son las v-tables, punteros o referencias al método llamable? ¿Qué ocurre cuando pasas una estructura a un método que dispara una excepción y pasas una referencia a esa estructura en uno de los campos de tu excepción (en principio al estar en la pila y el código que captura la excepción más arriba, ¿es un puntero loco o un loco puntero?)?

(No estoy siguiendo la discusión, solo es por meter cizalla). :-P

PS: A ver, ahora en serio, si estáis discutiendo sobre cuántos ángeles caben en la cabeza de un alfiler, lo mejor es hacer ejemplos de código y tirar de DotPeek o similar, aunque eso os dará solo la parte teórica y estática, luego el JIT hará lo que le salga de las pelotaris. Pues no me ha pasado de darle a los puntos suspensivos en el debugger y ver diferentes cosas en diferentes sesiones con un mismo código fuente... aunque supongo que con el ahora .NET Native ese el tema volverá a ser determinista, como en, ejem, C++. :-D

Responder

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.