Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

?id=61596355-2c84-4b58-9d5c-6f5d4f3b9f54

Qué son los tipos por valor y por referencia en .NET y C#

Valor-ReferenciaEste es un concepto clave para programar y al mismo tiempo uno de los que más le cuesta entender a los principiantes, así que vamos a intentar explicarlo lo mejor posible a continuación.

Antes de empezar, necesitamos comprender dos conceptos importantes que paso a resumir de manera sencilla:

  • La pila o “stack”: es una zona de memoria reservada para almacenar información de uso inmediato por parte del hilo de ejecución actual del programa. Por ejemplo, cuando se llama a una función se reserva un bloque en la parte superior de esta zona de memoria (de la pila) para almacenar los parámetros y demás variables de ámbito local. Cuando se llama a la siguiente función este espacio se “libera” (en el sentido de que ya no queda reservado) y puede ser utilizado por la nueva función. Es por esto que si hacemos demasiadas llamadas anidadas a funciones (en recursión, por ejemplo) podemos llegar a quedarnos sin espacio en la pila, obteniendo un “stack overflow”.
  • El montón o “heap”: es una zona de memoria reservada para poder asignarla de manera dinámica. Al contrario que en la pila no existen “normas” para poder asignar o desasignar información en el montón, pudiendo almacenar y eliminar datos en cualquier momento, lo cual hace más complicada la gestión de la memoria en esta ubicación.

Los tipos de datos llamados “por valor” son tipos sencillos que almacenan un dato concreto y que se almacenan en la pila. Por ejemplo, los tipos primitivos de .NET como int o bool, las estructuras o las enumeraciones. Se almacenan en la pila y se copian por completo cuando se asignan a una función. Por eso cuando se pasa un tipo primitivo a una función, aunque lo cambiemos dentro de ésta, el cambio no se ve reflejado fuera de la misma.

Los tipos “por referencia” son todos los demás, y en concreto todas las clases de objetos en .NET, así como algunos tipos primitivos que no tienen un tamaño determinado (como las cadenas). Estos tipos de datos se alojan siempre en el montón, por lo que la gestión de la memoria que ocupan es más compleja, y el uso de los datos es menos eficiente (y de menor rendimiento) que con los tipos por valor.

Bien. Ahora que ya conocemos de manera superficial estos conceptos, veamos qué pasa con los tipos por valor y por referencia al usarlos en un programa escrito con C#.

Si pasamos un tipo por valor a una función y lo modificamos dentro de ésta, el valor original permanecerá inalterado al terminar la llamada.

Por ejemplo:

public int Suma2(int n) {
	n = n+2;
	return n;
}

int i = 5;
Console.WriteLine(Suma2(i));
Console.WriteLine(i);
Console.ReadLine();

En este caso definimos una función que simplemente le suma 2 al parámetro que se le pase (de una forma poco eficiente, eso sí) transformando el valor que se le pasa. Se podría pensar que ya que la función cambia el valor que le hemos pasado, cuando mostremos por pantalla posteriormente el valor de i, éste debería ser 7. Sin embargo vemos que, aunque se ha cambiado dentro de la función, sigue siendo 5:

Valor-Referencia-01

Esto es debido a que los números enteros son tipos por valor y por lo tanto se pasa una copia de los mismos a la pila de la función que se llama, no viéndose afectado el valor original.

Sin embargo, si lo que le pasamos a la función es un objeto (por lo tanto, un tipo por referencia), veremos que sí podemos modificar los datos originales.

Por ejemplo, si ahora definimos este código:

public class Persona 
{
	public string Nombre;
	public string Apellidos;
	public int Edad;
}

public static void CambiaNombre(Persona per) {
	per.Nombre = per.Nombre + " CAMBIADO";
}

Persona p = new Persona();
p.Nombre = "Antonio";
p.Apellidos = "López";
p.Edad = 27;

CambiaNombre(p);
Console.WriteLine(p.Nombre);
Console.ReadLine();

Lo que hacemos en el fragmento anterior es definir una clase persona muy sencilla y pasársela a una función que modifica su nombre. Cuando ejecutemos el código se verá por pantalla el nombre de la persona cambiado:

Valor-Referencia-02

Es decir, los cambios son apreciables en el origen, fuera de la propia función.

¿A qué es debido esto?

El motivo es que, como hemos dicho, los tipos por referencia se almacenan siempre en el montón, y lo que se pasa a la función como parámetro no es una copia del dato, como en el caso de los tipos por valor, sino una copia de la referencia al dato.

Esto que suena muy lioso es fácil de entender si lo visualizamos.

Vamos a ver en un gráfico la situación de la pila y el montón en el caso del primer código, donde solo manejábamos un tipo por valor:

Valor-Referencia-03

Como podemos observar, al definir la variable i ésta se almacena en la pila directamente ya que es un tipo por valor. Al llamar a la función Suma2 pasándole i como argumento, lo que ocurre es que el valor de la variable i se copia a la pila y se asigna al parámetro n de la función (repasa el código anterior). De esta forma tenemos dos copias del dato en la pila, por eso al modificar n no se afecta para nada al valor original de i. En el montón, por cierto, no tenemos nada almacenado.

Consideremos ahora el código del segundo ejemplo y veamos qué se genera en la pila y en el montón en este caso:

Valor-Referencia-04

Ahora la cosa cambia bastante. Al declarar una variable de tipo Persona se genera una instancia de la misma en el montón, ya que es un tipo por referencia. Al asignar los valores de las diferentes propiedades, esos datos se almacenan en el montón asociadas a la instancia de la clase Persona que acabamos de crear. Lo que realmente almacena la variable p es una referencia a los datos de esa persona que están en el montón.

Es decir, ahora en la pila no se almacenan los datos en sí, sino tan solo un “puntero” a los mismos. Es una forma indirecta de referirse a ellos, no como antes en los tipos por valor que el dato se almacenaba directamente en la pila.

Al llamar a la función CambiaNombre, en la variable local per que constituye el parámetro de la función, lo que se duplica ahora en la pila es la referencia al objeto, no el objeto en sí. Es decir, al hacer la llamada disponemos de dos variables que apuntan al mismo tiempo a los datos almacenados en el montón. Por eso cuando cambiamos una propiedad del objeto, al estar ambas referencias apuntando a los mismos datos, los cambios se ven desde los dos lados.

En realidad el comportamiento de la pila es idéntico en ambos casos, lo que cambia es la información que se almacena, que es el propio dato en el caso de los tipos por valor, y la referencia al dato en el caso de los tipos por referencia.

Este modo de funcionamiento es la diferencia fundamental que existe entre los tipos por valor y los tipos por referencia, y es muy importante tenerla clara pues tiene muchas implicaciones a la hora de escribir código.

¡Espero que te ayude a entenderlo mejor!

Fecha de publicación:
José Manuel Alarcón Fundador de campusMVP, es ingeniero industrial y especialista en consultoría de empresa. Ha escrito diversos libros, habiendo publicado hasta la fecha cientos de artículos sobre informática e ingeniería en publicaciones especializadas. Microsoft lo ha reconocido como MVP (Most Valuable Professional) en desarrollo web desde el año 2004 hasta la actualidad. Puedes seguirlo en Twitter en @jm_alarcon o leer sus blog técnico o personal. Ver todos los posts de José Manuel Alarcón
Archivado en: Lenguajes y plataformas

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Comentarios (13) -

¡Muchas gracias por este pequeño pero muy útil artículo! Me ayudo bastante a entender las diferencias.

Responder

Gracias por el aporte, muy valioso.

Una consulta que pasa si asigno null a "p", que pasa con el objeto, se borra del montón o que sucede?
p = null;

Responder

José Manuel Alarcón
José Manuel Alarcón

Hola David:

Cuando haces p = null; estás desasociando la variable del objeto, es decir, la variable deja de apuntar a éste. Pero eso no significa que el objeto debe ser descartado. Puede haber otras variables apuntando al mismo objeto. Para saber eso, el runtime mantiene internamente una cuenta de referencias para saber cuántas variables (o propiedades, campos...) están apuntando al objeto. Cuando esta cuenta llega a cero, es decir, el objeto no está referenciado por nadie, pasa a un estado en el que el recolector de basura (www.campusmvp.es/.../...-II-Garbage-collector.aspx) puede eliminarlo cuando necesite memoria, ya que nadie lo está usando. Esto no quiere decir que se elimine. Nadie te lo garantiza, pero a todos los efectos es como si así hubiera sido, ya que no podrás obtener acceso de nuevo al objeto desde tu código.

Saludos.

Responder

Perfecto José, gracias por tomarte el tiempo para responder.
Son muy valiosos tus aportes.

Responder

Erik Omar Montes
Erik Omar Montes

Disculpa y que pasa cuando un tipo por referencia (que es una instancia de una clase) la pasamos por referencia a un metodo con la palabra reservada ref. es decir si tenemos este codigo:

public class Persona
{
  public string Nombre;
  public string Apellidos;
  public int Edad;
}

public static void CambiaNombre(Persona ref  per) {
  per.Nombre = per.Nombre + " CAMBIADO";
}

Persona p = new Persona();
p.Nombre = "Antonio";
p.Apellidos = "López";
p.Edad = 27;

CambiaNombre(p);
Console.WriteLine(p.Nombre);
Console.ReadLine();

Entiendo que en consola veremos como resultado "Antonio CAMBIADO" ya que la variable interna del metodo como la  externa que se pasa apuntan a la misma localidad de memoria. ¿Si estoy en lo correcto? y ¿Como se veria el diagrama del stack y el heap con este codigo usando la palabra ref?

Finalmente, cual es la diferencia de utilizar la palabra ref y no utilizar la palabra ref
Gracias de antemano

Responder

José Manuel Alarcón
José Manuel Alarcón

Hola Erik:

El ejemplo que has puesto te dará el mismo resultado uses ref en el parámetro de la función o no, ya que no estás reasignando el valor de ese parámetro dentro de la función (por cierto, ref va delante del tipo del argumento, no detrás, es decir, debes poner: public static void CambiaNombre(ref Persona  per) y no al revés como lo has puesto tú).

La cosa cambiaría bastante si cambias ligeramente tu función y la conviertes en esto:

public static void CambiaNombre(Persona  per)
{
  per = new Persona();
  per.Nombre = per.Nombre + " CAMBIADO";
}

En este caso hay un cambio importante: estás cambiando el objeto al que apunta el parámetro "per", en este caso asignándole una nueva persona. Dado que la variable apunta a otro objeto diferente y esa referencia se pasa por valor, los cambios que hagas en esta función no se ven fuera de ésta. Es decir, por la consola verás "Antonio" que es el valor original ya que no has afectado para nada al objeto original al haber cambiado el valor del parámetro.

Sin embargo si ahora le añade ref delante al argumento y ejecutas de nuevo el código, entonces sí que verás los cambios realizados ya que la reasignación del objeto que haces en la función se traslada a la variable original que tenías fuera.

Esto así contado suena complicado, así que lo mejor es que lo veas en funcionamiento. He creado dos repl.it para que puedas ver la diferencia.

El primero sin usar ref: https://repl.it/@jmalarcon/PalabraClave-ref-01

Y el segundo usándolo: https://repl.it/@jmalarcon/PalabraClave-ref-02

Mira los comentarios que he dejado y ejecútalo y verás las diferencias más claras.

Todo lo demás se entiende con las explicaciones del artículo.

Saludos!

Responder

José Manuel Alarcón
José Manuel Alarcón

Por lo que he visto los enlaces a repl.it no los ha pillado bien debido a la arroba. Cópialos y pégalos en el navegador para verlos.

Responder

Saludos;

string es una clase.
una clase es un typo por referencia.

usted podria probar el siguiente codigo, y darme una respuesta razonable.

static void ReferenceString(string x)
{
     x = "Hola";
}

public static void Main(string[] args)
{
     string x = default(string);
     ReferenceString(x);

     Console.WriteLine(x);
     Console.Read();
}

Responder

José Manuel Alarcón
José Manuel Alarcón

Hola:

No sé muy bien si entiendo qué me estás preguntando, porque realmente no hay ninguna pregunta, pero supongo que te refieres a que si string es un tipo por referencia, porque es una clase, entonces deberías ver por consola la palabra "Hola" pero realmente se ve un valor nulo ¿es eso?

Bien, en ese caso debes saber que la clase string es un tipo por referencia pero con semántica de tipo por valor. En realidad una cadena de texto (string) es un tipo inmutable. Cada vez que modificas una cadena en realidad estás creando una cadena nueva en memoria y reasignándola (más aquí: www.jasoft.org/.../...-b5ca-4be1-9db3-06e97d3.aspx).

De todos modos en este caso lo que está ocurriendo no tiene nada que ver con esto, ya que cuando escribes esto:

x = "Hola";

lo que estás haciendo es apuntando la variable x a una nueva cadena, pero esto no afecta para nada a dónde está apuntando la variable original, que sigue apuntando a la cadena original.

Es decir, si sustituyes tu código por cualquier otra clase (tipo por referencia), funcionaría igual. Por ejemplo:

----
class Persona
{
   public Persona(string nombre)
   {
       this.Nombre = nombre;
   }
  
   public string Nombre {get; set;}
}

static void ReferencePersona(Persona x)
{
     x = new Persona("Pepe");
}

public static void Main(string[] args)
{
     Persona p = new Persona("Antonio");
     ReferencePersona(p);

     Console.WriteLine(p.Nombre);
     Console.Read();
}
----

En este caso quizá esperarías ver por consola la palabra "Pepe", pero realmente verás "Antonio". El motivo es el mismo: has reasignado a donde apunta la variable x, pero eso no afecta a la variable original (p en este caso), ya que lo que estás pasando a la función como parámetro es una referencia por valor. O sea, lo que pasas es la referencia a un objeto, y esta referencia se pasa por valor. Si la cambias para apuntar a otro objeto, ya no afectas al objeto original. Por el contrario si dejases la referencia sin cambiar y modificases alguna propiedad del objeto, esto sí se vería reflejado en el objeto original, lógicamente. Es lo que se explica en el post.

Puede resultar lioso, pero está muy claro si te paras a pensarlo. Y en este ejemplo que me pones da igual que sea una cadena que otro objeto: el comportamiento es el mismo. En el caso de las cadenas se está apuntando a una nueva cadena, y en el caso de un objeto de otra clase (por ejemplo la clase Persona que he puesto), pues se apunta a un objeto diferente.

Saludos.

Responder

Hola,
Todo muy bien explicado pero.... si yo paso dos Integer a un método para que intercambie su contenido, por qué no los intercambia, los deja como estaba si dices que los objetos son por referencia y no por valor?

Responder

José Manuel Alarcón
José Manuel Alarcón

Hola Carlos:

Los enteros, como digo en uno de los primeros párrafos, así como muchos de los tipos fundamentales, son tipos por valor, no por referencia. Por eso funciona así. Te recomiendo que leas de nuevo toda la explicación y creo que verás por qué ocurre lo que dices.

Saludos.

Responder

Excelente publicación, me quedó muy claro todo. Gracias!!

Responder

Mario Figueroa
Mario Figueroa

Muchas gracias por el post. Esto mismo lo vi en otras páginas, pero, o muy enredado o incompleto... por fin me quedó claro. Gracias una vez más.

Responder

Pingbacks and trackbacks (1)+

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.