Ir al contenido

Borrowing en Rust

En la página de ownership viste un problema: si le pasas un String a una función, el valor se mueve, y después ya no puedes usarlo. Andar moviendo valores todo el tiempo (o clonándolos) sería muy incómodo. El borrowing (préstamo) es la solución de Rust.

Hacer borrowing significa dejar que otra parte del código use un valor sin quedarse con él. Es como prestarle un libro a un amigo: lo lee, te lo devuelve y el libro sigue siendo tuyo.

Creas una referencia poniendo & delante de un valor. Una referencia le permite a una función ver los datos sin ser su dueña:

fn print_length(s: &String) { // toma prestado s, no es su dueña
println!("Longitud: {}", s.len());
}
fn main() {
let text = String::from("hello");
print_length(&text); // le prestamos text a la función
println!("Sigue siendo mío: {text}"); // ✅ text sigue siendo válida aquí
}

Salida:

Longitud: 5
Sigue siendo mío: hello
▶ Pruébalo tú mismo

&text quiere decir “deja que esta función tome prestado text”. Como la función solo lo tomó prestado, main sigue siendo la dueña y puedes seguir usando text después. Compáralo con la página de ownership: allí, el mismo código sin & daba error.

Referencias mutables: tomar prestado para modificar

Sección titulada «Referencias mutables: tomar prestado para modificar»

Una referencia & normal es de solo lectura: puedes mirar, pero no tocar. Si una función necesita cambiar el valor prestado, usa una referencia mutable con &mut:

fn add_excitement(s: &mut String) {
s.push_str("!!!");
}
fn main() {
let mut text = String::from("hola");
add_excitement(&mut text); // lo prestamos de forma mutable
println!("{text}"); // hola!!!
}

Salida:

hola!!!
▶ Pruébalo tú mismo

Fíjate en que tres cosas tienen que encajar: text se declara con mut, la función pide un &mut String y la llamada pasa &mut text. Las tres son necesarias para modificar un valor prestado.

Rust aplica dos reglas a las referencias. Existen para evitar las carreras de datos (data races): un tipo de error en el que dos partes del código tocan los mismos datos al mismo tiempo y los corrompen.

En cualquier momento puedes tener o bien:

  • cualquier cantidad de referencias de solo lectura (&), o bien
  • exactamente una referencia mutable (&mut),

pero nunca las dos cosas a la vez.

Dicho de forma sencilla: muchos lectores o un solo escritor, pero nunca ambos juntos. Este código rompe la regla:

fn main() {
let mut text = String::from("hola");
let r1 = &text; // un préstamo de solo lectura
let r2 = &mut text; // ❌ no se puede prestar de forma mutable mientras r1 exista
println!("{r1} {r2}");
}

El compilador te detiene:

error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutable

El mensaje dice que no puedes tomar prestado text como mutable porque ya está prestado como inmutable.

Quizás esperes que un préstamo dure hasta la } que cierra su ámbito. No es así. Un préstamo dura solo hasta el último lugar donde se usa la referencia. A partir de ahí, el valor se puede volver a prestar, incluso de forma mutable.

Eso significa que el ejemplo roto de arriba se arregla simplemente terminando con r1 antes de crear r2:

fn main() {
let mut text = String::from("hola");
let r1 = &text;
println!("Leyendo: {r1}"); // último uso de r1, así que su préstamo termina aquí
let r2 = &mut text; // ✅ bien: ya nadie está leyendo
r2.push_str(", mundo");
println!("{text}");
}

Salida:

Leyendo: hola
hola, mundo
▶ Pruébalo tú mismo

La regla sigue siendo “muchos lectores o un solo escritor”. Lo que pasa es que se comprueba sobre el tramo de código en el que cada referencia se usa de verdad. El lector r1 ya ha terminado cuando aparece el escritor r2, y r2 ha terminado antes de que el último println! vuelva a leer text. Nada se solapa, así que Rust está contento.

Esta es la forma más habitual de arreglar un error de borrowing: cambiar el orden. Haz primero todas las lecturas que necesites y después las escrituras (o al revés), en lugar de mezclarlas.

Estas reglas se comprueban por completo al compilar, así que no cuestan nada mientras el programa se ejecuta. Así es como Rust puede prometer “sin carreras de datos” y a la vez ser tan rápido como C: la seguridad queda demostrada antes de que el programa arranque.

Intenta resolver cada ejercicio por tu cuenta primero, en el Playground de Rust o en un proyecto local. Si el compilador se queja, lee el error: casi siempre muestra exactamente dónde se solapan los préstamos.

Escribe una función total_letters que tome prestado un Vec<String> y devuelva cuántas letras tienen todas las palabras juntas. Después de llamarla, main todavía tiene que poder imprimir el vector.

Pista: el tipo del parámetro es &Vec<String>, y la llamas con &words. Recorre el vector prestado con for word in list y suma word.len().

Ver solución
fn total_letters(list: &Vec<String>) -> usize {
let mut total = 0;
for word in list {
total += word.len();
}
total
}
fn main() {
let words = vec![String::from("borrow"), String::from("check")];
let count = total_letters(&words); // prestamos el vector
println!("{count} letras");
println!("{:?} sigue siendo de main", words);
}

Salida:

11 letras
["borrow", "check"] sigue siendo de main
▶ Pruébalo tú mismo

La función solo necesita mirar las palabras, así que basta con un préstamo & de solo lectura. Si escribieras list: Vec<String>, el vector se movería a la función y el último println! no compilaría.

Las palabras se quedan en inglés a propósito: .len() cuenta bytes, no letras, y una letra con tilde ocupa dos bytes, así que con otras palabras el resultado podría no coincidir con las letras que ves.

Escribe dos funciones: add_item, que añade un artículo a una lista de la compra, y print_list, que imprime todos los artículos. Añade tres artículos y luego imprime la lista.

Pista: una función cambia la lista y la otra solo la lee. ¿Cuál necesita &mut Vec<String> y a cuál le basta con &Vec<String>? Para convertir un &str en un String, usa String::from(item).

Ver solución
fn add_item(list: &mut Vec<String>, item: &str) {
list.push(String::from(item));
}
fn print_list(list: &Vec<String>) {
for item in list {
println!("- {item}");
}
}
fn main() {
let mut list = Vec::new();
add_item(&mut list, "leche");
add_item(&mut list, "pan");
add_item(&mut list, "manzanas");
print_list(&list);
}

Salida:

- leche
- pan
- manzanas
▶ Pruébalo tú mismo

add_item cambia la lista, así que recibe un préstamo &mut, y list tiene que ser mut en main. print_list solo lee, así que le basta con un & normal. Cada préstamo &mut list termina cuando su llamada vuelve, y por eso la siguiente llamada puede volver a tomarlo prestado.

Este programa no compila. Arréglalo para que imprima Puntuación: 10 puntos, manteniendo tanto first como second.

fn main() {
let mut message = String::from("Puntuación:");
let first = &mut message;
let second = &mut message;
first.push_str(" 10");
second.push_str(" puntos");
println!("{message}");
}
error[E0499]: cannot borrow `message` as mutable more than once at a time
--> src/main.rs:4:18
|
3 | let first = &mut message;
| ------------ first mutable borrow occurs here
4 | let second = &mut message;
| ^^^^^^^^^^^^ second mutable borrow occurs here
5 | first.push_str(" 10");
| ----- first borrow later used here

El compilador dice que no puedes tomar prestado message como mutable más de una vez al mismo tiempo: el primer préstamo mutable ocurre en la línea 3, el segundo en la línea 4, y el primero se vuelve a usar después, en la línea 5.

Pista: solo un escritor a la vez. Mira la sección ¿Cuándo termina un préstamo? de arriba: ¿puedes terminar de usar first antes de crear second?

Ver solución
fn main() {
let mut message = String::from("Puntuación:");
let first = &mut message;
first.push_str(" 10"); // último uso de first: su préstamo termina aquí
let second = &mut message;
second.push_str(" puntos");
println!("{message}");
}

Salida:

Puntuación: 10 puntos
▶ Pruébalo tú mismo

En el original, first todavía se iba a usar (en la línea 5) cuando se creó second en la línea 4. Eso son dos escritores al mismo tiempo. Al subir first.push_str, first termina antes de que exista second, así que nunca hay más de un &mut a la vez.

  • El borrowing (&) permite que el código use un valor sin quedarse con él: te lo devuelven.
  • Una referencia & normal es de solo lectura; usa &mut para modificar el valor prestado.
  • Para modificar algo a través de una referencia, la variable, el parámetro y la llamada necesitan mut/&mut.
  • La regla: muchos lectores o un solo escritor, nunca ambos a la vez.
  • Un préstamo termina en el último uso de la referencia, así que reordenar el código suele arreglar un error de borrowing.
  • Todo esto se comprueba al compilar, así que no cuesta nada en tiempo de ejecución.

Comprueba lo aprendido

1. ¿Qué hace poner & delante de un valor al pasárselo a una función?

2. Según las reglas del borrowing, ¿qué combinación está permitida al mismo tiempo?

3. ¿Qué necesitas para modificar un valor a través de una referencia?

4. ¿Por qué Rust puede aplicar las reglas del borrowing sin ningún costo de rendimiento al ejecutar el programa?

Puntuación: 0 / 4