2008-10-15 9 views

Respuesta

283

Bien, veamos si puedo aclarar esto.

En primer lugar, Ash es correcto: la cuestión es no acerca de dónde tipo de valor las variables se asignan. Esa es una pregunta diferente, y una a la cual la respuesta no es solo "en la pila". Es más complicado que eso (y aún más complicado por C# 2). Tengo un article on the topic y ampliaré si así lo solicita, pero vamos a tratar solo con el operador new.

En segundo lugar, todo esto realmente depende del nivel de lo que está hablando.Estoy mirando lo que el compilador hace con el código fuente, en términos de 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, 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 operador new en los tipos de valor: puede llamar a un constructor sin parámetros (por ejemplo new Guid()) o un constructor parameterful (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. De acuerdo con la especificación CLI, no los tipos de valor tienen 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 que C# trate el "inicializar un valor con ceros" como un constructor, porque mantiene el lenguaje consistente - puede pensar en 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 la diferencia qué va a hacer con el valor después de haberlo inicializado. La IL utiliza para

Guid localVariable = new Guid(someString); 

es diferente a la IL utilizado 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 son ligeramente diferentes nuevamente. Para mostrar todas estas diferencias, aquí hay un breve programa de prueba. No muestra la diferencia entre las variables estáticas y las variables de instancia: el 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á la IL para la clase, excluidos los bits irrelevantes (como NOP):

.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 se puede ver, hay un montón de diferentes instrucciones utilizadas para llamar al constructor:

  • newobj: Asigna el valor en la pila, llama a un constructor con parámetros. Usado para valores intermedios, p. para la asignación a un campo o 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 usando varias llamadas new, solo inicializa los datos sobre el 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, una variable local intermedia se introduce efectivamente y su valor 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 pila, pero como hemos visto, eso no es lo que realmente ocurre incluso en el nivel IL. Me gustaría destacar un caso en particular. Tome este método:

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

que "lógicamente" ha 4 asignaciones de pila - uno para la variable, y uno para cada uno de los tres new llamadas - pero de hecho (para ese código específico) la pila es única asignada una vez , y luego se reutiliza la misma ubicación de almacenamiento.

EDITAR: Para que quede claro, esto solo es cierto en algunos casos ... en particular, el valor de guid no estará visible si el constructor Guid arroja una excepción, por lo que el compilador C# puede reutiliza la misma ranura de pila Vea el blog post on value type construction de Eric Lippert para más detalles y un caso donde no aplica.

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

+0

Buena respuesta Jon, la aclaración de la complejidad de esta área es muy importante. Debo saber que a estas alturas rara vez hay una respuesta en blanco y negro para la mayoría de las preguntas de desarrollo de software. – Ash

+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 entonces abordaría directamente la pregunta original de @ kedar. – Ash

+6

Guid ya es una estructura. Consulte http://msdn.microsoft.com/en-us/library/system.guid.aspx No habría elegido un tipo de referencia para esta pregunta :) –

4

Como en todos los tipos de valores, las estructuras siempre van donde estaban declaradas.

Consulte esta pregunta here para obtener más información sobre cuándo utilizar las estructuras. Y esta pregunta here para obtener más información sobre las estructuras.

Editar: tuve mistankely respondieron que SIEMPRE van en la pila. Esto es incorrect.

+0

"estructuras siempre van donde fueron declarados", esto es un poco engañoso 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 ser indirectamente en el montón, pero no es lo mismo que un tipo de referencia normal en absoluto. – Ash

+0

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

+0

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

0

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

Structs

Además, las clases cuando se crea una instancia dentro de .NET asignar memoria en el montón o 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 pila. Además, debe tenerse en cuenta que los parámetros de paso dentro de las estructuras se realizan por valor.

+5

Esto no cubre el caso cuando una estructura es parte de una clase, en ese punto vive en el montón, con el resto de los datos del objeto. –

+1

Sí, pero en realidad se centra y responde la pregunta que se hace. Votado arriba. – Ash

+0

... siendo incorrecto y engañoso. Lo sentimos, pero no hay respuestas cortas a esta pregunta: Jeffrey's es la única respuesta completa. –

32

La memoria que contiene los campos de una estructura se puede asignar a la pila o al montón, según 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 pila. 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 las estructuras asignadas en la pila, excepto que C# requiere que todas las variables locales se establezcan en algún valor antes de su uso, por lo que debe llamar un constructor personalizado o el constructor predeterminado (un constructor que no toma parámetros siempre está disponible para las estructuras).

1

Casi todas las estructuras que se consideran tipos de valores, 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.

8

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

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

Contraste esto con las clases, que tienen una referencia donde 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 pila)

Puede ser útil 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 pila o dentro del otro objeto, ala construye en C#.

3

Probablemente me falta algo aquí, pero ¿por qué nos preocupa 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 Array/inicialización: tipos de valor -> memoria cero [nombre, código postal] [nombre, zip] Tipos de referencia -> memoria cero -> null [ref] [ref]

+2

Los tipos de referencia no se pasan por referencia; las referencias se pasan por valor. Eso es muy diferente. –

2

Un class o struct La declaración es como un modelo que se usa para crear instancias u objetos en tiempo de ejecución. Si define un class o struct llamado Persona, Persona 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 valores diferentes en su properties y fields.

A class es un tipo de referencia. Cuando se crea un objeto del 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.

A struct es un tipo de valor. Cuando se crea un struct, la variable a la que está asignado el struct contiene los datos reales de la estructura. Cuando el 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, classes se utilizan para modelar comportamientos más complejos o datos que se pretende modificar después de crear un objeto class. Structs son los más adecuados para estructuras de datos pequeñas que contienen principalmente datos que no se pretende modificar después de crear el struct.

for more...

Cuestiones relacionadas