Tipos por VALOR y REFERENCIA en C#

02 Aug 2021 10 min (0) Comentarios

En este post vamos a ver la diferencia entre tipos por valor y tipos por referencia, además mostraremos como C# trabaja con ambos tipos de datos y las opciones que tenemos. 

 

 

1 - Diferencia entre tipos por valor y tipos por referencia

Para determinar cuándo debemos crear o utilizar un struct o una clase o incluso un record debemos saber cuales son sus características, limitaciones y cómo funcionan en términos de memoria.

 

1.1 - Tipos por valor

Cuando utilizamos tipos por valor en C# nos estamos refiriendo a los structs

 

Pero qué quiere decir tipo por valor?

Quiere decir que cuando utilizamos ese objeto, estamos utilizando el objeto como tal, leyendo y escribiendo en el stack (veremos lo que es el stack más adelante) y cuando escribimos un objeto en el stack lo hacemos de forma completa, no un puntero como hacemos en los tipos por referencia. 

tipos por valor

El caso más común de un struct es cuando representa un único valor, como puede ser un tipo primitivo (double, int, decimal, etc). Además, este debe ser menor de 16kb.

 

Cuando utilizamos la operación de asignación (=) a un struct lo que estamos haciendo es una copia del valor en una nueva variable, no asignamos la referencia del mismo. Por lo que, si modificamos uno de los objetos el valor del otro no cambia:

int ejemplo1 = 15; // asignamos un valor inicial
int ejemplo2 = ejemplo1; // asignamos ejempo2 con el valor de ejemplo1
ejemplo2 = 10; // modificamos el valor de ejemplo 2
Console.WriteLine(ejemplo1); // imprime 15;
Console.WriteLine(ejemplo2) // imprime 10;

Esto es importante ya que en los tipos por referencia no funciona de la misma manera. 

 

 

1.2 - Tipos por referencia

Cuando utilizamos tipos por referencia en C# nos estamos refiriendo a las clases, osea class y dese C# 9 también a los records. Y todas las instancias de las clases están ubicadas en el heap.

Finalmente la variable en sí es un puntero a ese objeto en el heap, no el objeto como tal.

tipos por referencia

¿Qué quiere decir que las clases son punteros a memoria? 

Imagínate que tenemos una clase llamada vehículo que contiene propiedades como la marca, el modelo y el número de puertas. 

Cuando hacemos una asignación como la siguiente: 

Vehiculo vehiculo1 = new Vehiculo(“Opel”, “Astra”, 4);

Estamos creando vehículo1 en el heap y asignando la posición de memoria a su valor;

Por lo que si creamos un vehiculo2 y le asignamos el valor de vehiculo1 lo que estaremos haciendo en verdad es asignar a esa variable el puntero a la misma posición de memoria.

Vehiculo vehiculo1 = new Vehiculo("Opel", "Astra", 4); //Creamos un vehículo
Vehiculo vehiculo2 = vehiculo1; //asignamos el vehiculo2 con el valor de vehiculo1
vehiculo2.Model = "Vectra";
Console.WriteLine(vehiculo1.Model); // imprime vectra;
Console.WriteLine(vehiculo2.Model) // imprime vectra;

Y como vemos en el ejemplo, cuando cambiamos el valor de uno de los objetos, “ambos” se actualizan.

 

Toda esta acción de utilizar punteros, a diferencia de en otros lenguajes como c, C# lo hace de forma automática por detrás. 

 

El motivo principal por el que tenemos que realizar un montón de comprobaciones en C# para ver si nuestros objetos son null es por este motivo, porque la variable en realidad es un puntero, y no el valor como tal.

Nota: el puntero añade otros 8 bytes de memoria a lo que pesa el objeto (en un programa de 64 bits) y otros 16 bytes por objeto son añadidos  para el uso interno de C# como puede ser el recolector de basura (garbage collector).

 

Desde C# 9 disponemos de la opción de records. Los cuales principalmente los utilizaremos para crear tipos por referencia inmutables

 

 

2 - Diferencia entre Heap y Stack

Cuando creamos un objeto en el código, ocupa espacio en la memoria, y para ello tenemos dos opciones, el heap y el stack, aqui vamos a ver en que se diferencian y como trabajan. 

 

2.1 - Qué es el stack?

El stack es un área de memoria contigua, la cual se asigna de la posición menor de memoria a la posición mayor, en orden. Y cuando queremos liberarla, lo hacemos de la mayor posición a la menor. 

 

Esto quiere decir que para liberar una posición en el medio, debemos liberar todo lo ubicado en una posición mayor.

representación del stack

 

Para saber si un punto de la memoria está asignado, utilizamos un puntero que apunta a una posición de la memoria, y cuando deasignamos memoria lo que hacemos es mover el puntero una posición para abajo, no limpiamos el espacio de memoria, simplemente la siguiente vez que asignemos un valor sobreescribirá el valor superior. 

funcionamiento del stack

 

Como vemos en la imagen al designar el valor lo que hacemos es mover el puntero a la posición con el “valor 1” pero la posición superior sigue manteniendo el valor que contenía. 

 

Una vez asignamos un nuevo valor, en este caso “valor A” lo sobreescribimos en la posición superior. 

 

Uno de los grandes beneficios de utilizar asignación con stack es que es muy eficiente y funciona muy bien por ejemplo en funciones locales. Cuando defines una función en tu código todas las variables van al stack y al salir de la función las limpia.

 

Como nota final, en C# Tenemos un límite de 16 bytes para los structs (van al stack) esto es porque al ser pasado por valor tenemos que pasar todo el elemento, y con 16 bytes se puede hacer con un par de instrucciones del procesador, si fuera mayor de 16 perderias las ventajas que trae el stack debido al rendimiento de copiar el elemento en sí. 

 

 

2.2 - Qué es el heap?

Como hemos visto, el stack tiene algunas restricciones, por lo que no siempre nos vale, aquí es donde el heap entra en juego. 

 

Heap es la memoria que utilizamos para la asignación dinámica de memoria

Y como su nombre indica, asignamos (y desasignamos) la memoria de forma desorganizada, lo cual puede provocar fragmentación.

uso del stack

A qué me refiero con fragmentación?

Como vemos tenemos varias franjas de memorias asignadas (colores azul, naranja, verde, morado) y varias franjas libres (blanco), si quisiéramos almacenar un objeto que ocupe 3 franjas no podríamos, ya que no tenemos 3 franjas contiguas libres. 

 

Por norma general podemos utilizar el stack para todos aquellos objetos cuyo uso vaya a ir más allá de una función o un proceso.

 

Finalmente, hemos mencionado que el stack se limpia solo al salir de la función, en el heap no, es el desarrollador el que tiene que limpiar el heap. Por suerte C#  sabe, gracias a los 16 bytes extra que almacena por objeto, cuando un objeto en memoria ya no se va a utilizar más y pasa el recolector de basura (garbage collector) para limpiar esa memoria que está asignada pero que no se va a volver a utilizar. 

 

 

3 - Cuándo utilizar struct, class o record

Ahora llega la gran pregunta, con toda esta información, cuando estamos desarrollando código, qué tipo de dato (data type) debemos utilizar.

 

3.1 - Cuándo utilizar struct

Podemos crear nuestro tipo como un struct si cumple las siguientes caracteríticas (todas): 

  1. La instancia es pequeña (16 bytes)  y su periodo de vida es comúnmente corto, por ejemplo únicamente lo utilizamos dentro de una función.
    • También es común si va a ser parte de otro objeto y nunca un objeto "root" independiente. 
  2. Una de las características principales de los tipos por valor, si va a ser inmutable.
  3. Finalmente si no va a ser convertido constantemente en un tipo de referencia (boxed).

Si nuestro tipo completa todas estas características, entonces debería ser un struct

En caso contrario, deberá ser un tipo por referencia.

 

3.2 - Cuándo utilizar record

Nuestro tipo va a ser inmutable.

Nota: para mi el ejemplo perfecto de un record es un DTO de nuestra API. Ya que nunca vamos a querer cambiar el contenido del mismo y a la vez nos ahorra tener que escribir un montón de código.

 

3.3 - Cuándo utilizar class

Si nuestro objeto no cumple las propiedades anteriores, ni de structs ni de records, el tipo que debemos usar es class.

 

Hay algunos datos más a tener en cuenta, ya que por ejemplo los records no implementan la interfaz IComparable o que los structs no soportan herencia. Pero en el 99% de los casos las pautas mostradas anteriormente.

 

 

Conclusión

  • En este post hemos visto que son y cuales son las diferencias entre los tipos por valor y los tipos por referencia. 
  • Hemos visto cuál es la diferencia entre la memoria ubicada en el stack y la memoria ubicada en el heap.
  • Finalmente hemos visto cuando utilizar struct, cuando utilizar record y cuando utilizar class en C#.

 


Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2024 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café