Conceptos que hacen único a Rust
Rust no es solo otro lenguaje de programación. Está basado en una serie de conceptos fundacionales que cambian en cierta forma las reglas del juego.

Vamos a ver de una forma muy rápida y resumida algunos de los conceptos que hacen único a Rust y cambian la forma en la que programamos.
Estos conceptos están relacionados con las premisas de diseño de Rust: un lenguaje muy seguro en cuanto a gestión de memoria, que a la vez sea muy eficiente y que permita escribir código expresivo y mantenible.
1. Ownership
La idea de que cada contenido en memoria, cada valor, está asociado a una única variable propietaria.
Es la razón por la que el lenguaje puede eliminar errores de memoria sin usar un recolector de basura.
Cuando la variable propietaria sale de ámbito (deja de estar activa, se consume), su contenido se libera de forma automática. En memoria no puede haber contenido que no tenga propietario.
fn saludar() {
let saludo = String::from("Saludos!");
// saludo es la variable propietaria de "Saludos!"
println!("{}", saludo); // ok
}
// saludo sale de su ámbito
// el contenido "Saludos" se libera de forma automática
Una de las implicaciones más evidentes del sistema de ownership es que en las asignaciones tiene que desaparecer una de las variables que intervienen (la variable original se consuma) o se tiene que clonar el contenido… Pero en ningún caso pueden dar lugar a contenido con más de un propietario.
fn saludar() {
let saludo = String::from("Saludos!");
let hola = saludo;
// hola pasa a ser la variable propietaria
// saludo se consume, deja de estar activa
println!("{}", saludo); // ERROR
println!("{}", hola); // OK
let numero = 10;
let otro_numero = numero;
// para las variables de copia (enteros, flotantes, etc.)
// es más eficiente clonar el contenido
// Rust lo hace de forma automática
// numero y otro_numero son propietarias de 10 y 10
// cada uno de esos 10 es independiente
let saludo = String::from("Saludos!");
let hola = saludo.clone();
// se puede hacer un clonado explícito
// para conservar la variable original
// ahora hay dos "Saludos!" independientes
}
// todas las variables locales
// se consumen al salir de su ámbito
// y sus contenidos se liberan automáticamente
2. Mutabilidad
En Rust podemos encontrar muchas ideas heredadas de la programación funcional, incluyendo el concepto de inmutabilidad.
Las variables en Rust son inmutables por defecto. Para que una variable sea mutable es necesario declararla de forma explícita con el modificador mut.
let saludo = String::from("Saludos");
// saludo es inmutable
saludo.push_str(", terrícolas!"); // ERROR
let mut saludo = String::from("Saludos");
// saludo es mutable
saludo.push_str(", terrícolas!"); // Ok
El compilador se encarga de comprobar (en tiempo de compilación, claro) que una variable inmutable no puede cambiar su estado en ningún punto del programa.
Esto también tiene muchas implicaciones en el código.
3. Borrowing
En Rust aparece el concepto de préstamo.
Trabajar sólo con variables propietarias puede resultar tedioso y poco eficiente. En muchos casos es preferible trabajar con referencias.
En Rust se dice que una referencia toma prestado el valor de la variable propietaria. O, desde otro punto de vista, la variable propietaria genera un préstamo, equivalente a lo que sería una referencia en otros lenguajes. En muchas situaciones podemos trabajar con la referencia de forma similar a como haríamos con la variable original.
let saludo = String::from("Saludos");
let saludo_prestado = &saludo;
// la referencia actúa en nombre de
// su variable propietaria
println!("{}", saludo_prestado);
La cuestión es que las variables en Rust son un poco más complejas que en otros lenguajes: incluyen información sobre su ownership y sobre su mutabilidad.
Rust fue diseñado para garantizar que nunca puedan aparecer referencias huérfanas (dangling pointers) en ningún punto del código. Una referencia no puede sobrevivir a su variable propietaria. Si una variable propietaria se consume y deja de estar activa, automáticamente dejan de estar activos todos sus préstamos, todas las referencias asociadas.
El compilador obliga al programador a cumplir una serie de reglas muy estrictas relacionadas con el uso de préstamos y referencias. Especialmente cuando se trata de un préstamo mutable.
Podríamos resumir estas reglas de una forma muy simplificada:
- Una variable inmutable nunca puede generar un préstamo mutable.
- Podemos tener muchas referencias inmutables…
- O una única referencia mutable…
- Pero nunca pueden convivir referencias mutables e inmutables que representan un mismo contenido.
Esto tiene como consecuencia directa que una referencia inmutable jamás puede ser testigo de un cambio de estado de la variable a la que representa. El compilador nos garantiza que esta afirmación se cumplirá siempre, en cualquier circunstancia.
En Rust es imposible que se den condiciones de carrera (data races) porque sólo puede haber una referencia mutable activa en un momento dado.
4. Lifetimes
Para hacer cumplir las reglas de ownership y borrowing, el compilador tiene que hacer un seguimiento de todas las variables y sus préstamos a lo largo del flujo de ejecución.
Recordemos que todo esto se hace en tiempo de compilación. Es algo muy complejo, sobre todo el seguimiento de los préstamos, que pueden estar dispersos en muchos lugares diferentes del flujo de ejecución. Y el compilador tiene que determinar que ninguna referencia pueda ser utilizada una vez que la variable propietaria se ha consumido.
Para hacer el seguimiento de las referencias utiliza internamente lifetimes. Los podemos imaginar como caminos dentro del flujo del código fuente o zonas de código, en cierta forma relacionadas con las zonas que delimitan los ámbitos.
La gestión de lifetimes es interna y transparente para el programador en la inmensa mayoría de las situaciones.
Sin embargo, hay situaciones ambiguas que el compilador no puede resolver de forma automática. En caso de duda, el compilador rechaza cualquier código que no tenga un comportamiento predecible.
Para resolver esas situaciones ambiguas se pueden utilizar etiquetas o anotaciones de lifetime, que le dicen al compilador que todas las referencias marcadas con esa etiqueta estarán activas, como mínimo, hasta que una de ellas deje de estarlo. Es decir, se establece un “ámbito” mínimo para todas las referencias que comparten ese lifetime.
// devolver una referencia
fn obtener_texto<'a>(texto: &'a str) -> &'a str {
texto
}
La etiqueta 'a le dice al compilador que la referencia devuelta tendrá que estar activa, como mínimo, en el mismo ámbito o flujo de ejecución que la referencia que recibe como argumento la función.
Las anotaciones de lifetime suelen ser necesarias en situaciones concretas:
- Al retornar referencias de funciones (en muchos casos el compilador puede inferir correctamente el lifetime de forma automática)
- Al guardar referencias en structs
- En código genérico avanzado
5. Traits
Los traits de Rust los podemos pensar como habilidades que podemos definir de forma abstracta.
Por ejemplo, puedo definir una habilidad Volar, que está relacionada con un determinado comportamiento o funcionalidad. Pero también puedo definir un trait Achuchable, que representa una cualidad.
trait Ronronear {
fn ronronea();
}
trait Achuchable {};
Esas habilidades o cualidades abstractas se pueden aplicar a los tipos de datos que nos interesen, como si fueran poderes especiales que asignamos a voluntad.
struct Gato {}
impl Ronronear for Gato {
fn ronronea() {
println("rrr rrr");
}
}
impl Achuchable for Gato {}
// el tipo de dato Gato puede ronronear
// y además es Achuchable
Aunque se parecen a las interfaces de otros lenguajes de programación, los traits de Rust son más flexibles y potentes. Permiten hacer composición de comportamientos y desacoplar partes del código de una forma muy granular y efectiva.
No son una característica más de Rust, son la base sobre la que se construye el propio lenguaje. La librería estándar está sustentada sobre una serie de traits que rigen el comportamiento del propio compilador.
6. Construcciones genéricas
Es otro de los grandes pilares de Rust.
Las construcciones genéricas existen en muchos lenguajes de programación. Las podemos imaginar como plantillas de código fuente que permiten reutilizar patrones para aplicarlos a diferentes tipos de datos (lo que evita reescribir código similar una y otra vez para cada tipo de dato).
Rust es un lenguaje fuertemente tipado, con un sistema de tipos muy rico y expresivo, y con una serie de características únicas: ownership, mutabilidad y reglas asociadas a los préstamos para evitar problemas de gestión de memoria. Así que las construcciones genéricas tienen que recoger e integrar todas esas características.
Los traits y las construcciones genéricas mantienen una relación de simbiosis en Rust. Los traits permiten por ejemplo restringir el uso de ciertas contrucciones genéricas a ciertos tipos de datos. De esa forma pueden convivir construcciones genéricas muy poco restrictivas pero con comportamientos muy limitados, con construcciones genéricas muy restrictivas pero con comportamientos muy complejos.
// función genérica en T
fn mostrar<T>(valor: T)
where T: std::fmt::Debug // restricción mediante traits
{
println!("{:?}", valor);
}
En el ejemplo anterior, la función mostrar() puede ser utilizada por cualquier tipo de dato, siempre que ese tipo implemente el trait Debug.
La librería estándar de Rust y su rico sistema de tipos, se sustenta en la combinación de construcciones genéricas y traits.
Y, por supuesto, podemos hacer uso de nuestros propios traits, nuestras propias construcciones genéricas y nuestros propios tipos de datos para trabajar con código expresivo, fácil de mantener, seguro y eficiente.