¿Usar “nuevo” en una estructura lo asigna en el montón o la stack?

Cuando crea una instancia de una clase con el new operador, la memoria se asigna en el montón. Cuando crea una instancia de una estructura con el new operador, ¿dónde se asigna la memoria, en el montón o en la stack?

De acuerdo, veamos si puedo aclarar esto.

En primer lugar, Ash tiene razón: la pregunta no es sobre dónde se asignan las variables de tipo de valor. Esa es una pregunta diferente, y una a la cual la respuesta no es solo “en la stack”. Es más complicado que eso (y aún más complicado por C # 2). Tengo un artículo sobre el tema y lo ampliaré si se solicita, pero tratemos solo con el new operador.

En segundo lugar, todo esto realmente depende de qué nivel estés hablando. Estoy mirando lo que el comstackdor hace con el código fuente, en términos de IL que crea. Es más que posible que el comstackdor JIT haga cosas inteligentes en términos de optimizar una gran cantidad de asignación “lógica”.

En tercer lugar, estoy ignorando los generics, sobre todo porque realmente no sé la respuesta, y en parte porque complicaría demasiado las cosas.

Finalmente, todo esto es solo con la implementación actual. La especificación de C # no especifica gran parte de esto; se trata efectivamente de un detalle de implementación. Hay quienes creen que los desarrolladores de código administrado realmente no deberían preocuparse. No estoy seguro de haber llegado tan lejos, pero vale la pena imaginar un mundo en el que, de hecho, todas las variables locales vivan en el montón, lo que aún se ajustaría a las especificaciones.


Hay dos situaciones diferentes con el new operador en los tipos de valor: puede llamar a un constructor sin parámetros (por ejemplo, new Guid() ) o un constructor con parámetros (por ejemplo, new Guid(someString) ). Estos generan IL significativamente diferente. Para comprender por qué, debe comparar las especificaciones C # y CLI: de acuerdo con C #, todos los tipos de valores tienen un constructor sin parámetros. Según la especificación CLI, ningún tipo de valor tiene constructores sin parámetros. (Recupere los constructores de un tipo de valor con reflexión alguna vez; no encontrará uno sin parámetros).

Tiene sentido para C # tratar el “inicializar un valor con ceros” como un constructor, porque mantiene la coherencia del lenguaje; se puede pensar en algo new(...) como siempre llamando a un constructor. Tiene sentido que la CLI piense de otra manera, ya que no hay un código real para llamar, y ciertamente no hay un código específico del tipo.

También hace una diferencia lo que vas a hacer con el valor después de haberlo inicializado. El IL utilizado para

 Guid localVariable = new Guid(someString); 

es diferente al IL utilizado para:

 myInstanceOrStaticVariable = new Guid(someString); 

Además, si el valor se usa como un valor intermedio, por ejemplo, un argumento para una llamada a un método, las cosas son ligeramente diferentes otra vez. Para mostrar todas estas diferencias, aquí hay un breve progtwig de prueba. No muestra la diferencia entre las variables estáticas y las variables de instancia: la IL diferiría entre stfld y stsfld , pero eso es todo.

 using System; public class Test { static Guid field; static void Main() {} static void MethodTakingGuid(Guid guid) {} static void ParameterisedCtorAssignToField() { field = new Guid(""); } static void ParameterisedCtorAssignToLocal() { Guid local = new Guid(""); // Force the value to be used local.ToString(); } static void ParameterisedCtorCallMethod() { MethodTakingGuid(new Guid("")); } static void ParameterlessCtorAssignToField() { field = new Guid(); } static void ParameterlessCtorAssignToLocal() { Guid local = new Guid(); // Force the value to be used local.ToString(); } static void ParameterlessCtorCallMethod() { MethodTakingGuid(new Guid()); } } 

Aquí está el IL para la clase, excluyendo bits irrelevantes (como nops):

 .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object { // Removed Test's constructor, Main, and MethodTakingGuid. .method private hidebysig static void ParameterisedCtorAssignToField() cil managed { .maxstack 8 L_0001: ldstr "" L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string) L_000b: stsfld valuetype [mscorlib]System.Guid Test::field L_0010: ret } .method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed { .maxstack 2 .locals init ([0] valuetype [mscorlib]System.Guid guid) L_0001: ldloca.s guid L_0003: ldstr "" L_0008: call instance void [mscorlib]System.Guid::.ctor(string) // Removed ToString() call L_001c: ret } .method private hidebysig static void ParameterisedCtorCallMethod() cil managed { .maxstack 8 L_0001: ldstr "" L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string) L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid) L_0011: ret } .method private hidebysig static void ParameterlessCtorAssignToField() cil managed { .maxstack 8 L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field L_0006: initobj [mscorlib]System.Guid L_000c: ret } .method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed { .maxstack 1 .locals init ([0] valuetype [mscorlib]System.Guid guid) L_0001: ldloca.s guid L_0003: initobj [mscorlib]System.Guid // Removed ToString() call L_0017: ret } .method private hidebysig static void ParameterlessCtorCallMethod() cil managed { .maxstack 1 .locals init ([0] valuetype [mscorlib]System.Guid guid) L_0001: ldloca.s guid L_0003: initobj [mscorlib]System.Guid L_0009: ldloc.0 L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid) L_0010: ret } .field private static valuetype [mscorlib]System.Guid field } 

Como puede ver, hay muchas instrucciones diferentes para llamar al constructor:

  • newobj : newobj el valor en la stack, llama a un constructor parametrizado. Se utiliza para valores intermedios, por ejemplo, para la asignación a un campo o como un argumento de método.
  • call instance : utiliza una ubicación de almacenamiento ya asignada (ya sea en la stack o no). Esto se usa en el código anterior para asignar a una variable local. Si a la misma variable local se le asigna un valor varias veces utilizando varias llamadas new , solo inicializa los datos sobre el valor anterior: no asigna más espacio de stack cada vez.
  • initobj : utiliza una ubicación de almacenamiento ya asignada y simplemente borra los datos. Esto se usa para todas nuestras llamadas de constructor sin parámetros, incluidas las que se asignan a una variable local. Para la llamada al método, una variable local intermedia se introduce de manera efectiva y su valor es borrado por initobj .

Espero que esto muestre cuán complicado es el tema, al mismo tiempo que arroja un poco de luz sobre él. En algunos sentidos conceptuales, cada llamada a new asigna espacio en la stack, pero como hemos visto, eso no es lo que sucede realmente incluso en el nivel IL. Me gustaría destacar un caso en particular. Toma este método:

 void HowManyStackAllocations() { Guid guid = new Guid(); // [...] Use guid guid = new Guid(someBytes); // [...] Use guid guid = new Guid(someString); // [...] Use guid } 

Eso “lógicamente” tiene 4 asignaciones de stack, una para la variable y una para cada una de las tres llamadas new , pero de hecho (para ese código específico) la stack solo se asigna una vez, y luego se reutiliza la misma ubicación de almacenamiento.

EDITAR: Para ser claros, esto solo es cierto en algunos casos … en particular, el valor de guid no será visible si el constructor Guid arroja una excepción, por lo que el comstackdor C # puede reutilizar la misma stack espacio. Consulte la publicación de blog de Eric Lippert sobre la construcción del tipo de valor para obtener más detalles y un caso en el que no se aplica.

Aprendí mucho al escribir esta respuesta: solicite una aclaración si algo no está claro.

La memoria que contiene los campos de una estructura se puede asignar a la stack o al montón dependiendo de las circunstancias. Si la variable struct-type es una variable local o un parámetro que no es capturado por un delegado o clase de iterador anónimo, entonces se asignará en la stack. Si la variable es parte de alguna clase, se asignará dentro de la clase en el montón.

Si la estructura se asigna en el montón, llamar al nuevo operador no es realmente necesario para asignar la memoria. El único propósito sería establecer los valores de campo según lo que esté en el constructor. Si no se llama al constructor, todos los campos obtendrán sus valores predeterminados (0 o nulo).

De forma similar para estructuras asignadas en la stack, excepto que C # requiere que todas las variables locales se establezcan en algún valor antes de que se utilicen, por lo que debe llamar a un constructor personalizado o predeterminado (un constructor que no toma parámetros está siempre disponible para estructuras).

Para decirlo de forma compacta, new es un nombre inapropiado para las estructuras, llamar a new simplemente llama al constructor. La única ubicación de almacenamiento para la estructura es la ubicación en la que está definida.

Si se trata de una variable miembro, se almacena directamente en lo que se define, si se trata de una variable o parámetro local, se almacena en la stack.

Contraste esto con las clases, que tienen una referencia dondequiera que la estructura se hubiera almacenado en su totalidad, mientras que los puntos de referencia en algún lugar en el montón. (Miembro dentro, local / parameter en la stack)

Puede ayudar mirar un poco en C ++, donde no hay una distinción real entre clase / estructura. (Hay nombres similares en el idioma, pero solo se refieren a la accesibilidad predeterminada de las cosas) Cuando llamas a nuevo, obtienes un puntero a la ubicación del montón, mientras que si tienes una referencia sin puntero, se almacena directamente en la stack o dentro del otro objeto, ala construye en C #.

Al igual que con todos los tipos de valores, las estructuras siempre van donde fueron declaradas .

Consulte esta pregunta aquí para obtener más detalles sobre cuándo usar las estructuras. Y esta pregunta aquí para obtener más información sobre las estructuras.

Editar: Yo había respondido mal que SIEMPRE van en la stack. Esto es incorrecto

Probablemente me falta algo aquí, pero ¿por qué nos importa la asignación?

Los tipos de valores se pasan por valor;) y, por lo tanto, no se pueden mutar en un ámbito diferente de donde se definen. Para poder modificar el valor, debe agregar la palabra clave [ref].

Los tipos de referencia se pasan por referencia y pueden mutarse.

Por supuesto, las cadenas de tipos de referencia inmutables son las más populares.

Diseño / inicialización de matriz: tipos de valor -> memoria cero [nombre, código postal] [nombre, código postal] Tipos de referencia -> memoria cero -> nulo [ref] [ref]

Una statement de class o struct es como un modelo que se usa para crear instancias u objetos en tiempo de ejecución. Si define una class o struct llamada Person, Person es el nombre del tipo. Si declara e inicializa una variable p de tipo Persona, p se dice que es un objeto o instancia de Persona. Se pueden crear varias instancias del mismo tipo de persona, y cada instancia puede tener diferentes valores en sus properties y fields .

Una class es un tipo de referencia. Cuando se crea un objeto de la class , la variable a la que está asignado el objeto solo contiene una referencia a esa memoria. Cuando la referencia del objeto se asigna a una nueva variable, la nueva variable se refiere al objeto original. Los cambios realizados a través de una variable se reflejan en la otra variable porque ambos se refieren a los mismos datos.

Una struct es un tipo de valor. Cuando se crea una struct , la variable a la que se asigna la estructura contiene los datos reales de la estructura. Cuando la struct se asigna a una nueva variable, se copia. La nueva variable y la variable original por lo tanto contienen dos copias separadas de los mismos datos. Los cambios realizados en una copia no afectan la otra copia.

En general, las classes se utilizan para modelar comportamientos más complejos o datos que se pretende modificar después de crear un objeto de class . Structs son más adecuadas para estructuras de datos pequeñas que contienen principalmente datos que no están destinados a modificarse una vez creada la struct .

para más…

Casi todas las estructuras que se consideran tipos de valor, se asignan en la stack, mientras que los objetos se asignan en el montón, mientras que la referencia del objeto (puntero) se asigna en la stack.

Las estructuras se asignan a la stack. Aquí hay una explicación útil:

Estructura

Además, las clases cuando se instancian dentro de .NET asignan memoria en el montón o en el espacio de memoria reservado de .NET. Mientras que las estructuras producen más eficiencia cuando se crean instancias debido a la asignación en la stack. Además, debe tenerse en cuenta que los parámetros de paso dentro de las estructuras se hacen así por valor.