Rust se compila a código máquina como C y C++, pero su compilador comprueba las reglas de propiedad de la memoria y rechaza el código que podría provocar errores como usar memoria ya liberada o carreras de datos entre hilos. Por eso lo adoptan proyectos de sistemas, navegadores y herramientas de línea de comandos.
Programas de sistemas y herramientas de terminal
Servicios web rápidos
WebAssembly y dispositivos
Para empezar: Prueba los ejemplos en el Rust Playground oficial o instala Rust con rustup y crea un proyecto con cargo new.
Nivel 1 Básico
1
Lección 1 · Básico
Tu primer programa con Cargo
Cargo es la herramienta oficial de Rust: crea proyectos, descarga dependencias, compila y ejecuta. cargo new hola crea la carpeta con src/main.rs y cargo run lo compila y ejecuta.
println! lleva una exclamación porque es una macro, no una función. Las llaves {} dentro del texto se sustituyen por los valores que pases después.
Ejemplo práctico
// src/main.rs
fn main() {
println!("¡Hola, mundo!");
println!("2 + 3 = {}", 2 + 3);
}
// cargo new hola && cd hola && cargo run
Ejercicio
¿Por qué println! lleva exclamación?
Ver la solución
Respuesta: Porque es una macro. En Rust, el ! indica que se llama a una macro, que genera código al compilar.
2
Lección 2 · Básico
Variables inmutables y mut
En Rust las variables son inmutables por defecto: una vez asignadas no cambian. Si necesitas cambiarlas, decláralas con let mut.
El compilador deduce los tipos, pero puedes indicarlos: i32 (entero), f64 (decimal), bool, char y &str o String para texto.
Ejemplo práctico
fn main() {
let nombre = "Ana"; // &str
let mut edad: u32 = 30;
edad += 1;
let precio: f64 = 19.99;
println!("{nombre} tiene {edad} años y paga {:.2}", precio * 3.0);
// nombre = "Eva"; // error: cannot assign twice to immutable variable
}
Ejercicio
Completa para que la variable pueda cambiar.
let contador = 0;
contador += 1;
Ver la solución
Respuesta:mut. Sin mut, el compilador rechaza cualquier reasignación.
3
Lección 3 · Básico
Control de flujo
if no lleva paréntesis y es una expresión: puede devolver un valor. for recorre rangos (1..=5 incluye el 5) y colecciones, while repite mientras se cumpla algo y loop repite hasta un break.
Las condiciones tienen que ser bool: Rust no convierte números en verdadero o falso.
Ejemplo práctico
fn main() {
let nota = 7;
let resultado = if nota >= 5 { "aprobado" } else { "suspenso" };
println!("{resultado}");
for i in 1..=3 {
println!("vuelta {i}");
}
}
Ejercicio
¿Qué números recorre for i in 1..=3?
Ver la solución
Respuesta: 1, 2 y 3. ..= incluye el último valor; 1..3 iría de 1 a 2.
Encuentra los errores
Este programa debería mostrar tres vueltas y «Terminado», pero no compila. Tiene 2 errores.
Los errores
Línea 2: En Rust las variables son inmutables por defecto: para poder hacer contador += 1 hay que declararla con mut. Corrección: let mut contador = 0;
Línea 7:= asigna; para comparar se usa ==. Rust da un error porque una asignación no es un booleano. Corrección: if contador == 3 {
El compilador de Rust explica los dos errores y hasta propone la corrección («help: consider changing this to be mutable»).
Ver la solución
Línea 2: En Rust las variables son inmutables por defecto: para poder hacer contador += 1 hay que declararla con mut. Corrección: let mut contador = 0;
Línea 7:= asigna; para comparar se usa ==. Rust da un error porque una asignación no es un booleano. Corrección: if contador == 3 {
4
Lección 4 · Básico
Vectores y texto
Vec<T> es una lista que crece; la macro vec! la crea con valores. Recorre sus elementos con for sobre una referencia (¬as) para no consumir el vector.
Hay dos tipos de texto: &str es una referencia a un texto que no es tuyo (como un literal) y String es un texto propio que puede crecer.
Ejemplo práctico
fn main() {
let mut notas = vec![7, 8, 9];
notas.push(10);
let suma: i32 = notas.iter().sum();
println!("Media: {}", suma as f64 / notas.len() as f64); // 8.5
let mut saludo = String::from("Hola");
saludo.push_str(", Rust");
println!("{saludo}");
}
Ejercicio
¿Qué tipo de texto puedes ampliar con push_str?
Ver la solución
Respuesta: String. String es dueño de su contenido y puede crecer; &str solo apunta a un texto existente.
Escribe el código
Escribe fn suma_pares(numeros: &[i32]) -> i32: devuelve la suma de los números pares del slice. Recórrelo con un bucle for.
Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.
Una solución posible
fn suma_pares(numeros: &[i32]) -> i32 {
let mut suma = 0;
for &n in numeros {
if n % 2 == 0 {
suma += n;
}
}
suma
}
En Rust la última expresión de una función, sin punto y coma, es el valor que devuelve: por eso basta con escribir suma al final.
Ver la solución
fn suma_pares(numeros: &[i32]) -> i32 {
let mut suma = 0;
for &n in numeros {
if n % 2 == 0 {
suma += n;
}
}
suma
}
En Rust la última expresión de una función, sin punto y coma, es el valor que devuelve: por eso basta con escribir suma al final.
Nivel 2 Intermedio
5
Lección 5 · Intermedio
Ownership y préstamos
Cada valor tiene un único dueño. Al asignar un String a otra variable o pasarlo a una función, la propiedad se mueve y la variable original ya no se puede usar.
Para usar un valor sin quedártelo, lo pides prestado con una referencia &. Puede haber muchas referencias de solo lectura o una sola &mut que modifica, pero no las dos a la vez: así Rust evita errores de memoria al compilar.
Ejemplo práctico
fn longitud(texto: &String) -> usize { // préstamo de solo lectura
texto.len()
}
fn anadir_punto(texto: &mut String) { // préstamo para modificar
texto.push('.');
}
fn main() {
let mut frase = String::from("Hola");
println!("{}", longitud(&frase)); // 4
anadir_punto(&mut frase);
println!("{frase}"); // Hola.
let otra = frase; // la propiedad se mueve
// println!("{frase}"); // error: borrow of moved value
println!("{otra}");
}
Ejercicio
Tras let otra = frase; (con frase un String), ¿qué pasa al usar frase?
Ver la solución
Respuesta: Error de compilación: el valor se ha movido. La propiedad pasó a otra; el compilador impide usar frase después.
Encuentra los errores
Este programa no compila: saludo se usa después de haberse movido. Marca las 2 líneas que hay que cambiar para prestar el texto en vez de moverlo.
Los errores
Línea 1: Si la función recibe un String, se queda con él (lo «mueve»). Para solo leerlo, recibe una referencia: &str. Corrección: fn longitud(texto: &str) -> usize {
Línea 7: Al llamarla, presta el texto con & para que saludo siga siendo válido en la línea siguiente. Corrección: let n = longitud(&saludo);
Prestar (&) permite usar un valor sin quedarse con él; el dueño sigue siendo saludo. Una función que recibe &str acepta tanto &String como textos literales.
Ver la solución
Línea 1: Si la función recibe un String, se queda con él (lo «mueve»). Para solo leerlo, recibe una referencia: &str. Corrección: fn longitud(texto: &str) -> usize {
Línea 7: Al llamarla, presta el texto con & para que saludo siga siendo válido en la línea siguiente. Corrección: let n = longitud(&saludo);
6
Lección 6 · Intermedio
Structs e impl
Un struct agrupa datos con nombre. En un bloque impl escribes sus métodos: &self para leer, &mut self para modificar.
Una función asociada sin self, como new, se llama con Tipo::new() y suele servir de constructor.
Completa el parámetro de un método que modifica el struct.
fn ingresar(& self, cantidad: f64) { … }
Ver la solución
Respuesta:mut. &mut self presta el objeto para modificarlo.
7
Lección 7 · Intermedio
Enums, match y Option
Un enum define un tipo que es una de varias variantes, y cada una puede llevar datos. match decide qué hacer en cada caso y el compilador exige que cubras todos.
Rust no tiene null: un valor que puede faltar es un Option<T>, que vale Some(valor) o None, y tienes que tratar los dos casos.
Ejemplo práctico
enum Pago {
Efectivo,
Tarjeta { ultimos4: String },
}
fn describir(p: &Pago) -> String {
match p {
Pago::Efectivo => "Pago en efectivo".to_string(),
Pago::Tarjeta { ultimos4 } => format!("Tarjeta ****{ultimos4}"),
}
}
fn main() {
println!("{}", describir(&Pago::Tarjeta { ultimos4: "1234".into() }));
let precios = vec![3.5, 12.0];
match precios.first() {
Some(p) => println!("Primero: {p}"),
None => println!("Lista vacía"),
}
}
Ejercicio
¿Cómo representa Rust un valor que puede faltar?
Ver la solución
Respuesta: Option<T>. Option<T> obliga a tratar el caso None, así que no hay errores por valores nulos olvidados.
Escribe el código
Escribe fn dividir(a: f64, b: f64) -> Option<f64>: devuelve None si b es 0 y, si no, Some con el resultado de la división.
Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.
Una solución posible
fn dividir(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
Option obliga a quien llama a tratar el caso sin resultado (con match, if let o unwrap_or), así que no hay forma de olvidarse de la división entre cero.
Ver la solución
fn dividir(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
Option obliga a quien llama a tratar el caso sin resultado (con match, if let o unwrap_or), así que no hay forma de olvidarse de la división entre cero.
Nivel 3 Avanzado
8
Lección 8 · Avanzado
Errores con Result y el operador ?
Las operaciones que pueden fallar devuelven Result<T, E>: Ok(valor) o Err(error). No hay excepciones; el tipo te obliga a decidir qué hacer con el error.
Dentro de una función que también devuelve Result, el operador ? devuelve el error hacia arriba automáticamente si algo falla.
Ejemplo práctico
use std::fs;
use std::num::ParseIntError;
fn leer_numero(texto: &str) -> Result<i32, ParseIntError> {
let n: i32 = texto.trim().parse()?; // si falla, devuelve el Err
Ok(n * 2)
}
fn main() {
println!("{:?}", leer_numero("21")); // Ok(42)
println!("{:?}", leer_numero("hola")); // Err(ParseIntError { kind: InvalidDigit })
match fs::read_to_string("notas.txt") {
Ok(texto) => println!("{texto}"),
Err(e) => println!("No se pudo leer: {e}"),
}
}
Ejercicio
Completa el operador que propaga el error.
let n: i32 = texto.parse();
Ver la solución
Respuesta:?. ? desempaqueta el Ok o devuelve el Err a quien llamó a la función.
9
Lección 9 · Avanzado
Traits y genéricos
Un trait define un comportamiento que varios tipos pueden implementar, parecido a una interfaz. Con genéricos y límites de trait (T: Display) escribes funciones que aceptan cualquier tipo que cumpla ese comportamiento.
Muchas capacidades básicas son traits de la biblioteca estándar: Display (mostrar), Clone (copiar) o Debug (mostrar para depurar, con {:?}).
Respuesta: A una interfaz. Un trait declara métodos que cada tipo implementa a su manera, como una interfaz.
10
Lección 10 · Avanzado
Concurrencia sin miedo
std::thread::spawn lanza un hilo; move le pasa la propiedad de los datos que usa, y join() espera a que termine.
Para compartir datos entre hilos se usan tipos como Arc (contador de referencias seguro entre hilos) y Mutex (acceso de uno en uno). Si intentas compartir algo de forma insegura, el programa no compila.
Ejemplo práctico
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let contador = Arc::new(Mutex::new(0));
let mut hilos = vec![];
for _ in 0..4 {
let c = Arc::clone(&contador);
hilos.push(thread::spawn(move || {
*c.lock().unwrap() += 1;
}));
}
for h in hilos {
h.join().unwrap();
}
println!("Total: {}", *contador.lock().unwrap()); // Total: 4
}
Ejercicio
¿Qué hace Mutex?
Ver la solución
Respuesta: Permite que solo un hilo a la vez acceda al dato. Un Mutex (exclusión mutua) bloquea el dato mientras un hilo lo usa.
11
Lección 11 · Avanzado
Iteradores y closures
Un iterador recorre una colección elemento a elemento. Encadenando adaptadores como filter (quedarse con algunos), map (transformar) y sum o collect (juntar el resultado) se escriben transformaciones de datos claras y, a menudo, tan rápidas como un bucle.
Los adaptadores reciben closures, funciones anónimas que se escriben con barras: |x| x * 2. Los iteradores son perezosos: no hacen nada hasta que algo, como collect, pide los resultados.
Ejemplo práctico
fn main() {
let precios = vec![12.0, 40.0, 8.0, 25.0];
let caros: Vec<f64> = precios.iter().copied().filter(|&p| p > 20.0).collect();
println!("{:?}", caros); // [40.0, 25.0]
let total: f64 = precios.iter().map(|p| p * 1.21).sum();
println!("{:.2}", total); // 102.85
let nombres = ["ana", "luis"];
let mayus: Vec<String> = nombres.iter().map(|n| n.to_uppercase()).collect();
println!("{}", mayus.join(", ")); // ANA, LUIS
}
Escribe el código
Escribe fn cuadrados_pares(v: &[i32]) -> Vec<i32>: devuelve los cuadrados de los números pares de v, sin bucles: con iter(), filter, map y collect.
Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.
Una solución posible
fn cuadrados_pares(v: &[i32]) -> Vec<i32> {
v.iter().filter(|&&n| n % 2 == 0).map(|&n| n * n).collect()
}
iter() da referencias (&i32), por eso en el filter aparece &&n: el filtro recibe una referencia a cada referencia. Escribir |n| *n % 2 == 0 también vale.
Ver la solución
fn cuadrados_pares(v: &[i32]) -> Vec<i32> {
v.iter().filter(|&&n| n % 2 == 0).map(|&n| n * n).collect()
}
iter() da referencias (&i32), por eso en el filter aparece &&n: el filtro recibe una referencia a cada referencia. Escribir |n| *n % 2 == 0 también vale.
12
Lección 12 · Avanzado
HashMap y la API entry
HashMap<K, V> guarda pares clave-valor y encuentra un valor por su clave al instante. get devuelve un Option (la clave puede no existir) e insert añade o sustituye.
La API entry resuelve en una línea el caso típico de «si la clave no existe, créala con un valor inicial; después, modifícala»: *mapa.entry(clave).or_insert(0) += 1 cuenta apariciones.
Ejemplo práctico
use std::collections::HashMap;
fn main() {
let texto = "el gato y el perro y el pez";
let mut cuenta: HashMap<&str, i32> = HashMap::new();
for palabra in texto.split_whitespace() {
*cuenta.entry(palabra).or_insert(0) += 1;
}
println!("{:?}", cuenta.get("el")); // Some(3)
println!("{:?}", cuenta.get("lobo")); // None
let mut ordenadas: Vec<_> = cuenta.into_iter().collect();
ordenadas.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
println!("{:?}", &ordenadas[..2]); // [("el", 3), ("y", 2)]
}
Ejercicio
Completa para sumar 1 a la cuenta de palabra, creándola con 0 si no existe.
*cuenta.entry(palabra).(0) += 1;
Ver la solución
Respuesta:or_insert. entry(clave) apunta a la posición de esa clave y or_insert(0) la crea con 0 si no estaba; después se le suma 1.
13
Lección 13 · Avanzado
Tiempos de vida: 'a sin miedo
Rust comprueba que ninguna referencia viva más que el dato al que apunta. Casi siempre lo deduce solo, pero cuando una función recibe varias referencias y devuelve una, necesita que le digas con cuál está relacionada la que devuelve.
Eso es una anotación de tiempo de vida: fn mas_largo<'a>(a: &'a str, b: &'a str) -> &'a str significa «el resultado es válido mientras lo sean a y b». No cambia cuánto dura nada; solo describe la relación para que el compilador la compruebe.
Ejemplo práctico
fn mas_largo<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let ciudad = String::from("Valencia");
let resultado;
{
let otra = String::from("Ávila");
resultado = mas_largo(&ciudad, &otra);
println!("{}", resultado); // Valencia
}
// println!("{}", resultado); // error: «otra» ya no existe aquí
}
Respuesta: Que el resultado solo es válido mientras lo sean las dos referencias de entrada. La anotación relaciona las referencias: el compilador impedirá usar el resultado cuando alguna de las entradas ya no exista.
14
Lección 14 · Avanzado
Pruebas con cargo test
Rust trae las pruebas incorporadas: una función con el atributo #[test] es una prueba, y cargo test las ejecuta todas y resume cuáles fallan. Se suelen poner en un módulo tests al final del mismo archivo.
assert_eq!(obtenido, esperado) falla mostrando los dos valores, assert!(condición) comprueba que algo es cierto y #[should_panic] marca una prueba que debe provocar un pánico.
Estas pruebas deberían comprobar es_par, pero tienen 2 errores: una no se ejecutaría nunca y otra espera un resultado equivocado.
Los errores
Línea 9: Sin el atributo #[test], cargo test no la ejecuta: es una función normal (y el compilador avisa de que no se usa). Corrección: #[test] fn cero_es_par() {
Línea 15: 7 no es par, así que es_par(7) devuelve false: la prueba fallaría aunque la función esté bien. Corrección: assert_eq!(es_par(7), false);
Una prueba que espera el resultado equivocado es tan engañosa como un error en el código. Comprueba que cada prueba falla cuando debe.
Ver la solución
Línea 9: Sin el atributo #[test], cargo test no la ejecuta: es una función normal (y el compilador avisa de que no se usa). Corrección: #[test] fn cero_es_par() {
Línea 15: 7 no es par, así que es_par(7) devuelve false: la prueba fallaría aunque la función esté bien. Corrección: assert_eq!(es_par(7), false);
15
Lección 15 · Avanzado
Módulos y visibilidad
Un proyecto grande se organiza en módulos con mod. Cada módulo puede estar en su propio archivo (mod facturas; busca facturas.rs) y todo lo que contiene es privado salvo lo que marques con pub.
use trae nombres de otro módulo para no escribir la ruta completa cada vez: use crate::facturas::calcular_total;. Las bibliotecas de crates.io se añaden con cargo add nombre y se usan igual.
Ejemplo práctico
mod facturas {
pub struct Linea {
pub concepto: String,
pub importe: f64,
}
pub fn total(lineas: &[Linea]) -> f64 {
lineas.iter().map(|l| l.importe).sum::<f64>() * (1.0 + IVA)
}
const IVA: f64 = 0.21; // privado: solo se usa dentro del módulo
}
use facturas::{total, Linea};
fn main() {
let lineas = vec![
Linea { concepto: "Diseño".into(), importe: 300.0 },
Linea { concepto: "Hosting".into(), importe: 100.0 },
];
println!("{} líneas, total {:.2} €", lineas.len(), total(&lineas)); // 2 líneas, total 484.00 €
// println!("{}", facturas::IVA); // error: IVA es privado
}
Ejercicio
Dentro de mod facturas, la constante IVA no lleva pub. ¿Qué pasa si main intenta usar facturas::IVA?
Ver la solución
Respuesta: Error de compilación: es privada para quien está fuera del módulo. En Rust todo es privado por defecto; solo lo marcado con pub se puede usar desde fuera del módulo.
¿Has terminado? Sigue practicando
Repasa los conceptos en el diccionario, practica con estas herramientas o prueba otro lenguaje: las ideas que has aprendido (variables, condiciones, funciones, objetos…) te servirán en cualquiera.
* Porcentaje de personas que dijeron haber usado Rust en la Stack Overflow Developer Survey 2026: Technology (pregunta sobre lenguajes de programación, de scripts y de marcado; todas las respuestas). El progreso de las lecciones se guarda solo en este navegador.
¿Nos dejas medir las visitas?
Con tu permiso usamos Google Analytics, que guarda cookies, para saber cuántas personas visitan la web y qué páginas les resultan útiles. No usamos cookies de publicidad y tus archivos nunca salen de tu navegador. Puedes cambiar de opinión cuando quieras en «Configurar cookies», al pie de la página. Más información.