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

290

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

kedar kamthe
fuente

Respuestas:

305

Bien, veamos si puedo aclarar esto.

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

En segundo lugar, todo esto realmente depende del nivel del que estés hablando. Estoy mirando 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 asignación "lógica".

En tercer lugar, estoy ignorando los genéricos, principalmente porque en realidad 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 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 llegar 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 newoperador en los tipos de valores: 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 de 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. (Obtenga los constructores de un tipo de valor con reflexión en algún momento; no encontrará uno sin parámetros).

Tiene sentido que C # trate el "inicializar un valor con ceros" como un constructor, porque mantiene el lenguaje consistente; puede pensar new(...)que siempre se llama a un constructor. Tiene sentido que la CLI piense de manera diferente, ya que no hay un código real al que llamar, y ciertamente no hay un código específico de tipo.

También hace una diferencia lo que va a hacer con el valor después de haberlo inicializado. La IL utilizada para

Guid localVariable = new Guid(someString);

es diferente a la IL utilizada para:

myInstanceOrStaticVariable = new Guid(someString);

Además, si el valor se usa como un valor intermedio, por ejemplo, un argumento para una llamada al método, las cosas son ligeramente diferentes nuevamente. 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 stfldy 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: Asigna el valor en la pila, llama a un constructor parametrizado. Se usa para valores intermedios, por ejemplo, para la asignación a un campo o se usa como 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 mediante varias newllamadas, solo 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 constructor 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 se borra su valor initobj.

Espero que esto muestre lo complicado que es el tema, al mismo tiempo que arroja algo de luz sobre él En algunos sentidos conceptuales, cada llamada a newasignar espacio 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 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 pila, una para la variable y otra para cada una de las tres newllamadas, 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.

EDITAR: para ser claros, esto solo es cierto en algunos casos ... en particular, el valor de guidno será visible si el Guidconstructor lanza una excepción, por lo que el compilador de C # puede reutilizar el mismo espacio de pila. Consulte la publicación del blog de Eric Lippert sobre la construcción de tipos de valor para obtener más detalles y un caso en el que no se aplica.

Aprendí mucho al escribir esta respuesta. ¡Solicite aclaraciones si algo de eso no está claro!

Jon Skeet
fuente
1
Jon, el código de ejemplo HowManyStackAllocations es bueno. Pero, ¿podría cambiarlo para usar un Struct en lugar de Guid o agregar un nuevo ejemplo de Struct? Creo que eso abordaría directamente la pregunta original de @ kedar.
Ash el
99
Guid ya es una estructura. Consulte msdn.microsoft.com/en-us/library/system.guid.aspx . No habría elegido un tipo de referencia para esta pregunta :)
Jon Skeet
1
¿Qué sucede cuando tienes List<Guid>y le agregas esos 3? ¿Serían 3 asignaciones (misma IL)? Pero se mantienen en un lugar mágico
Arec Barrwin
1
@Ani: Te estás perdiendo el hecho de que el ejemplo de Eric tiene un bloque try / catch, por lo que si se produce una excepción durante el constructor de la estructura, debes poder ver el valor antes del constructor. Mi ejemplo no tiene esa situación: si el constructor falla con una excepción, no importa si el valor de guidsolo se ha sobrescrito a medias, ya que de todos modos no será visible.
Jon Skeet
2
@Ani: De hecho, Eric dice esto cerca del final de su publicación: "Ahora, ¿qué pasa con el punto de Wesner? Sí, de hecho, si es una variable local asignada a la pila (y no un campo en un cierre) que se declara al mismo nivel de anidamiento "try" que la llamada del constructor, entonces no pasamos por este rigamarole de hacer un nuevo temporal, inicializar el temporal y copiarlo en el local. En ese caso específico (y común) podemos optimizar la creación de lo temporal y la copia porque es imposible que un programa de C # observe la diferencia ".
Jon Skeet
40

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

Si la estructura está asignada 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 esté 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 establezcan 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 siempre está disponible para estructuras).

Jeffrey L Whitledge
fuente
13

Para decirlo de manera compacta, nuevo es un nombre inapropiado para estructuras, llamar a 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 se define, si es una variable o parámetro local, se almacena en la pila.

Compare esto con las clases, que tienen una referencia donde la estructura se hubiera almacenado en su totalidad, mientras que la referencia apunta a algún lugar del montón. (Miembro interno, local / parámetro en la pila)

Puede ser útil analizar un poco C ++, donde no existe 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 a nuevo, obtiene un puntero a la ubicación del montón, mientras que si tiene una referencia sin puntero, se almacena directamente en la pila o dentro del otro objeto, se estructura en C #.

Guvante
fuente
5

Como con todos los tipos de valor, las estructuras siempre van a donde fueron declaradas .

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

Editar: había respondido por error que SIEMPRE van a la pila. Esto es incorrecto .

Esteban Araya
fuente
"Las estructuras siempre van a donde fueron declaradas", esto es un poco confuso y confuso. Un campo de estructura en una clase siempre se coloca en "memoria dinámica cuando se construye una instancia del tipo" - Jeff Richter. Esto puede estar indirectamente en el montón, pero no es lo mismo que un tipo de referencia normal.
Ash el
No, creo que es exactamente correcto, aunque no es lo mismo que un tipo de referencia. El valor de una variable vive donde se declara. El valor de una variable de tipo de referencia es una referencia, en lugar de los datos reales, eso es todo.
Jon Skeet
En resumen, cada vez que crea (declara) un tipo de valor en cualquier parte de un método, siempre se crea en la pila.
Ash el
2
Jon, echas de menos mi punto. La razón por la que se hizo esta pregunta por primera vez es que no está claro para muchos desarrolladores (incluido yo hasta que leí CLR a través de C #) dónde se asigna una estructura si usa el nuevo operador para crearla. Decir "las estructuras siempre van a donde fueron declaradas" no es una respuesta clara.
Ash el
1
@ Ash: si tengo tiempo, intentaré escribir una respuesta cuando llegue al trabajo. Sin embargo, es un tema demasiado grande para tratar de cubrirlo en el tren :)
Jon Skeet
4

Probablemente me estoy 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 están definidos. Para poder cambiar el valor, debe agregar la palabra clave [ref].

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

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

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

usuario18579
fuente
3
Los tipos de referencia no se pasan por referencia; las referencias se pasan por valor. Eso es muy diferente
Jon Skeet
2

Una declaración classo structes como un plano que se usa para crear instancias u objetos en tiempo de ejecución. Si define una classo structllamada 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 varias instancias del mismo tipo de persona, y cada instancia puede tener valores diferentes en su propertiesy fields.

A classes un tipo de referencia. Cuando un objeto de laclass se crea , 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.

A structes un tipo de valor. Cuando structse crea a, la variable a la que structse asigna contiene los datos reales de la estructura. Cuando structse 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, classesse utilizan para modelar comportamientos más complejos o datos que se pretende modificar después de classcrear un objeto. Structsson más adecuados para pequeñas estructuras de datos que contienen principalmente datos que no están destinados a ser modificados después delstruct creación.

para más...

Sujit
fuente
1

Casi 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.

bashmohandes
fuente
1

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

Estructuras

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

DaveK
fuente
55
Esto no cubre el caso cuando una estructura es parte de una clase, en cuyo punto vive en el montón, con el resto de los datos del objeto.
Jon Skeet
1
Sí, pero en realidad se enfoca y responde a la pregunta que se hace. Votado
Ash el
... sin dejar de ser incorrecto y engañoso. Lo sentimos, pero no hay respuestas cortas a esta pregunta: Jeffrey es la única respuesta completa.
Marc Gravell