Lección 07 de 26 Ownership y préstamos
Referencias y préstamos
- Con &s prestas el valor sin darlo: r1 puede leer el texto, pero la dueña sigue siendo s.
- Las referencias & son de solo lectura, así que puede haber todas las que quieras a la vez.
- Con &mut el préstamo permite modificar, pero es exclusivo: solo uno a la vez y sin & activas.
- Si mezclas & y &mut a la vez, el borrow checker lo detecta y el código no compila.
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 nadaFíjate en los dos &:
&s1crea una referencia as1: un puntero que apunta al valor sin ser su dueño.&Stringen la firma indica que el parámetro es una referencia a unString.
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 hereY 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 immutableQuien 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 froms 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
| Tipo | Qué es | ¿Puedes modificar? | ¿Cuántos a la vez? |
|---|---|---|---|
String | El valor, con su propiedad | Sí, si la variable es mut | Un solo dueño |
&String | Préstamo compartido | No | Todos los que quieras |
&mut String | Préstamo exclusivo | Sí | Solo uno, y sin & activos |
Consejo: Por defecto, haz que tus funciones reciban referencias (
&T). Pide&mut Tsolo 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 Resuelto
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().Ferris tiene 6 letras
Al ejecutar, unos tests ocultos comprueban tus funciones.
Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.