[C# Básico] Paso por referencia

¡Buenas! Este es un nuevo post de la serie C# Básico, que como su propio nombre indica trata sobre aspectos digamos elementales del lenguaje. Cada post es independiente y el orden de publicación no tiene porque ser el de lectura. Los temas los voy sacando de los foros o consultas que se me realizan 🙂

Hoy vamos a tratar un tema que veo que causa mucha confusión: el paso de parámetros por referencia. Como en todos los posts de esta serie lo haremos de forma didáctica y paso a paso.

1. Paso por valor

Para entender que es el paso por referencia, primero es necesario ver que significa el paso por valor. Que salida genera este programa?

static void Main(string[] args)

{

    var inicial = 10;

    Incrementa(inicial);

    Console.WriteLine("Valor DESPUES de incrementar es " + inicial);

}

static void Incrementa(int num)

{

    num = num + 1;

}

El sentido común dice que el programa imprimirá “Valor DESPUES de incrementar es 11”, pero la realidad es otra:

image

¿Como es posible esto? Pues porque la variable incial ha sido pasada por valor. Pasar una variable por valor significa hacer una copia de dicha variable (de ahí el nombre, ya que se pasa el valor y no la variable en sí). De este modo el parámetro num toma el valor de la variable inicial, es decir 10. Pero num es una copia de inicial, así que modificar num, no modifica para nada inicial. Al salir del método, efectivamente num vale 11 pero inicial continua valiendo 10 (además al salir del método la variable num es destruída ya que su alcance es de método).

2. Paso de objetos

Veamos ahora el siguiente código, donde en lugar de pasar un entero, pasamos un objeto de la clase Foo que tiene una propiedad entera:

class Foo

{

    public int Bar { get; set; }

}

 

class Program

{

    static void Main(string[] args)

    {

        var inicial = new Foo();

        inicial.Bar = 10;

        Incrementa(inicial);

        Console.WriteLine("Valor DESPUES de incrementar es " + inicial.Bar);

    }

    static void Incrementa(Foo foo)

    {

        foo.Bar = foo.Bar + 1;

    }

}

¿Cual es la salida del programa ahora? Si nos basamos en lo que vimos en el punto anterior deberíamos responder que va a imprimir “Valor DESPUES de incrementar es 10”, ya que el parámetro foo debería ser una copia de inicial y por lo tanto modificar foo no debe afectar para nada a inicial.

Pero la realidad es otra:

image

Pasar un objeto no crea una copia del objeto. Es por eso que decimos que los objetos no se pasan por valor, se pasan por referencia. Así pues modificar un objeto desde un método modifica el objeto original. No hay manera en C# de pasar una copia entera de un objeto entre métodos (a no ser que se haga manualmente).

Nota: En el código anterior, simplemente modifica “class” por “struct” cuando declaramos Foo. ¿Qué ocurre entonces? Pues que el programa ahora muestra “Valor DESPUES de incrementar es 10”. ¿A que es debido esto? Pues a que las estructuras se pasan por valor (¡es decir se copia su contenido!). Ver el punto (4) para más detalles.

Pero si nos quedamos en este punto obviamos una pregunta muy importante: Efectivamente los objetos se pasan por referencia… ¿pero las propias referencias como se pasan? Pues la respuesta es que las referencias se pasan por valor. Es decir la referencia foo es una copia de la referencia inicial. Pero copiar una referencia no es copiar su contenido (el objeto). Copiar una referencia significa que ahora tenemos dos referencias distintas que apuntan al mismo objeto. Por ello debemos tener muy claro que no es lo mismo modificar el contenido de una referencia (el objeto) que modificar la referencia misma. Si modificamos el contenido (es decir una propiedad del objeto apuntado, en este caso la propiedad Bar), este cambio es compartido ya que ambas referencias apuntan al mismo objeto. Pero si modificamos la referencia este cambio no será compartido:

static void Main(string[] args)

{

    var inicial = new Foo();

    inicial.Bar = 10;

    Incrementa(inicial);

    Console.WriteLine("Valor DESPUES de incrementar es " + inicial.Bar);

}

static void Incrementa(Foo foo)

{

    int valor = foo.Bar;

    foo = new Foo();

    foo.Bar = valor + 1;

}

Este código modifica la referencia foo. No modifica el contenido, modifica la referencia ya que asigna un nuevo objeto a la referencia foo. Al salir del método tenemos:

  1. Una referencia (inicial) que apunta a un objeto
  2. Otra referencia (foo) que apunta a un objeto nuevo
  3. El valor de la propiedad “Bar” del objeto apuntado por inicial es 10.
  4. El valor de la propiedad “Bar” del objeto apuntado por foo es 11.

Al salir del método la referencia foo se pierde, y su contenido (el objeto cuya propiedad Bar vale 11) al no ser apuntado por ninguna otra referencia será destruido por el Garbage Collector. Y efectivamente ahora la salida del programa es:

image

Si entiendes la diferencia entre modificar una referencia y modificar el contenido de una referencia, entonces ya estás listo para el siguiente punto…

3. El paso por referencia

En el punto anterior hemos visto que los objetos se pasan por referencia, pero las propias referencias se pasan por valor. Así que la pregunta obvia es: ¿hay alguna manera de pasar las referencias por referencia?

Y la respuesta es sí: usando la palabra clave ref:

static void Main(string[] args)

{

    var inicial = new Foo();

    inicial.Bar = 10;

    Incrementa(ref inicial);

    Console.WriteLine("Valor DESPUES de incrementar es " + inicial.Bar);

    Console.ReadLine();

}

static void Incrementa(ref Foo foo)

{

    int valor = foo.Bar;

    foo = new Foo();

    foo.Bar = valor + 1;

}

Fíjate que ref debe usarse tanto al declarar el parámetro como al invocar al método. El uso de ref significa que queremos pasar el parámetro por referencia. Si el parámetro es un objeto (como el caso que nos ocupa), ref no significa “pasar el objeto por referencia”, pues eso se hace siempre (como hemos visto en el punto (2)). En este caso ref significa “pasar la referencia por referencia”.

Es por ello que ahora foo y inicial son la misma referencia. Dado que son la misma referencia, forzosamente las dos deben apuntar al mismo objeto. Por ello cuando hacemos foo = new Foo(); estamos modificando la referencia foo haciendo que apunte a otro objeto distinto. Pero si foo y inicial son la misma referencia, al modificar foo modificamos inicial, por lo que ahora al salir del método Incrementa:

  1. La referencia foo apunta a un objeto nuevo cuya propiedad Bar vale 11.
  2. La referencia inicial, dado que es la misma que foo, apunta al mismo objeto.
  3. El antiguo objeto (el que su valor Bar valía 10) al no estar apuntado por ninguna referencia, será destruído por el Garbage Collector.

Por eso, ahora la salida del programa es:

image

4. Paso por referencia de tipos por valor

En terminología de .NET llamamos tipos por valor aquellos tipos que no son pasados por referencia. Así los objetos no son tipos por valor, ya que hemos visto en el punto (2) que se pasan por referencia. Pero p.ej. un int es un tipo por valor, ya que hemos visto en el punto (1) que se pasa por valor.

En general son tipos por valor todos los tipos simples (boolean, int, float,…), los enums, las estructuras (como DateTime). No son tipos por valor los objetos (¡ojo, que eso incluye a string!). Hay una forma sencilla de saber si un tipo es por referencia o por valor: si admite null es por referencia y si no admite tener el valor null es por valor.

Pues bien, ref puede usarse para pasar por referencia un tipo por valor, como p.ej. un int:

static void Main(string[] args)

{

    var inicial = 10;

    Incrementa(ref inicial);

    Console.WriteLine("Valor DESPUES de incrementar es " + inicial);

}

static void Incrementa(ref int valor)

{

    valor = valor + 1;

}

Como ya debes suponer ahora la salida del programa es:

image

En este caso la variable valor no es una copia de la variable inicial. Ambas variables son la misma, por lo que al modificar valor, estamos modificando también inicial. Por ello al salir del método, el valor de inicial sigue siendo 11.

5. Resumen

En resumen, hay cuatro puntos a tener en cuenta:

  1. Los tipos simples, estructuras y enums se pasan por valor (de ahí que digamos que son tipos por valor). Es decir, se pasa una copia de su contenido.
  2. Los objetos se pasan por referencia. Pero la referencia se pasa por valor, es decir el método recibe otra referencia que apunta al mismo objeto.
  3. La palabra clave “ref” permite pasar un parámetro por referencia.
    1. Si este parámetro es un tipo por valor se pasa por referencia, es decir no se pasa una copia sino que se pasa la propia variable.
    2. Si este parámetro es un objeto, lo que se pasa por referencia es la propia referencia.

¡Espero que este post os haya aclarado un poco el tema de paso por valor y paso por referencia! 😉

6 comentarios sobre “[C# Básico] Paso por referencia”

  1. @Soren
    Cierto!
    La diferencia entre ref y out es una muy pequeña diferencia semántica.

    La pregunta es… ¿Por qué existen ambas? 😉 😉 😉

    Gracias por comentar! 😀
    Saludos!

  2. @ecardenas
    ¡Exacto! Aunque la necesidad de que haya «out» para hacer esto y no pueda usarse «ref» es por la obligatoriedad de C# de inicializar las variables antes de usarlas. Esta obligatoriedad se incluyó en el lenguaje porque los diseñadores de C# pensaron que por norma general usar una variable sin antes inicializar su valor es un error.
    Sin esa obligatoriedad el siguiente código podría ser válido:
    int i;
    int.TryParse(«100», ref i);

    Pero NO lo es ya que NO hemos inicializado i. En este caso entre inicializar y no inicializar i no hay apenas diferencia. Pero si el parámetro en lugar de un int fuese una clase cuya inicialización es costosa, es una tontería que nos obliguen a inicializarla antes si luego el método que llamamos la inicializa (llama al constructor) otra vez.
    Así que para esos casos se usa out:
    int i;
    int.TryParse(«100», out i);

    El usar otra palabra clave como out, deja claro que en este caso «no inicializar» la variable i NO es un error, ya que la inicializará el método llamado (TryParse).

    Un saludo!

    1. Más que con la inicializacion tiene que ver con la obligatoriedad de la salida. El método destino debe asignar un valor a los out.

      Saludos.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *