Aprende Rust

Lección 07 de 26 Ownership y préstamos

Referencias y préstamos

  • 5 min de lectura
  • 4 ejercicios
  1. Con &s prestas el valor sin darlo: r1 puede leer el texto, pero la dueña sigue siendo s.
  2. Las referencias & son de solo lectura, así que puede haber todas las que quieras a la vez.
  3. Con &mut el préstamo permite modificar, pero es exclusivo: solo uno a la vez y sin & activas.
  4. Si mezclas & y &mut a la vez, el borrow checker lo detecta y el código no compila.
1/4

En la lección anterior viste que pasar un String a una función lo mueve y que, para recuperarlo, la función tenía que devolverlo. Es como si, para dejarle un libro a alguien, tuvieras que regalárselo y pedirle que te lo regale de vuelta.

Las referencias resuelven esto: permiten usar un valor sin tomar su propiedad. Crear una referencia se llama tomar prestado (borrowing), y el compilador vigila que todos los préstamos sean seguros.

Referencias con &

fn main() {
    let s1 = String::from("hola");
    let longitud = calcular_longitud(&s1); // prestamos s1
    println!("«{s1}» tiene {longitud} bytes"); // s1 sigue siendo válida
}

fn calcular_longitud(s: &String) -> usize {
    s.len()
} // s sale de ámbito, pero no es dueña de nada: no se libera nada

Fíjate en los dos &:

  • &s1 crea una referencia a s1: un puntero que apunta al valor sin ser su dueño.
  • &String en la firma indica que el parámetro es una referencia a un String.

Como calcular_longitud no es dueña del texto, al terminar no lo libera. La propiedad nunca cambia de manos y s1 sigue siendo válida en main.

Analogía: Prestar es como dejar un libro: la otra persona puede leerlo, pero sigue siendo tuyo y te lo devuelve al terminar.

Las referencias son inmutables por defecto

Igual que las variables, una referencia normal no permite modificar el valor al que apunta:

fn main() {
    let s = String::from("hola");
    cambiar(&s);
}

fn cambiar(texto: &String) {
    texto.push_str(", mundo");
}
error[E0596]: cannot borrow `*texto` as mutable, as it is behind a `&` reference
 --> src/main.rs:7:5
  |
7 |     texto.push_str(", mundo");
  |     ^^^^^ `texto` is a `&` reference, so it cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
6 | fn cambiar(texto: &mut String) {
  |                    +++

Referencias mutables con &mut

Para prestar un valor y permitir que lo modifiquen, usa una referencia mutable, &mut. Es justo lo que te sugería el help: del error:

fn main() {
    let mut s = String::from("hola");
    cambiar(&mut s);
    println!("{s}");
}

fn cambiar(texto: &mut String) {
    texto.push_str(", mundo");
}

Necesitas mut en tres sitios: en la variable (let mut s), al prestarla (&mut s) y en el tipo del parámetro (&mut String). Así, con solo leer la llamada ya sabes que la función puede cambiar tu valor.

Con números, para cambiar el valor al que apunta la referencia tienes que desreferenciarla con el operador *:

fn main() {
    let mut puntos = 10;
    sumar_bonus(&mut puntos);
    println!("Puntos: {puntos}");
}

fn sumar_bonus(puntos: &mut i32) {
    *puntos += 5; // *puntos es el i32 al que apunta la referencia
}

Con métodos como push_str o len no hace falta: al usar el punto, Rust desreferencia por ti.

Las reglas del préstamo

En cada momento, para un mismo valor puedes tener:

  • una referencia mutable (&mut), o bien
  • cualquier número de referencias inmutables (&),
  • pero nunca las dos cosas a la vez.

Además, una referencia siempre tiene que ser válida: no puede apuntar a un valor que ya no existe.

Piensa en un documento compartido: muchas personas pueden leerlo a la vez, pero si alguien lo está editando, nadie más debería tocarlo ni leer una versión a medias. Con estas reglas, Rust evita al compilar las carreras de datos (data races), uno de los errores más difíciles de depurar.

Dos referencias mutables al mismo valor no compilan:

fn main() {
    let mut s = String::from("hola");
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{r1}, {r2}");
}
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:4:14
  |
3 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
4 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
5 |     println!("{r1}, {r2}");
  |                -- first borrow later used here

Y mezclar una referencia inmutable con una mutable, tampoco:

fn main() {
    let mut s = String::from("hola");
    let r1 = &s;
    let r2 = &mut s;
    r2.push_str("!");
    println!("{r1}");
}
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

Quien tiene r1 espera que el texto no cambie mientras lo usa. Si r2 pudiera modificarlo, r1 se encontraría datos inesperados.

El borrow checker

La parte del compilador que comprueba estas reglas se llama borrow checker (comprobador de préstamos). Al principio te parecerá muy estricto, pero cada error que te muestra es un fallo que en C o C++ podría haber llegado a producción. En Rust, si compila, esos errores de memoria no existen.

Un préstamo acaba en su último uso

Un préstamo no dura hasta el final del bloque, sino hasta la última vez que se usa la referencia. Esto se conoce como non-lexical lifetimes (NLL) y hace que este código sea correcto:

fn main() {
    let mut s = String::from("hola");

    let r1 = &s;
    let r2 = &s;
    println!("{r1} y {r2}");
    // r1 y r2 no se vuelven a usar: sus préstamos terminan aquí

    let r3 = &mut s; // ahora sí se puede prestar de forma mutable
    r3.push_str(", mundo");
    println!("{r3}");
}

Muchas veces, para arreglar un error del borrow checker basta con reordenar el código para que los préstamos no se solapen.

Referencias colgantes: imposibles

Una referencia colgante (dangling reference) apunta a memoria que ya se ha liberado. En C es un error clásico. En Rust, el compilador no te deja crearla:

fn main() {
    let referencia = colgante();
    println!("{referencia}");
}

fn colgante() -> &String {
    let s = String::from("hola");
    &s // s se libera al terminar la función...
}
error[E0106]: missing lifetime specifier
 --> src/main.rs:6:18
  |
6 | fn colgante() -> &String {
  |                  ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

s se crea dentro de la función y se libera al terminar, así que la referencia apuntaría a la nada. El error habla de lifetimes, que verás más adelante en el curso. La solución aquí es devolver el String directamente para mover su propiedad a quien llama:

fn main() {
    let texto = no_colgante();
    println!("{texto}");
}

fn no_colgante() -> String {
    let s = String::from("hola");
    s // se mueve fuera: no se libera nada
}

Resumen

TipoQué es¿Puedes modificar?¿Cuántos a la vez?
StringEl valor, con su propiedadSí, si la variable es mutUn solo dueño
&StringPréstamo compartidoNoTodos los que quieras
&mut StringPréstamo exclusivoSíSolo uno, y sin & activos

Consejo: Por defecto, haz que tus funciones reciban referencias (&T). Pide &mut T solo si necesitan modificar el valor, y toma la propiedad (T) solo si la función tiene que quedarse con él.

Hora de practicar

Ejercicios

0/4 resueltos

Reto 1 de 4

Es el problema de la lección anterior, ahora con referencias: contar_letras se queda con nombre y el println! ya no puede usarlo. Cambia la función para que tome prestado el texto (&String) y pásale una referencia desde main. No uses .clone().
Salida esperada
Ferris tiene 6 letras
src/main.rs
$ cargo run Listo

Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.

¿Algo no cuadra en este reto? Avísanos