Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme - lo agradezco.
Ya has trabajado con referencias. Parecen pasivas - una forma de mirar un valor sin tocarlo. Tomas una, lees, sigues adelante. Nada se mueve. En la mayoría de los lenguajes, eso es todo.
El borrow checker sabe que está pasando algo más.
La sala de lectura
Imagina una sala de lectura con un único libro de consulta sobre la mesa. Varias personas pueden sentarse y leer a la vez - no hay problema. Los lectores no se interfieren entre sí. El libro no se mueve.
Pero si el bibliotecario necesita llevarse el libro para hacer correcciones - añadir un capítulo, reordenar páginas - todos los lectores deben abandonar la mesa primero. No es un mero trámite. Porque el bibliotecario podría reemplazar el libro entero: una edición revisada, en otra estantería. Quien aún tenga el dedo sobre una página descubrirá que su mano apunta a la nada.
La sala de lectura tiene una regla: los lectores y el bibliotecario no pueden estar activos al mismo tiempo.
La referencia inofensiva
Una referencia parece inofensiva. Escribes &tasks[0] y estás leyendo el libro - nada cambia de
manos. La lista sigue siendo tuya. Varias referencias pueden coexistir - distintas partes de tu
programa pueden mantener &tasks a la vez sin que nada se rompa. Para eso sirven las referencias.
Esa intuición se rompe en el momento en que modificas un valor mientras una de esas referencias
sigue abierta. Cualquier tipo con propiedad que gestione memoria en el heap - Vec, String,
HashMap - puede necesitar reasignar su buffer interno al crecer: asigna uno nuevo más grande,
mueve todo allí y libera el antiguo. Una referencia a esa ubicación anterior apunta ahora a memoria
liberada. El bibliotecario no solo añadió un capítulo - trasladó el libro entero a otra estantería
y descartó el ejemplar antiguo.
Llega el bibliotecario
Este es el conflicto en código. El lector ya está en la mesa.
fn main() {
let mut tasks: Vec<String> = vec![
String::from("Buy coffee"),
String::from("Write tests"),
];
let first = &tasks[0]; // el lector se sienta
tasks.push(String::from("Deploy")); // llega el bibliotecario - la mesa no está libre
println!("First task: {}", first); // el dedo apunta a la nada
}
error[E0502]: cannot borrow `tasks` as mutable because it is also borrowed as immutable
--> src/main.rs:8:5
|
7 | let first = &tasks[0];
| ----- immutable borrow occurs here
8 | tasks.push(String::from("Deploy"));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
9 | println!("First task: {}", first);
| ----- immutable borrow later used here
El compilador no se niega a ejecutar el push. Se niega a ejecutarlo mientras first sigue en la
mesa. first aparece en la línea 7 y sigue vivo en la línea 9 - el push de la línea 8 cae en ese
intervalo. Rust deduce esto de la forma de tu código antes de ejecutarlo, sin ninguna comprobación
en tiempo de ejecución.
La mesa vacía
La solución es despejar la mesa antes de que llegue el bibliotecario. Si no necesitas almacenar
first, lee directamente donde se necesita el valor - el préstamo se abre y se cierra en el mismo
instante.
fn main() {
let mut tasks: Vec<String> = vec![
String::from("Buy coffee"),
String::from("Write tests"),
];
println!("First task: {}", tasks[0]); // el lector llega, lee, se va
tasks.push(String::from("Deploy")); // mesa vacía - el bibliotecario puede trabajar
}
Sin referencia almacenada, nada mantiene la mesa ocupada. Cuando se ejecuta push, la sala de
lectura está libre.
Ahora encaja
Cuando aparece un error del borrow checker, la pregunta cambia. No “¿por qué Rust rechaza mi código?” - sino “¿quién sigue en la mesa?” Encuentra el préstamo que aún está vivo, entiende por qué llega tan lejos, y pregúntate si puede terminar antes. El mensaje de error nombra las líneas. La sala de lectura explica por qué entran en conflicto.
Dos reglas producen todos los errores del borrow checker que encontrarás:
- **En cualquier momento: o una referencia mutable, o cualquier número de referencias inmutables
- pero no ambas.** El bibliotecario trabaja, o los lectores leen. No simultáneamente.
- Las referencias no pueden sobrevivir al valor al que apuntan. La sala de lectura debe existir mientras alguien ocupe un asiento.
La primera regla es la definición de una carrera de datos, hecha irrepresentable en tiempo de compilación. La segunda es la definición de un puntero colgante, hecho imposible. No detectado en tiempo de ejecución - imposible en Rust seguro, por construcción.
Una cosa que vale la pena saber: nada de esto tiene coste en tiempo de ejecución. No se adquiere ningún bloqueo al tomar una referencia. No se comprueba ningún contador cuando expira. El compilador lo deduce de forma estática - de la forma de tu código, antes de ejecutarlo. La reestructuración ocasional que exige el borrow checker es el precio de una garantía: toda una clase de errores no puede existir en Rust seguro.