c# - con - Arrays, montón y pila y tipos de valores
colas con arreglos en c# (8)
Creo que en el centro de su pregunta se encuentra un malentendido acerca de los tipos de referencia y de valor. Esto es algo que probablemente todos los desarrolladores de .NET y Java tuvieron problemas.
Una matriz es solo una lista de valores. Si se trata de una matriz de un tipo de referencia (digamos una string[]
), la matriz es una lista de referencias a varios objetos de string
en el montón, ya que una referencia es el valor de un tipo de referencia. Internamente, estas referencias se implementan como punteros a una dirección en la memoria. Si desea visualizar esto, tal matriz se vería así en la memoria (en el montón):
[ 00000000, 00000000, 00000000, F8AB56AA ]
Esta es una matriz de string
que contiene 4 referencias a objetos de string
en el montón (los números aquí son hexadecimales). Actualmente, solo la última string
señala algo (la memoria se inicializa a cero cuando se asigna), esta matriz sería básicamente el resultado de este código en C #:
string[] strings = new string[4];
strings[3] = "something"; // the string was allocated at 0xF8AB56AA by the CLR
La matriz anterior estaría en un programa de 32 bits. En un programa de 64 bits, las referencias serían el doble de grandes ( F8AB56AA
sería 00000000F8AB56AA
).
Si tiene una matriz de tipos de valores (por ejemplo, un int[]
), la matriz es una lista de enteros, ya que el valor de un tipo de valor es el valor mismo (de ahí el nombre). La visualización de tal matriz sería esta:
[ 00000000, 45FF32BB, 00000000, 00000000 ]
Esta es una matriz de 4 enteros, donde solo a la segunda int se le asigna un valor (a 1174352571, que es la representación decimal de ese número hexadecimal) y el resto de los enteros sería 0 (como dije, la memoria se inicializa a cero y 00000000 en hexadecimal es 0 en decimal). El código que produjo esta matriz sería:
int[] integers = new int[4];
integers[1] = 1174352571; // integers[1] = 0x45FF32BB would be valid too
Esta matriz int[]
también se almacenaría en el montón.
Como otro ejemplo, la memoria de una matriz short[4]
se vería así:
[ 0000, 0000, 0000, 0000 ]
Como el valor de un short
es un número de 2 bytes.
Donde se almacena un tipo de valor, es solo un detalle de implementación como explica muy bien Eric Lippert, no inherente a las diferencias entre el valor y los tipos de referencia (que es la diferencia en el comportamiento).
Cuando pasa algo a un método (ya sea un tipo de referencia o un tipo de valor), entonces una copia del valor del tipo se pasa al método. En el caso de un tipo de referencia, el valor es una referencia (piense en esto como un puntero a una pieza de memoria, aunque también es un detalle de implementación) y en el caso de un tipo de valor, el valor es la cosa misma.
// Calling this method creates a copy of the *reference* to the string
// and a copy of the int itself, so copies of the *values*
void SomeMethod(string s, int i){}
El boxeo solo ocurre si convierte un tipo de valor a un tipo de referencia. Este código recuadros:
object o = 5;
int[] myIntegers;
myIntegers = new int[100];
En el código anterior, ¿es nuevo int [100] la generación de la matriz en el montón? De lo que he leído en CLR a través de c #, la respuesta es sí. Pero lo que no puedo entender es lo que sucede con las int reales dentro de la matriz. Como son tipos de valor, supongo que tendrían que estar encuadrados, ya que puedo, por ejemplo, pasar myIntegers a otras partes del programa y se llenaría la pila si se dejaran en él todo el tiempo. . ¿O estoy equivocado? Supongo que simplemente estarían en caja y vivirían en el montón por el tiempo que la matriz existió.
No hay boxeo en tu código de ejemplo.
Los tipos de valores pueden vivir en el montón como lo hacen en su conjunto de ints. La matriz se asigna en el montón y almacena las entradas, que son tipos de valores. El contenido de la matriz se inicializa por defecto (int), que resulta ser cero.
Considere una clase que contiene un tipo de valor:
class HasAnInt
{
int i;
}
HasAnInt h = new HasAnInt();
La variable h se refiere a una instancia de HasAnInt que vive en el montón. Solo sucede que contiene un tipo de valor. Eso está perfectamente bien, ''yo'' pasa a vivir en el montón como está contenido en una clase. No hay boxeo en este ejemplo tampoco.
Para entender lo que está sucediendo, aquí hay algunos hechos:
- Los objetos siempre se asignan en el montón.
- El montón solo contiene objetos.
- Los tipos de valores se asignan en la pila o forman parte de un objeto en el montón.
- Una matriz es un objeto.
- Una matriz solo puede contener tipos de valores.
- Una referencia de objeto es un tipo de valor.
Por lo tanto, si tiene una matriz de enteros, la matriz se asigna en el montón y los enteros que contiene son parte del objeto de matriz en el montón. Los enteros residen dentro del objeto de matriz en el montón, no como objetos separados, por lo que no están encasillados.
Si tiene una matriz de cadenas, es realmente una matriz de referencias de cadenas. Como las referencias son tipos de valores, serán parte del objeto de matriz en el montón. Si coloca un objeto de cadena en la matriz, realmente pone la referencia al objeto cadena en la matriz, y la cadena es un objeto separado en la pila.
Sí, la matriz se ubicará en el montón.
Las entradas dentro de la matriz no estarán encuadradas. El hecho de que exista un tipo de valor en el montón, no necesariamente significa que estará encuadrado. El boxeo solo ocurrirá cuando un tipo de valor, como int, se asigne a una referencia de tipo objeto.
Por ejemplo
No caja:
int i = 42;
myIntegers[0] = 42;
Cajas:
object i = 42;
object[] arr = new object[10]; // no boxing here
arr[0] = 42;
También puede consultar la publicación de Eric sobre este tema:
Su matriz está asignada en el montón, y las entradas no están encasilladas.
La fuente de su confusión es probable porque la gente ha dicho que los tipos de referencia se asignan en el montón y los tipos de valores se asignan en la pila. Esta no es una representación completamente precisa.
Todas las variables y parámetros locales se asignan en la pila. Esto incluye tanto tipos de valores como tipos de referencia. La diferencia entre los dos es solo lo que está almacenado en la variable. Como era de esperar, para un tipo de valor, el valor del tipo se almacena directamente en la variable, y para un tipo de referencia, el valor del tipo se almacena en el montón, y una referencia a este valor es lo que se almacena en la variable.
Lo mismo es cierto para los campos. Cuando la memoria se asigna para una instancia de un tipo agregado (una clase o una estructura), debe incluir almacenamiento para cada uno de sus campos de instancia. Para los campos de tipo referencia, este almacenamiento contiene solo una referencia al valor, que se asignaría en el montón más tarde. Para los campos de tipo valor, este almacenamiento contiene el valor real.
Entonces, dados los siguientes tipos:
class RefType{
public int I;
public string S;
public long L;
}
struct ValType{
public int I;
public string S;
public long L;
}
Los valores de cada uno de estos tipos requerirían 16 bytes de memoria (suponiendo un tamaño de palabra de 32 bits). El campo I
en cada caso toma 4 bytes para almacenar su valor, el campo S
toma 4 bytes para almacenar su referencia, y el campo L
toma 8 bytes para almacenar su valor. Entonces la memoria para el valor de RefType
y ValType
ve así:
0 ┌───────────────────┐ │ I │ 4 ├───────────────────┤ │ S │ 8 ├───────────────────┤ │ L │ │ │ 16 └───────────────────┘
Ahora bien, si tuviera tres variables locales en una función, de tipos RefType
, ValType
e int[]
, así:
RefType refType;
ValType valType;
int[] intArray;
entonces tu stack podría verse así:
0 ┌───────────────────┐ │ refType │ 4 ├───────────────────┤ │ valType │ │ │ │ │ │ │ 20 ├───────────────────┤ │ intArray │ 24 └───────────────────┘
Si asignó valores a estas variables locales, así:
refType = new RefType();
refType.I = 100;
refType.S = "refType.S";
refType.L = 0x0123456789ABCDEF;
valType = new ValType();
valType.I = 200;
valType.S = "valType.S";
valType.L = 0x0011223344556677;
intArray = new int[4];
intArray[0] = 300;
intArray[1] = 301;
intArray[2] = 302;
intArray[3] = 303;
Entonces tu stack podría verse más o menos así:
0 ┌───────────────────┐ │ 0x4A963B68 │ -- heap address of `refType` 4 ├───────────────────┤ │ 200 │ -- value of `valType.I` │ 0x4A984C10 │ -- heap address of `valType.S` │ 0x44556677 │ -- low 32-bits of `valType.L` │ 0x00112233 │ -- high 32-bits of `valType.L` 20 ├───────────────────┤ │ 0x4AA4C288 │ -- heap address of `intArray` 24 └───────────────────┘
La memoria en la dirección 0x4A963B68 (value of refType
) sería algo así como:
0 ┌───────────────────┐ │ 100 │ -- value of `refType.I` 4 ├───────────────────┤ │ 0x4A984D88 │ -- heap address of `refType.S` 8 ├───────────────────┤ │ 0x89ABCDEF │ -- low 32-bits of `refType.L` │ 0x01234567 │ -- high 32-bits of `refType.L` 16 └───────────────────┘
La memoria en la dirección 0x4AA4C288 (valor de intArray
) sería algo así como:
0 ┌───────────────────┐ │ 4 │ -- length of array 4 ├───────────────────┤ │ 300 │ -- `intArray[0]` 8 ├───────────────────┤ │ 301 │ -- `intArray[1]` 12 ├───────────────────┤ │ 302 │ -- `intArray[2]` 16 ├───────────────────┤ │ 303 │ -- `intArray[3]` 20 └───────────────────┘
Ahora, si pasó intArray
a otra función, el valor insertado en la pila sería 0x4AA4C288, la dirección de la matriz, no una copia de la matriz.
Suficiente ha sido dicho por todos, pero si alguien está buscando una muestra clara (pero no oficial) y documentación sobre montón, pila, variables locales y variables estáticas, consulte el artículo completo de Jon Skeet sobre la memoria en .NET: qué pasa dónde
Extracto:
Cada variable local (es decir, una declarada en un método) se almacena en la pila. Eso incluye variables de tipo de referencia: la variable en sí está en la pila, pero recuerde que el valor de una variable de tipo de referencia es solo una referencia (o nulo), no el objeto en sí. Los parámetros de método cuentan también como variables locales, pero si se declaran con el modificador de referencia, no obtienen su propia ranura, sino que comparten una ranura con la variable utilizada en el código de llamada. Vea mi artículo sobre el paso de parámetros para más detalles.
Las variables de instancia para un tipo de referencia siempre están en el montón. Ahí es donde el objeto en sí mismo "vive".
Las variables de instancia para un tipo de valor se almacenan en el mismo contexto que la variable que declara el tipo de valor. La ranura de memoria para la instancia efectivamente contiene las ranuras para cada campo dentro de la instancia. Eso significa (dados los dos puntos anteriores) que una variable struct declarada dentro de un método siempre estará en la pila, mientras que una variable struct que es un campo de instancia de una clase estará en el montón.
Cada variable estática se almacena en el montón, independientemente de si está declarada dentro de un tipo de referencia o un tipo de valor. Solo hay un espacio en total sin importar cuántas instancias se creen. (No es necesario que existan instancias creadas para que exista ese espacio). Los detalles de en qué montón exactamente viven las variables son complicados, pero se explican en detalle en un artículo de MSDN sobre el tema.
Una matriz de enteros se asigna en el montón, nada más, nada menos. referencias de myIntegers al comienzo de la sección donde se asignan las entradas. Esa referencia se encuentra en la pila.
Si tiene una matriz de objetos de tipo de referencia, como el tipo de objeto, myObjects [], ubicado en la pila, haría referencia al conjunto de valores que hacen referencia a los objetos por sí mismos.
Para resumir, si pasa myIntegers a algunas funciones, solo pasa la referencia al lugar donde se asigna el grupo real de enteros.