Etiquetas:
C#,
heap,
memoria,
referencia,
stack,
tipos
Parte I | Parte III | Parte IV
A pesar de que con el marco. NET no tenemos que preocuparnos activamente
sobre la gestión de memoria y recolección de basura (GC), todavía tenemos
que mantener la gestión de memoria y GC en cuenta a fin de optimizar el
rendimiento de nuestras aplicaciones. Además, tener una comprensión básica
de cómo funciona la gestión de memoria ayudará a explicar el comportamiento
de las variables con las que trabajamos en todos los programas que escribimos.
Aquí cubriremos algunos de los comportamientos que tenemos que tener en
cuenta al pasar parámetros a los métodos.
En la Parte I hemos cubierto los fundamentos del Heap y la funcionalidad del
Stack y donde los Tipos Variables y los Tipos Referencia son asignados cuando
nuestro programa se ejecuta. También se cubrió la idea básica de lo que es un
puntero.
Parámetros.
Aquí está la vista detallada de lo que sucede cuando nuestro código se ejecuta.
Hemos cubierto las bases de lo que ocurre cuando hacemos una llamada al
método en la Parte I. Ahora, vamos a entrar en más detalles ...
Cuando hacemos una llamada a un método, esto es lo que sucede:
1. Se asigna espacio en el Stack para guardar la información necesaria para la
ejecución de nuestro método (llamado de Stack Frame). Esto incluye la
dirección de llamada a (un puntero), que es básicamente una instrucción GOTO
de modo que cuando el hilo termine su ejecución, nuestro método tenga
conocimiento de dónde debe ir a fin de continuar la ejecución.
2. Los parámetros del método son copiados. Aquí nos fijaremos con más atención.
3. El control se pasa al método que se ha compilado JIT y el hilo inicia la
ejecución del código. Por lo tanto, tenemos otro método representado por una
Stack Frame en la "llamada al Stack".
El código
public int SumaDos(int punteroValor)
{
int resultado;
resultado = punteroValor + 2;
return resultado;
}
Hará que el Stack tenga este aspecto:
NOTA: el método no existe en el Stack, y se presenta aquí sólo como referencia
al inicio del Stack Frame.
Como se discutió en la Parte I, la colocación de parámetros en el Stack se
tratan de manera diferente dependiendo de si los parámetros son de un Tipo
Valor o un Tipo Referencia.
Pasando Tipos Valor
Aquí está el truco con Tipos Valor ...
En primer lugar, cuando estamos pasando un Tipo Valor, se asigna el espacio
y el valor de nuestro tipo y a continuación se copia en el nuevo espacio en
el Stack. Observe el siguiente método:
class Clase1
{
public void Go()
{
int x = 3;
SumaTres(x);
Console.WriteLine(x.ToString());
}
public int SumaTres(int punteroValor)
{
punteroValue += 3;
return punteroValor;
}
}
A medida que el método se ejecuta, el espacio para "x" se coloca en el Stack
con un valor de 3.
A continuación, SumaTres() se coloca en el Stack con espacio para sus parámetros y el valor se copia, bit a bit de x.
Cuando SumaTres() ha terminado la ejecución, el hilo se pasa de nuevo a Go()
y porque SumaTres() se ha completado, punteroValor es esencialmente "retirado":
Así que es lógico que la salida de nuestro código sea "3", ¿no? El punto es
que cualquier parámetro pasado a un método de Tipo Valor es una copia al
carbón y continuamos contando con el valor de la variable original el cual
aún se conserva.
Una cosa a tener en cuenta es que si tenemos un Tipo Valor muy grande (como
una gran estructura) y queremos pasarlo al Stack, lo cual puede ser muy
costoso en términos de espacio y los ciclos de procesador necesarios para
efectuar la copia. El Stack no tiene un espacio infinito y al igual que
cuando llenamos un vaso con agua del grifo, se puede desbordar. La estructura
esta formada por Tipos Valor que pueden ser bastante grande y tenemos que
ser conscientes de cómo la estamos manejando.
Aquí tenemos una estructura bastante grande
public struct MiEstructura
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
Observe a continuación lo que sucede cuando ejecutamos Go() y llegamos al
método HacerAlgo():
public void Go()
{
MiEstructura x = new MiEstructura();
HacerAlgo(x);
}
public void HacerAlgo(MiEstructura punteroValor)
{
// HACER ALGO AQUI....
}
Esto puede ser proceso ineficiente. Imagine que pasamos MiEstructura un par
de miles de veces.
¿Cómo podemos solucionar este problema? Con el recurso de una referencia al
Tipo Valor original de la siguiente manera:
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(ref x);
}
public struct MiEstructura
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
public void HacerAlgo(ref MiEstructura punteroValor)
{
// HACER ALGO AQUI....
}
De esta manera nos encontramos con una asignación más eficiente de los
objetos en la memoria.
Lo único que tenemos que tener en cuenta al pasar nuestro Tipo Valor por referencia es que tenemos acceso al valor el Tipo Valor. Aquello que cambiamos en punteroValor se cambia en x. Usando el código siguiente, nuestros resultados van a ser "12345", porque la realidad es que punteroValor esta apuntando para el espacio de memoria donde se declaró originalmente nuestra variable x.
public void Go()
{
MiEstructura x = new MiEstructura();
x.a = 3;
HacerAlgo(ref x);
Console.WriteLine(x.a.ToString());
}
public void HacerAlgo(ref MiEstructura punteroValor)
{
punteroValor.a = 12345;
}
Pasar Tipos Referencia
Los parámetros Tipos Referencia se pasan de forma es similar a los Tipos Valor
por referencia como en el ejemplo anterior
Si utilizamos el Tipo Valor
public class MiEntero
{
public int MiValor;
}
Y llamamos al método Go(), MiEntero acaba por localizarse en el Heap, porque
es un Tipo Referencia:
public void Go()
{
MiEntero x = new MiEntero();
}
Si ejecutamos Go() con el siguiente código...
public void Go()
{
MiEntero x = new MiEntero();
x.MiValor = 3;
HacerAlgo(x);
Console.WriteLine(x.MiValor.ToString());
}
public void HacerAlgo(MiEntero punteroValor)
{
punteroValor.MiValor = 12345;
}
Esto es lo que ocurre...
1. A partir de la llamada al método Go() la variable x va al Stack.
2. Al iniciar la llamada al método HacerAlgo() el parámetro punteroValor va en la
Stack.
3. El valor de x (la dirección de MiEntero en el Stack) se copia en punteroValor
Así que es lógico que cuando cambie la propiedad MiValor del objeto MiEntero en el Heap con punteroValor por esta razón se obtiene entonces el valor "12345" al referir el objeto en el Heap a través de x.
Así que, aquí es donde se pone interesante el asunto.
¿Qué sucede cuando se pasa un Tipo Referencia por referencia?
Si tenemos una Clase Cosas con Animal y Vegetal representando ambos a cosas:
public class Cosa
{
}
public class Animal:Cosa
{
public int Peso;
}
public class Vegetal:Cosa
{
public int Largo;
}
y ejecutamos el siguiente método Go()
public void Go()
{
Cosa x = new Animal();
Cambia(ref x);
Console.WriteLine(
"x is Animal : "
+ (x is Animal).ToString());
Console.WriteLine(
"x is Vegetal : "
+ (x is Vegetal).ToString());
}
public void Cambia(ref Cosa punteroValor)
{
punteroValor = new Vegetal();
}
Convertimos la variable x en un Vegetal
x is Animal : false
x is Vegetal : true
Veamos que es lo que ocurre:
1. Empezamos con la llamada al método Go(), el puntero x es colocado en
el Stack
2. El objeto Animal es colocado en el Heap
3. Iniciamos la llamada al método Cambia(), colocamos el punteroValor en
el Stack el cual apunta para x.
4. El objeto Vegetal es colocado en el Heap
5. El valor de x se cambia a través de punteroValor de Animal para Vegetal
Si no pasamos la Cosa por referencia, vamos a mantener al objeto animal y
obtener los resultados opuestos de lo que tenemos
Conclusión
Hemos visto cómo se maneja la transmisión de parámetros en la memoria y
ahora sabemos qué atenernos. En la siguiente parte de esta serie, vamos a
observar lo que ocurre con las variables de referencia que viven en el
Stack y la manera de superar algunas cuestiones que pueden ocurrir al
momento de efectuar copias de objetos.
Hasta la próxima parte de esta serie...