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 pila?
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 pila?
Respuestas:
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 new
operador.
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 new
operador 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 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
: 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 new
llamadas, 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 new
asignar 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 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.
EDITAR: para ser claros, esto solo es cierto en algunos casos ... en particular, el valor de guid
no será visible si el Guid
constructor 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!
List<Guid>
y le agregas esos 3? ¿Serían 3 asignaciones (misma IL)? Pero se mantienen en un lugar mágico
guid
solo se ha sobrescrito a medias, ya que de todos modos no será visible.
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).
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 #.
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 .
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]
Una declaración class
o struct
es como un plano que se usa 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 varias instancias del mismo tipo de persona, y cada instancia puede tener valores diferentes en su properties
y fields
.
A class
es 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 struct
es un tipo de valor. Cuando struct
se crea a, la variable a la que struct
se asigna contiene los datos reales de la estructura. Cuando 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, classes
se utilizan para modelar comportamientos más complejos o datos que se pretende modificar después de class
crear un objeto. Structs
son 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.
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.
Las estructuras se asignan a la pila. Aquí hay una explicación útil:
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.