Hoy vamos a tratar, por medio de un ejemplo práctico, de distinguir entre dos sencillos conceptos del lenguaje C# que en ocasiones llevan a confusión: los tipos por referencia y el paso por referencia. Separar estos conceptos claramente puede ahorrarnos sutiles errores de programación.

Los tipos de C# se pueden clasificar en dos:

  • Tipos por valor: Contienen directamente los datos. Una asignación a una variable de este tipo implica sobrescribir el valor existente previamente. Los tipos valor que incluye el lenguaje C# son heredados de enum y struct, incluyendo tipos numéricos básicos, booleano, coma flotante, etc.
  • Tipos por referencia: No almacenan datos, sino referencias al verdadero lugar de la memoria donde está almacenados los datos. Dos variables de este tipo pueden hacer referencia al mismo dato y, modificarlo de forma independiente. C# considera tipos de referencia a los que heredan de class, interface o delegate y a su vez proporciona los tipos integrados object, string y dynamic.

Llevando esto a la práctica:

class ReferencedObject
{
    public string Value { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var firstReference = new ReferencedObject();
        firstReference.Value = "first";

        var secondReference = firstReference;
        secondReference.Value = "second";
        
        Console.WriteLine($"First reference: {firstReference.Value}");
        Console.WriteLine($"Second reference: {secondReference.Value}");

        Console.ReadKey();
    }
}

Comprobamos en la salida que una modificación en la segunda referencia modifica el valor apuntado por ambas:

First reference: second
Second reference: second

Haciendo lo mismo modificando la referencia desde un método vemos que el resultado es idéntico:

class Program
{
    static void Main(string[] args)
    {
        var firstReference = new ReferencedObject();
        firstReference.Value = "first";

        var secondReference = firstReference;
        AlterReference(secondReference);

        Console.WriteLine($"First reference: {firstReference.Value}");
        Console.WriteLine($"Second reference: {secondReference.Value}");

        Console.ReadKey();
    }

    static void AlterReference(ReferencedObject reference) 
    {
        reference.Value = "second";
    }
}

Veamos qué pasa con un string, que también es un tipo por referencia

class Program
{
    static void Main(string[] args)
    {
        var firstReference = "first";

        var secondReference = firstReference;
        AlterReference(secondReference);

        Console.WriteLine($"First reference: {firstReference}");
        Console.WriteLine($"Second reference: {secondReference}");

        Console.ReadKey();
    }

    static void AlterReference(string reference)
    {
        reference = "second";
    }
}

La salida es:

First reference: first
Second reference: first

¿Qué ha pasado? Aquí es donde debemos introducir los conceptos de paso por valor y paso por referencia. En C# todos los parámetros se pasan por valor a no ser que se indique por medio del modificador ref, o in en caso de parámetros de entrada y out en caso de parámetros de salida. Paso por valor indica que se copia el dato pasado. En el caso de un tipo valor, como por ejemplo un entero, esto implica que al método le llega un parámetro con el mismo valor de ese entero, pero que no es la variable original, por lo que cualquier modificación que sufra el parámetro dentro del método no repercutirá en el original. Podemos comprobarlo de forma sencilla:

class Program
{
    static void Main(string[] args)
    {
        int value = 1;
        
        AlterValue(value);

        Console.WriteLine($"Value: {value}");

        Console.ReadKey();
    }

    static void AlterValue(int value)
    {
        value = 2;
    }
}

Comprobamos, que la modificación realizada en el método no se refleja en la variable original

Value: 1

En el caso de tipos por referencia, su paso por valor no implica la copia del objeto entero, sino una copia de su referencia. Hay que tener en cuenta que si asignamos el valor de una de estas referencias a otro objeto distinto, dejará de estar referenciando al objeto original. Por ejemplo:

class Program
{
    static void Main(string[] args)
    {
        var firstReference = new ReferencedObject();
        firstReference.Value = "first";

        var secondReference = firstReference;
        AlterReference(secondReference);

        Console.WriteLine($"First reference: {firstReference.Value}");
        Console.WriteLine($"Second reference: {secondReference.Value}");

        Console.ReadKey();
    }

    static void AlterReference(ReferencedObject reference)
    {
        // Nueva referencia
        reference = new ReferencedObject();
        reference.Value = "second";
    }
}

Esto es exactamente lo que nos ha pasado en el ejemplo anterior con la cadena de texto, solo que es la propia naturaleza de la clase string la que ha impedido verlo de forma clara. El objeto string, es por definición inmutable, es decir, que a la hora de modificar una variable de este tipo lo que se hace es sustituir su valor por uno nuevo, lo que implica que se crea una referencia a un texto que ya no es el original.

Si queremos que nuestro método modifique la variable original deberemos pasarle el modificador ref, esto hace que el argumento se pase por referencia con lo que el método manejará la referencia original, y no una copia, por lo que la asignación del nuevo texto se verá reflejada sobre la variable que invocó dicho método.

class Program
{
    static void Main(string[] args)
    {
        var firstReference = "first";

        // Nueva referencia
        var secondReference = firstReference;
        AlterReference(secondReference);

        Console.WriteLine($"First reference: {firstReference}");
        Console.WriteLine($"Second reference: {secondReference}");

        Console.ReadKey();
    }

    static void AlterReference(ref string reference)
    {
        reference = "second";
    }
}

En los ejemplos anteriores, los argumentos eran de tipo por referencia (un tipo de variable) pero estaban pasados por valor (un tipo de paso de parámetros).