parametros-por-valor-o-referencia

Paso de parámetros por valor o referencia

Vamos a seguir hablando de tipos valor y tipos referencia viendo cómo se comporta otra parte del programa (una función) cuando le pasamos una variable.

Por si no lo habéis hecho, conviene mucho mucho que hayáis mirado los dos artículos anteriores Qué es una referencia y Tipos valor y tipos referencia (o esta se os va a hacer durita).

Pues venga, con los deberes hechos ¡voy a empezar la casa por el tejado! Lo que nos interesa es, sobre todo, saber si una función que recibe una variable puede modificarla, o no puede (y qué modificaciones puede hacerle).

De eso es de lo que va todo este lío, principalmente. Veamos la respuesta clásica:

Si la función recibe un parámetro de tipo valor, no puede modificar su valor

Si lo recibe por referencia, sí puede modificar su valor

Pues así de fácil ¿no? Ya hemos terminado ¡a casita! Eeeeh … evidentemente no… (precisamente esa es la respuesta normal, que quiero evitar, porque no saca de duda alguna).

La auténtica respuesta es:

Una función siempre recibe una copia de los parámetros

“Pues a mi me han dicho que si es tipo referencia y lo paso por referencia no sé… y… hay un eclipse lunar 🌒 … y… y… “. Pues que quieres que te diga, te han mentido 😜.

Por tanto, la función que recibe un parámetro nunca puede modificarlo, porque únicamente tiene una copia. Esto es así desde prácticamente el origen, y es inherente a la forma en la que funciona la arquitectura actual de nuestros procesadores.

Pero claro, que tus funciones no puedan modificar nada… les quita mucho la gracia. Bien, lo que podemos hacer es una cosa:

  1. Pasamos una copia de los datos a la función y modifica su copia
  2. Me pasa otra copia con el resultado
  3. Tiro los datos que tenía y los cambio por la nueva copia

¡Pero esto sería lentísimo! Tendría que estar moviendo todo el rato datos de aquí para allá. Así no va a funcionar la cosa, no era nada práctico.

Esto ya lo sabían la gente muy inteligente que diseñaron los procesadores. Tenían que inventar una forma de que las funciones, que solo pueden recibir copias de datos, intercambiaran datos sin tener que copiar todos los datos enteros.

Así que vamos a ver qué inventaron para arreglarlo, que lógicamente está relacionado con porqué existen los tipos valor y, sobre todo, los tipos referencia.

Cómo hacemos que una función pueda modificar una variable

Imagina que vives en un piso compartido, con un compañero. Por ponerlo difícil, este compañero no habla tu idioma, y trabaja por la noche así que nunca puedes verlo en persona. Os comunicáis a través de fotos por móvil.

Tu necesitas que tu compañero firme el contrato de alquiler. Pero no puedes pasarle el contrato por el móvil, sólo puedes mandarle una fotografía del contrato.

paso-por-valor-referencia-1

Ya puedes intentar mandarle la fotografía una y otra vez, que evidentemente tu compañero no puede firmarlo. Como mucho podría imprimirlo y firmaría SU copia del contrato. Pero no el contrato de verdad.

Así que se te ocurre una cosa diferente. Voy a dejarle el contrato en un cajón, y le mando una foto del cajón donde lo he guardado.

paso-por-valor-referencia-2

Así tu compañero va por la noche, coge el contrato, lo firma. Y tú al día siguiente, recuperas el contrato ya firmado. Todos contentos 😊.

Es decir, en vez de mandarle tus datos, le mandas una referencia diciendo dónde están los datos guardados. Así ambos podéis modificar los mismos datos.

Paso de parámetros a funciones

Recapitulemos. Acabamos de ver que las REFERENCIAS permiten que diferentes partes del programa puedan modificar una variable. Esto es así porque no contienen datos, sino enlaces a los datos.

Por otro lado, las funciones siempre reciben copias de los datos, no los datos. Pero el comportamiento será diferente si lo que reciben es un tipo valor o un tipo referencia. (en particular, la función que recibe no podrá, o sí podrá, modificar los parámetros que recibe en un caso u otro).

Para mejorar el día a todos, muchos lenguajes permiten pasar las variables a una función por tipo, o pasarlas por referencia. Lo cuál aún complica un poco más la cosa (lo vemos enseguida).

Así que, al final, tenemos cuatro posibles combinaciones entre “pasar por valor, o por referencia” y pasar un “tipo valor, o tipo referencia”.

Vamos a ver cada uno de los cuatro casos, y qué puede modificarse o no en cada uno de ellos.

  • Pasar por valor, tipo valor: Si el parámetro se modifica dentro de la función NO ❌ es modificado para el resto del programa
  • Pasar por valor, tipo referencia: Si el parámetro se modifica dentro de la función, SI ✔️ se modifica para el resto del programa
  • Pasar por referencia, tipo valor: Es idéntico al anterior, SI ✔️ se modifica.
  • Pasar por referencia, tipo referencia: La función SI ✔️ puede modificar el parámetro que recibe. Es más, SI ✔️ puede incluso devolverme una referencia diferente.

Y si lo ponemos en una tabla resumen queda así:

TipoPor valorPor referencia
Valor❌/❌✔️/❌
Referencia✔️/❌✔️/✔️

Donde, en cada celda, la combinación🅰️/🅱️ representa:

  • 🅰️: ¿puede la función modificar la variable?
  • 🅱️: ¿puede la función devolverme una variable distinta?

Funcionamiento interno Avanzado

Si queréis entender el comportamiento de cada una de las cuatro combinaciones, sigue leyendo que vamos a analizarlas.

Es importante que entendáis bien dos cosas:

  • Que la función siempre recibe una copia de los parámetros
  • Cómo funciona una REFERENCIA

Y con eso por delante, vamos al lío 👇

1. Parámetros por valor

Primer supuesto, pasar los parámetros por valor. Esta es la forma “normal” de pasar parámetros a una función.

Caso 1, pasemos una variable de tipo valor. Para el ejemplo voy a usar un número entero, un sencillo int. Pero valdría cualquier tipo valor.

paso-por-valor-referencia-3

// funcion que cambia una variable
function cambia_variable(int variable_recibida)
{
	variable_recibida = 20;
}

int mi_variable = 10;  // creo una variable de tipo valor
cambia_variable(mi_variable)  // pasamos la variable por tipo valor

// cuanto vale mi_variable ahora? 10

Veamos que es lo que ha pasado:

  1. Creamos una variable A que vale 10
  2. La función cambia_variable recibe una copia A-2, que tiene su propio 10
  3. Dentro de la función, A-2 cambia su valor a 20
  4. Al finalizar la función, A fuera sigue valiendo 10

Es decir, cada variable tiene sus propio valor. Ambas variables son independientes, y cambiar una no modifica la otra.

Caso 2, ahora hacemos lo mismo pero con una variable de tipo referencia. Por ejemplo, usemos un array para el ejemplo, pero podríamos usar cualquier otro tipo referencia.

paso-por-valor-referencia-4

// funcion que cambia una variable
function cambia_variable(int[3] variable_recibida)
{
	variable_recibida[0] = 20;
}

int[] mi_variable = {10, 0, 0};  // creo una variable de tipo referencia
cambia_variable(mi_variable)  // pasamos la variable por tipo referencia

// cuanto vale mi_variable[0] ahora? 20

Veamos que es lo que ha pasado:

  1. Creamos una variable de tipo referencia A
  2. La función cambia_variable recibe una copia de la referencia A-2
  3. Ambas referencias apuntan a los mismos datos
  4. Dentro de la función, cambiamos el valor de la Referencia
  5. Al finalizar la función, mi_variable se ha modificado

Es decir, ahora ambas variables cambian los datos porque son dos Referencias iguales. Son copias, son dos referencias distintas, pero que apuntan a los mismos datos.

Por tanto, si a través de una u otra accedemos a los datos, están modificando los mismos datos. Por eso la variable cambia.

2. Paso de parámetros por referencia

Pasar por referencia es un “sintactic sugar” para indicar al programa que cree una referencia automáticamente por nosotros. Es decir, es una forma que ofrece el lenguaje de hacer una cosa más sencilla, o fácil de leer.

En este caso, lo que hace “más fácil” es crear una referencia, sin que tengamos que hacerlo nosotros, ni la veamos. Lo cuál tiene ventajas y desventajas (ver más abajo Consejos)

Caso 3, básicamente, es exactamente lo mismo que el caso 2, pasar una referencia por tipo valor. Simplemente, en vez de trabajar con una referencia, el lenguaje crea una por nosotros.

paso-por-valor-referencia-5

Esta referencia ni la llegamos a ver (y por eso le he puesto de nombre ...) pero es el mismo caso. que el anterior. Es decir, la función puede modificar el valor, y se modifica para el resto del programa.

Caso 4, que es el que genera más confusión, pasar por referencia un tipo referencia. Spoiler de los Consejos de abajo, evitad hacer esto.

Pero vamos a analizarlo, primero lo más básico. Si la función que recibe el parámetro la modifica, la modifica para el resto del programa. Igual que los otros dos casos (2 y 3) que implican referencias, solo el tipo valor / por valor (caso 1) se libra.

paso-por-valor-referencia-6

Entonces ¿Qué tiene de diferente este caso 4? Es el único que puede modificar la referencia que le pasamos para que apunte a otro sitio.

paso-por-valor-referencia-7

Las dos referencias “automáticas” apuntan a la misma referencia. Es decir, podemos cambiar el valor a donde apunta A, por ejemplo para apuntar a nuevos datos.

// funcion que cambia una variable
function cambia_variable(ref int[3] variable_recibida)
{
	variable_recibida = {20, 0, 0}; // cambiamos donde apunta la referencia
}

int[] mi_variable = {10, 0, 0};  // creo una variable de tipo referencia
cambia_variable(mi_variable)  // pasamos la variable por tipo referencia

// cuanto vale mi_variable ahora? {20, 0, 0}

El resto de casos (1 a 3) que hemos visto no pueden cambiarme la referencia para que apunte a algo diferente. Los casos 2 y 3 sí pueden modificar los datos a los que apunta mi referencia, pero no a la referencia en sí.

Buenas prácticas Consejos

En la medida de lo posible, intentad evitar siempre pasar los parámetros por referencia. Es muy atípico, y da lugar a efectos raros difíciles de detectar.

Es mucho mejor que, si necesitáis pasar algo por referencia, creéis vosotros explícitamente un tipo que envuelva lo que queréis.

Por ejemplo, si tenéis un objeto jugador_de_futbol y necesitáis que una función que los modifique, simplemente le pasáis el jugador por valor.

function doSomething(jugador_de_futbol jugador)
{
}

Si ahora necesitáis que una función que manipule los jugador_de_futbol os creáis el objeto que necesitéis, por ejemplo equipo_de_futbol

class equipo_de_futbol {
   jugador_de_futbol[] jugadores;
}

function doSomething(equipo_de_futbol equipo)
{
}

No hay ninguna necesidad de pasar parámetros por referencia si vuestros objetos están bien. En muy raras ocasiones (nunca) es necesario pasar los parámetros por referencia.

Por otro lado, además de las consideraciones que hemos visto sobre si es posible modificar o no una variable desde una función, copiar por referencia también tiene implicaciones en cuanto a velocidad.

No obstante, no le deis excesiva importancia a esto, al menos al principio. Incluso, muchas veces el compilador va a hacer cosas por nosotros, como cambiar de tipo valor a referencia o viceversa si estima que va a ser más eficiente la ejecución.

Vosotros simplemente usad el tipo que realmente toque, salvo en casos donde realmente sea muy importante la eficiencia. Y en estos casos, testearlo bien, porque a veces te llevas sorpresas como que tu “mejora” realmente empeore el rendimiento.