Usando la palabra clave “ref”

No hace mucho, estabamos tomando unas cervezas algunos desarrolladores de .net de Barcelona. Entonces salió el tema de los talibanes del código. Cosas como fxCop o styleCop. Aunque estas herramientas y sus reglas dan para varios artículos de debate, el caso es que surgió una norma del code analysis (fxCop) que recomienda no usar la palabra en clave "ref".

En ese momento varios afirmamos que estaba bien porque no era necesario, al menos con objetos. Entonces uno de los comensales, Jaume Jornet (@jaumejornet) rapidamente respondió con un "es un doble-asterísco de c". A partir de ahí ya le habíamos comprado la respuesta, pero puso un ejemplo rápido usando un dibujo del heap y otro del stack, que para las personas que siempre han usado lenguajes manejados puede resultar muy útil.

Como es un tema muy interesante, vamos a intentar ampliar la explicación que nos dió de Jaume y comentar el uso de la palabra en clave ref.

Antes de empezar vamos a explicar vagamente qué es la stack y qué el heap:

Esta descripción quizá no sea la más exacta y tampoco la que más aclara. Así que vamos a ver un ejemplo:

public class Person
{
   public string Name { get; set; }
   public int Age { get; set; }
}
int a = 3;
long b = 4;
var c = new Person();

En el momento de ejecutar ese código, quedaría así almacenado (en azul stack y en rojo heap):

Para los que no vienen de lenguajes como c o c++, cuando vemos el símbolo asterisco delante del nombre de una variable significa que es donde se almacena la dirección de memoria (el lugar en el heap) del objeto. Pero el objeto verdaderamente no está ahí. Solo es una referencia a la zona del heap donde está almacenado. Un puntero.

Ahora declaramos una función de esta forma:

public void Method1(int i, Person p)
{
   i = 12;
   p.Name = "Fernando";
}

Method1(a, c);

Esta función cuando es llamada genera dos nuevas entradas en la stack, una de tipo entero (int) con el valor de 'a' copiado y otra de tipo puntero a persona con la misma dirección de memoria que la anterior:

Entonces vemos que si dentro de la función cambiamos el valor del entero, este es una copia y se cambia en la pila (stack). Pero si cambiamos el valor del nombre de la persona, aunque el puntero sea una copia, referencia la misma dirección de memoria que la variable 'c'.

Por esta razón, cuando termine la ejecución del método, la variable 'a' seguirá valiendo 3, pero el nombre de 'c' será "Fernando". Y de aquí viene ese dicho que reza: que en c# (y otros lenguajes manejados) los objetos se pasan siempre por referencia. Porque lo cierto es que se copia la referencia (o dirección en el heap) de ese objeto en la pila.

Si queremos cambar el valor del entero, tendremos que pasarlo por referencia, usando la palabra en clave ref:

public void Method2(ref int i, Person p)
{
   i = 12;
   p.Name = "Fernando";
}

Method2(ref a, c);

Al escribir la palabra ref, la variable que se crea en la pila, en lugar de ser un entero, será una referencia (un puntero) a la dirección de la pila donde se encuentra el entero original. Y su ejecución causaría esto:

Como antes hemos dicho que el objeto 'p' ya se pasaba por referencia sin necesitad del comando ref, ¿qué pasaría si lo reasignamos a un nuevo valor?

public void Method3(ref int i, Person p)
{
   i = 12;
   p.Name = "fernando";
   p = new Person();
   p.Name = "pablo";
}

Method3(ref a, c);

En este contexto el objeto 'p' es una copia de la dirección de memoria del objeto 'c' original. Y al asignarle una nueva instancia, se cambia la dirección de memoria y genera un nuevo bloque en el heap. Pero el objeto 'c' permanece apuntando al original:

Así pues llegamos al último punto, ¿qué pasaría si quisieramos cambiar la instancia original? Entonces tendríamos que crear el famoso doble-puntero. O lo que es lo mismo, una referencia del objeto Person:

public void Method4(int i, ref Person p)
{
   p = new Person();
   p.Name = "pablo";
}

Method4(a, ref c);

Así al salir del método cuarto, el valor de 'c' sería la dirección de memoria del nuevo objeto person con nombre "pablo":

Ahora os invito a que hagaís estas pruebas y observeís por vosotros mismos este comportamiento.