c# .net memory-management

c# - ¿El uso de "nuevo" en una estructura lo asigna en el montón o pila?



.net memory-management (8)

Al igual que con todos los tipos de valor, las estructuras siempre van donde se declararon .

Vea esta pregunta here para obtener más detalles sobre cuándo usar estructuras. Y esta pregunta here para más información sobre estructuras.

Edit: Yo había respondido erróneamente que SIEMPRE van en la pila. Esto es incorrect

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


Bien, veamos si puedo aclarar esto.

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

En segundo lugar, todo esto realmente depende de qué nivel estás hablando. Estoy viendo lo que hace el compilador con el código fuente, en términos de la IL que crea. Es más que posible que el compilador JIT haga cosas inteligentes en términos de optimizar una gran cantidad de asignaciones "lógicas".

En tercer lugar, estoy ignorando los genéricos, sobre todo porque no conozco la respuesta, y en parte porque complicaría mucho las cosas.

Finalmente, todo esto es solo con la implementación actual. La especificación de C # no especifica mucho de esto, es efectivamente un detalle de implementación. Hay quienes creen que a los desarrolladores de código administrado realmente no les debería importar. No estoy seguro de ir tan lejos, pero vale la pena imaginar un mundo donde, de hecho, todas las variables locales viven en el montón, lo que aún estaría de acuerdo con la especificación.

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 a un constructor con parámetros (por ejemplo, new Guid(someString) ). Estos generan IL significativamente diferente. Para entender por qué, necesita comparar las especificaciones de C # y CLI: según C #, todos los tipos de valor tienen un constructor sin parámetros. Según la especificación de CLI, ningún tipo de valor tiene constructores sin parámetros. (Obtenga los constructores de un tipo de valor con reflexión alguna vez, no encontrará uno sin parámetros).

Es lógico que C # trate el "inicializar un valor con ceros" como un constructor, ya que mantiene el lenguaje coherente; se puede pensar que el new(...) siempre llama a un constructor. Es lógico que el CLI lo piense de manera diferente, ya que no hay un código real al que llamar, y ciertamente ningún código específico para cada 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 a la IL utilizada para:

myInstanceOrStaticVariable = new Guid(someString);

Además, si el valor se utiliza como un valor intermedio, por ejemplo, un argumento para una llamada a un método, las cosas vuelven a ser ligeramente diferentes. Para mostrar todas estas diferencias, aquí hay un breve programa de prueba. No muestra la diferencia entre variables estáticas y 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 los 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 utilizadas para llamar al constructor:

  • newobj : newobj el valor en la pila, llama a un constructor parametrizado. Se utiliza para valores intermedios, por ejemplo, para la asignación a un campo o para usar como un argumento de método.
  • call instance : utiliza una ubicación de almacenamiento ya asignada (ya sea en la pila 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 , simplemente inicializa los datos por encima del valor anterior, no asigna más espacio de pila cada vez.
  • initobj : utiliza una ubicación de almacenamiento ya asignada y simplemente borra los datos. Esto se usa para todas nuestras llamadas de constructores sin parámetros, incluidas las que se asignan a una variable local. Para la llamada al método, se introduce efectivamente una variable local intermedia, y su valor se initobj con initobj .

Espero que esto muestre lo complicado que es el tema, al mismo tiempo que brille un poco de luz sobre él. En algunos sentidos conceptuales, cada llamada a un new espacio asignado en la pila, pero como hemos visto, eso no es lo que realmente sucede, incluso a nivel de IL. Me gustaría destacar un caso 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 }

Esa "lógica" tiene 4 asignaciones de pila, una para la variable y otra para cada una de las tres new llamadas, pero de hecho (para ese código específico) la pila solo se asigna una vez, y luego se reutiliza la misma ubicación de almacenamiento.

EDIT: Solo para ser claro, esto solo es cierto en algunos casos ... en particular, el valor de guid no será visible si el constructor Guid lanza una excepción, por lo que el compilador de C # puede reutilizar la misma pila espacio. Consulte la publicación del 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.

He aprendido mucho por escrito sobre esta respuesta. ¡Solicite una aclaración si no está claro!


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


La memoria que contiene los campos de una estructura puede asignarse en la pila o en el montón, según las circunstancias. Si la variable de tipo estructura es una variable o parámetro local que no es capturado por algún delegado anónimo o clase de iterador, entonces se asignará en la pila. Si la variable es parte de alguna clase, entonces se asignará dentro de la clase en el montón.

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

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


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

Structs

Además, las clases cuando se crean instancias 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 crea una instancia debido a la asignación en la pila. Además, se debe tener en cuenta que los parámetros de paso dentro de las estructuras se realizan de forma automática.


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

Si es una variable miembro, se almacena directamente en lo que esté definido, si es una variable o parámetro local, se almacena en la pila.

Contraste esto con las clases, que tienen una referencia donde la estructura se habría almacenado en su totalidad, mientras que los puntos de referencia en algún lugar en el montón. (Miembro dentro, local / parámetro en pila)

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 llama nuevo, obtiene un puntero a la ubicación del montón, mientras que si tiene una referencia que no es de puntero, se almacena directamente en la pila o dentro del otro objeto, ala estructuras en C #.


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

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

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

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

Disposición de matrices / inicialización: tipos de valor -> memoria cero [nombre, zip] [nombre, zip] tipos de referencia -> memoria cero -> nula [ref] [ref]


Una declaración de class o struct es como un plano que se utiliza para crear instancias u objetos en tiempo de ejecución. Si define una class o struct llamada Persona, Persona es el nombre del tipo. Si declara e inicializa una variable p de tipo Persona, se dice que p es un objeto o instancia de Persona. Se pueden crear múltiples 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 se asigna el objeto contiene solo 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 a la otra copia.

En general, las classes se utilizan para modelar comportamientos más complejos, o los datos que se pretenden 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 se pretende modificar después de que se crea la struct .

para más...