Non-lexical lifetimes en Rust: qué son y cómo simplifican el código

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

¿Qué son los non-lexical lifetimes? Conociendo la principal mejora de Rust

\n\n

Cualquiera que haya escrito en Rust al menos un par de semanas se ha encontrado con esta situación: el código es absolutamente lógico, seguro, pero el compilador (o más bien, su estricto borrow checker) se niega a compilarlo. Miras el error y piensas: «¡Pero si ya no uso más esta referencia! ¿Por qué se queja?».

\n\n

Hasta la versión Rust 1.31 (edición 2018), la respuesta era simple: «Porque el tiempo de vida de una variable (lifetime) se define por el ámbito de visibilidad — léxicamente». Esto es lexical lifetimes. A partir de Rust 2018, el compilador se volvió más inteligente. Obtuvo non-lexical lifetimes (NLL) — tiempos de vida no léxicos.

\n\n

En este artículo analizaremos qué es esta criatura, por qué su aparición fue una revolución para los desarrolladores de Rust y cómo hace que el código sea más limpio e intuitivo. No nos adentraremos en los vericuetos de la teoría de tipos, sino que nos centraremos en ejemplos prácticos.

\n\n

El problema de los lexical lifetimes: por qué el borrow checker era demasiado estricto

\n\n

Recordemos cómo funciona el préstamo (borrowing) en Rust. Tenemos reglas:

\n
    \n
  • En cualquier momento puede haber una única referencia mutable (&mut T) o cualquier número de referencias inmutables (&T).
  • \n
  • Las referencias siempre deben ser válidas (no se puede hacer referencia a datos eliminados).
  • \n
\n\n

Antes de NLL, el compilador determinaba el tiempo de vida de una referencia según su ámbito léxico. Es decir, desde { hasta }, donde se declara la variable. Si una referencia se crea dentro de un bloque, «vive» hasta el final de ese bloque, incluso si ya no la usas.

\n\n

Mira este ejemplo clásico que no se compilaba en versiones antiguas de Rust:

\n\n
fn main() {\n    let mut data = vec![1, 2, 3];\n\n    let x = &data[0];  // referencia inmutable\n\n    data.push(4);       // intento de préstamo mutable\n\n    println!("{}", x);  // uso de la referencia inmutable\n}
\n\n

Desde el punto de vista humano: creamos una referencia x, luego modificamos el vector, luego usamos x. Pero el vector data al llamar a push podría haber reasignado memoria, y la referencia x se habría vuelto inválida. El compilador tiene razón — es peligroso.

\n\n

Pero ¿qué pasa si usamos la referencia antes de la modificación? Aquí está el verdadero dolor:

\n\n
fn main() {\n    let mut name = String::from("Alice");\n\n    let first_char = &name[0..1]; // referencia inmutable\n    println!("First char: {}", first_char); // la usamos\n\n    name.push_str(" Johnson"); // modificamos la cadena\n    println!("Full name: {}", name);\n}
\n\n

Este código es seguro. Usamos la referencia first_char, la imprimimos, y luego modificamos la cadena. ¡Pero hasta Rust 2018 el compilador daba un error! ¿Por qué? Porque el tiempo de vida léxico de la referencia first_char duraba hasta el final de la función main, es decir, hasta la llave de cierre. Y el préstamo mutable (push_str) dentro de ese intervalo está prohibido.

\n\n

Los desarrolladores tenían que usar soluciones alternativas: introducir bloques adicionales {} para limitar manualmente el tiempo de vida de la referencia:

\n\n
fn main() {\n    let mut name = String::from("Alice");\n\n    {\n        let first_char = &name[0..1];\n        println!("First char: {}", first_char);\n    } // la referencia first_char muere aquí\n\n    name.push_str(" Johnson");\n    println!("Full name: {}", name);\n}
\n\n

Esto funcionaba, pero era incómodo, ensuciaba el código con llaves adicionales y dificultaba la lectura. Especialmente en funciones complejas con múltiples préstamos.

\n\n

Non-lexical lifetimes (NLL): cómo funciona

\n\n

La idea de NLL es genial en su simplicidad: el tiempo de vida de una referencia no se define por el ámbito de visibilidad, sino por dónde se usa realmente.

Blogs

Book Recommendations