DevTools

Cheatsheet Rust

Linguagem de sistemas com segurança de memória sem garbage collector

Volver a los lenguajes
Rust
92 tarjetas encontradas
Categorías:
Versiones:

Básico e Tipos


11 cards
Variables y mutabilidad
let x = 5;        // inmutable
let mut y = 10;   // mutable
y = 20;           // OK

// Shadowing (nueva variable):
let x = x + 1;    // nuevo x
let x = x * 2;    // otro x

// Type annotation:
let n: i64 = 42;
let s: &str = "hola";

En Rust las variables son inmutables por defecto — el compilador impide la reasignación accidental. Usa mut solo cuando sea necesario. El shadowing crea una nueva variable con el mismo nombre (puede incluso cambiar el tipo).

Strings
// &str (slice inmutable):
let s: &str = "Hola Mundo";

// String (heap, mutable):
let mut s = String::from("Hola");
s.push_str(" Mundo");
s.push('!');

// Formateo:
let msg = format!("¡Hola, {}!", nombre);
let n = format!("{:.2}", 3.14159); // "3.14"
let p = format!("{:05}", 42);      // "00042"

// Operaciones:
s.len();  s.contains("Mundo");
s.replace("Mundo", "Rust");

Rust distingue &str (porción inmutable) de String (buffer mutable en el heap). Usa &str en parámetros para aceptar ambos sin asignar memoria. La macro format! crea strings formateadas sin mutación.

Type aliases
// Alias para tipos anchos:
type Result<T> = std::result::Result<T, AppError>;
type Punto = (f64, f64);
type Callback = Box<dyn Fn(i32) -> i32>;

// Uso:
fn origen() -> Punto {
    (0.0, 0.0)
}

// No crea un tipo nuevo (es solo un atajo):
type Metros = f64;
type Segundos = f64;
// Metros y Segundos son intercambiables

El type crea un alias (atajo) para un tipo existente — no crea un tipo nuevo, por eso los tipos son intercambiables. Es útil para simplificar firmas largas como Result<T, AppError> o callbacks complejos.

Constantes y statics
// const: evaluada en compilación
const MAX: u32 = 100_000;
const PI: f64 = 3.14159;

// static: vive todo el programa
static NOMBRE: &str = "Rust";
static mut CONTADOR: u32 = 0;

// Diferencia:
// const se copia en cada uso
// static tiene dirección fija en memoria

El const se evalúa en tiempo de compilación y se copia en cada uso. El static vive durante todo el programa y tiene una dirección fija. Usa static mut con cuidado (requiere unsafe).

Conversiones de tipos
// parse (string -> tipo):
let n: i32 = "42".parse().unwrap();

// as (casting numérico):
let y: u8 = 100i32 as u8;
let z: f64 = 42 as f64;

// to_string:
let s = 42.to_string();

// From / Into (infalible):
let s = String::from("hola");

// TryFrom (puede fallar):
use std::convert::TryFrom;
let r = u8::try_from(300); // Err

Rust no hace conversiones implícitas — todas son explícitas. El parse() convierte strings en números (devuelve Result). El as hace casting pero puede truncar. From/Into son infalibles; TryFrom devuelve Result cuando puede fallar.

Inferencia de tipos
// El compilador infiere el tipo:
let x = 5;          // i32 (default)
let f = 3.14;       // f64 (default)
let v = vec![1, 2]; // Vec<i32>

// Inferencia por el uso:
let n = "42".parse().unwrap();
// n: ¿i32? ¿i64? — necesita contexto
let n: i64 = "42".parse().unwrap();

// Turbofish (::<>):
let n = "42".parse::<i32>().unwrap();
let v = vec![1, 2, 3].iter().collect::<Vec<_>>();

El compilador infiere tipos a partir del contexto, pero enteros y floats tienen default (i32, f64). Cuando es ambiguo, anota el tipo o usa la sintaxis turbofish ::<Tipo> para especificar explícitamente el parámetro genérico.

Tipos escalares
// Enteros:
i8, i16, i32, i64, i128, isize
u8, u16, u32, u64, u128, usize

// Float:
let f: f32 = 3.14;
let d: f64 = 3.14159265;

// Bool y Char (Unicode 4 bytes):
let activo: bool = true;
let emoji: char = '🦀';

// Literales:
let dec = 98_222;
let hex = 0xff;
let bin = 0b1111_0000;

Rust tiene enteros con tamaño explícito (i8 a i128, u8 a u128); isize/usize se adaptan a la arquitectura. El char es un scalar Unicode de 4 bytes. Los guiones bajos en literales (98_222) mejoran la legibilidad.

Operadores
// Aritméticos:
+ - * / %
let r = 10 / 3;  // 3 (entero)

// Comparación y lógicos:
==  !=  >  <  >=  <=
&&  ||  !

// Bitwise:
&  |  ^  !  <<  >>

// Range:
1..5    // 1,2,3,4
1..=5   // 1,2,3,4,5

// No hay ++ ni -- (usa += 1)

La división entera trunca hacia cero. Los ranges 1..5 (exclusivo) y 1..=5 (inclusivo) son iteradores lazy. Rust no tiene ++ ni -- — usa += 1 para evitar ambigüedad de precedencia.

Tuples y destructuring
let tup = (1, "dos", 3.0);

// Acceso por índice:
let a = tup.0;  // 1
let b = tup.1;  // "dos"

// Destructuring:
let (x, y, z) = tup;

// Ignorar elementos:
let (primero, ..) = tup;
let (.., último) = tup;

// Retorno múltiple de función:
fn dividir(a: f64, b: f64) -> (f64, f64) {
    (a / b, a % b)
}
let (q, r) = dividir(10.0, 3.0);

Las tuples agrupan valores de tipos diferentes con acceso por índice (.0, .1). El destructuring extrae todo de una vez; .. ignora el resto. Son ideales para devolver múltiples valores de una función sin crear un struct.

Tipos compuestos
// Tuple:
let tup: (i32, f64, char) = (500, 6.4, 'A');
let (x, y, z) = tup;   // destructuring
let primero = tup.0;

// Array (tamaño fijo, stack):
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let ceros = [0; 10];   // 10 ceros

// Slice (referencia parcial):
let slice = &arr[1..3]; // [2, 3]

// Unit type:
let _unit: () = ();

Las tuples agrupan tipos diferentes con acceso por índice (.0) o destructuring. Los arrays tienen tamaño fijo en la stack. Los slices (&arr[1..3]) son vistas sin copiar datos. El unit type () representa la ausencia de valor.

Macros básicas
// println! (stdout):
println!("¡Hola, {}!", nombre);
println!("{x} + {y} = {}", x + y);
println!("{:#?}", struct_compleja);

// eprintln! (stderr):
eprintln!("Error: {}", msg);

// format! (devuelve String):
let s = format!("{} años", edad);

// dbg! (debug):
let x = dbg!(2 + 2);

// assert:
assert!(x > 0);
assert_eq!(a, b);

Las macros (con !) se expanden en compilación. El println! usa {:?} para Debug y {:#?} formateado. El dbg! imprime archivo, línea y valor. assert!/assert_eq! causan panic si fallan.

Ownership e Memória


10 cards
Ownership
// Un valor = un dueño:
let s1 = String::from("hola");
let s2 = s1;   // ¡s1 movido! inválido
// println!("{s1}"); // ¡ERROR!

// Clone (copia profunda):
let s2 = s1.clone(); // ambos válidos

// Tipos Copy (stack, copiados):
let x = 5;
let y = x;   // x aún válido (i32 es Copy)

// Al salir del scope: drop automático

El ownership es el concepto central de Rust — cada valor tiene un dueño, y al salir del scope se libera (sin GC). Asignar mueve el valor (el original queda inválido). Los tipos simples en la stack (i32, bool) son Copy y no se mueven.

Rc y Arc
// Rc (múltiples dueños, single-thread):
use std::rc::Rc;
let a = Rc::new(String::from("compartido"));
let b = Rc::clone(&a);  // ref count = 2
let c = Rc::clone(&a);  // ref count = 3
drop(c);                // ref count = 2

// Arc (atomic, thread-safe):
use std::sync::Arc;
let datos = Arc::new(vec![1, 2, 3]);
let d2 = Arc::clone(&datos);
std::thread::spawn(move || {
    println!("{:?}", d2);
});

El Rc (Reference Counting) permite múltiples dueños del mismo dato — solo se libera cuando el contador llega a cero. No es thread-safe. El Arc (Atomic Rc) es la versión thread-safe, con conteo atómico — necesario para compartir entre threads.

Drop y RAII
struct Conexion { nombre: String }

impl Drop for Conexion {
    fn drop(&mut self) {
        println!("Cerrando {}", self.nombre);
    }
}

{
    let l = Conexion { nombre: "DB".into() };
    // usa la conexión...
}   // drop() llamado automáticamente aquí

// Forzar drop antes:
let l = Conexion { nombre: "DB".into() };
drop(l);   // cierra ya
// l no puede usarse después

El trait Drop ejecuta código cuando un valor sale del scope (RAII — Resource Acquisition Is Initialization). Se llama automáticamente — ideal para cerrar ficheros, conexiones y liberar recursos. La función drop() fuerza la liberación anticipada.

Borrowing y referencias
// Borrow inmutable (&):
fn tamano(s: &String) -> usize { s.len() }
tamano(&s1);   // s1 aún válido

// Borrow mutable (&mut, solo 1 a la vez):
fn agregar(s: &mut String) {
    s.push_str(" mundo");
}
agregar(&mut s1);

// Reglas:
// 1. Un &mut O varios & (nunca ambos)
// 2. Las referencias deben ser siempre válidas

El borrowing (&) permite referenciar sin tomar ownership. Las reglas: puede haber un &mut O varios &, nunca ambos a la vez. Esto previene data races y use-after-free en tiempo de compilación — seguridad de memoria sin coste en runtime.

RefCell
use std::cell::RefCell;

// Interior mutability:
let datos = RefCell::new(vec![1, 2, 3]);

// Mutar a través de referencia inmutable:
datos.borrow_mut().push(4);

// Leer:
let r = datos.borrow();
println!("{:?}", *r);

// Reglas verificadas en RUNTIME:
// - varios borrow() O un borrow_mut()
// - violarlas causa panic (no error de compilación)

El RefCell permite mutación a través de una referencia inmutable (interior mutability). Las reglas de borrowing se verifican en runtime (borrow()/borrow_mut()) — violarlas causa panic. Úsalo cuando necesites mutar datos detrás de &self.

Stack vs Heap
// Stack (rápida, tamaño fijo):
let x = 42;            // i32 en la stack
let p = (1.0, 2.0);    // tuple en la stack
let a = [0; 100];      // array en la stack

// Heap (Box, Vec, String):
let b = Box::new(42);  // i32 en el heap
let v = vec![1, 2, 3]; // datos en el heap
let s = String::from("hola");

// El tamaño debe ser conocido (Sized):
// fn f(x: dyn Resumen) {} // ERROR
fn f(x: &dyn Resumen) {}   // OK (puntero)

La stack es rápida pero exige tamaño fijo conocido en compilación (Sized). Los datos de tamaño variable (Vec, String) guardan los datos en el heap y un puntero en la stack. Los trait objects necesitan & o Box porque su tamaño es dinámico.

Move y Clone
// Move (heap types):
let v1 = vec![1, 2, 3];
let v2 = v1;        // v1 inválido

// Clone (copia profunda):
let v2 = v1.clone(); // ambos válidos

// Las funciones también mueven:
fn consumir(v: Vec<i32>) { }
consumir(v2);       // v2 movido, inválido

// Borrow para no mover:
fn leer(v: &Vec<i32>) { }
leer(&v1);          // v1 sigue válido

Los tipos con datos en el heap (Vec, String) se mueven por defecto al asignar o pasar a funciones. El clone() hace una copia profunda (coste). Para evitar mover, pasa por referencia (&) — borrowing.

Cow
use std::borrow::Cow;

fn normalizar(s: &str) -> Cow<str> {
    if s.contains(' ') {
        // Necesita modificar -> asigna
        Cow::Owned(s.replace(' ', "_"))
    } else {
        // Sin modificación -> presta
        Cow::Borrowed(s)
    }
}

let a = normalizar("sin_espacio"); // Borrowed
let b = normalizar("con espacio"); // Owned

El Cow (Clone-On-Write) evita clones innecesarios: si no modifica, presta (Borrowed); si necesita cambiar, asigna (Owned). Es ideal para funciones que solo a veces necesitan transformar el input, ahorrando asignaciones.

Box (heap)
// Asigna en el heap:
let b = Box::new(5);

// Tipos recursivos:
enum Lista {
    Vacia,
    Nodo(i32, Box<Lista>),
}
let lista = Lista::Nodo(1, Box::new(Lista::Nodo(2, Box::new(Lista::Vacia))));

// Trait objects:
let item: Box<dyn Resumen> = Box::new(articulo);

// Struct grande en la stack -> heap:
let grande = Box::new(StructEnorme::nuevo());

Box<T> asigna datos en el heap (la stack guarda solo el puntero). Es necesario para tipos recursivos (si no, el tamaño sería infinito), trait objects (Box<dyn Trait>) y para mover structs grandes sin copiar.

Interior mutability
struct Cache {
    valor: RefCell<Option<String>>,
}

impl Cache {
    // &self (no &mut) pero muta internamente:
    fn obtener(&self) -> String {
        let mut v = self.valor.borrow_mut();
        if v.is_none() {
            *v = Some(self.calcular());
        }
        v.clone().unwrap()
    }
}

La interior mutability permite mutar datos a través de &self (referencia inmutable), usando RefCell, Cell o Mutex por dentro. Es útil para caches, contadores y lazy initialization donde el método es lógicamente inmutable.

Funções e Closures


10 cards
Definir funciones
fn sumar(a: i32, b: i32) -> i32 {
    a + b   // sin ; = retorno
}

// Sin retorno (unit):
fn saludar(nombre: &str) {
    println!("¡Hola, {nombre}!");
}

// Early return:
fn abs(x: i32) -> i32 {
    if x < 0 { return -x; }
    x
}

fn main() {
    println!("{}", sumar(2, 3));
}

Las funciones declaran tipos de todos los parámetros (obligatorio). El retorno implícito es la última expresión sin ; — el return es para early exits. Sin ->, la función devuelve () (unit).

Funciones genéricas
// Trait bound:
fn mayor<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

// Where clause:
fn procesar<T, U>(t: T, u: U) -> String
where
    T: Display + Clone,
    U: Debug,
{
    format!("{t}: {u:?}")
}

// impl Trait en el retorno:
fn crear_iter() -> impl Iterator<Item = i32> {
    (0..10).filter(|x| x % 2 == 0)
}

Los generics funcionan con cualquier tipo que satisfaga los trait bounds — sin coste en runtime (monomorfización). La where clause organiza bounds complejos. El impl Trait en el retorno esconde el tipo concreto (útil para iteradores y closures).

Funciones const
// const fn: evaluable en compilación:
const fn cuadrado(x: u32) -> u32 {
    x * x
}

const AREA: u32 = cuadrado(10); // 100

// También puede usarse en runtime:
let n = cuadrado(5); // 25

// Limitaciones (menos ops permitidas):
const fn max(a: u32, b: u32) -> u32 {
    if a > b { a } else { b }
}

Una const fn puede evaluarse en tiempo de compilación cuando los argumentos son constantes. También funciona en runtime normalmente. Tiene limitaciones (operaciones permitidas), pero permite calcular valores para const y arrays.

Closures
// Sintaxis:
let suma = |a, b| a + b;
let doble = |x: i32| -> i32 { x * 2 };

// Captura el entorno:
let factor = 3;
let triple = |x| x * factor;

// Como argumento:
let mut nums = vec![3, 1, 4, 1, 5];
nums.sort_by(|a, b| a.cmp(b));

nums.iter().filter(|&&x| x > 2);

Las closures son funciones anónimas que capturan variables del entorno. El compilador infiere los tipos (anotaciones opcionales). La sintaxis |params| cuerpo es compacta e ideal para callbacks en iteradores y ordenación.

Métodos (impl)
struct Círculo { radio: f64 }

impl Círculo {
    // Constructor (método asociado):
    fn nuevo(radio: f64) -> Self {
        Círculo { radio }
    }

    // Borrow (&self):
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radio.powi(2)
    }

    // Mutable (&mut self):
    fn escalar(&mut self, f: f64) {
        self.radio *= f;
    }
}

Los bloques impl asocian métodos a structs. El primer parámetro define el receiver: &self (borrow), &mut self (mutación) o self (consume). Las funciones sin self son métodos asociados llamados con Círculo::nuevo().

Parámetros y retornos
// Múltiples retornos (tuple):
fn dividir(a: f64, b: f64) -> (f64, f64) {
    (a / b, a % b)
}
let (q, r) = dividir(10.0, 3.0);

// Retorno nombrado (no existe):
// Rust no tiene named returns

// never type (!):
fn fallar() -> ! {
    panic!("error fatal");
}

// impl Trait como parámetro:
fn mostrar(item: impl Display) {
    println!("{item}");
}

Para múltiples valores de retorno usa tuples con destructuring. El never type ! indica funciones que nunca retornan (como panic!). El impl Trait como parámetro es azúcar para un genérico con trait bound.

Closures con move
let datos = vec![1, 2, 3];

// move toma ownership:
let closure = move || {
    println!("{:?}", datos);
};
closure();

// Necesario para threads:
let handle = std::thread::spawn(move || {
    // datos fue movido a la thread
    datos.len()
});

// Sin move: la closure solo presta
// (puede causar error si datos sale del scope)

El move obliga a la closure a tomar ownership de los valores capturados. Es esencial cuando la closure sobrevive al scope donde fue creada — por ejemplo, al enviarla a otra thread con thread::spawn.

Function pointers
fn doblar(x: i32) -> i32 { x * 2 }

// fn es un tipo (no captura el entorno):
let f: fn(i32) -> i32 = doblar;
println!("{}", f(5)); // 10

// Como parámetro:
fn aplicar(v: &[i32], f: fn(i32) -> i32) -> Vec<i32> {
    v.iter().map(|&x| f(x)).collect()
}

// Devolver closure:
fn multiplicador(f: i32) -> impl Fn(i32) -> i32 {
    move |x| x * f
}

El tipo fn (minúscula) es un function pointer — a diferencia de las closures, no captura el entorno. Es útil para pasar funciones con nombre sin overhead de generics. Para devolver closures usa impl Fn (esconde el tipo anónimo).

Traits funcionales
// Fn (borrow, se llama varias veces):
fn aplicar(f: &dyn Fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

// FnMut (muta el entorno):
fn contador(f: &mut dyn FnMut()) {
    f(); f();
}

// FnOnce (se consume, se llama 1 vez):
fn ejecutar(f: impl FnOnce() -> String) {
    println!("{}", f());
}

// Uso:
let r = aplicar(&|x| x * 2, 5); // 10

Rust tiene tres traits funcionales: Fn (solo lee, se llama varias veces), FnMut (puede mutar variables capturadas) y FnOnce (se consume al ser llamada). El compilador elige cuál implementa la closure automáticamente.

Recursión
// Factorial:
fn factorial(n: u64) -> u64 {
    if n <= 1 { 1 } else { n * factorial(n - 1) }
}

// Fibonacci:
fn fib(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fib(n - 1) + fib(n - 2),
    }
}

// Recursión de cola (tail):
fn suma(n: u64, acc: u64) -> u64 {
    if n == 0 { acc } else { suma(n - 1, acc + n) }
}

Rust soporta recursión, pero el compilador no garantiza optimización de cola. Para grandes profundidades prefiere iteración o recursión de cola con acumulador. El match es ideal para definir casos base y recursivos de forma clara.

Coleções


11 cards
Vec (vector)
// Crear:
let mut v: Vec<i32> = vec![1, 2, 3, 4, 5];
let ceros = vec![0; 10];

// Modificar:
v.push(6);
v.pop();          // Option<i32>
v.insert(0, 0);
v.retain(|&x| x > 2);

// Acceso:
v[0];             // panics si está fuera
v.get(0);         // Option<&i32>
v.first();  v.last();
v.len();  v.is_empty();

// Iterar:
for x in &v { println!("{x}"); }

Vec<T> es la colección más usada — array dinámico en el heap. v[i] hace panic si el índice es inválido; v.get(i) devuelve Option (seguro). El retain() filtra in-place sin asignar un nuevo vector.

String operations
let mut s = String::from("Hola Mundo");

// Añadir:
s.push('!');
s.push_str(" Rust");

// Transformar:
s.to_uppercase();
s.replace("Mundo", "Rust");
s.trim().to_string();

// Split:
let partes: Vec<&str> = s.split(' ').collect();
let lineas: Vec<&str> = texto.lines().collect();

// Verificar:
s.contains("Mundo");
s.starts_with("Hola");
s.len();           // ¡bytes!
s.chars().count(); // caracteres

String en Rust es UTF-8 — len() devuelve bytes, no caracteres (un emoji puede ser 4 bytes). Para contar caracteres usa chars().count(). El split() devuelve un iterador de &str zero-copy. lines() divide por líneas.

VecDeque
use std::collections::VecDeque;

let mut cola = VecDeque::new();

// Insertar en ambos extremos:
cola.push_back(1);   // [1]
cola.push_back(2);   // [1, 2]
cola.push_front(0);  // [0, 1, 2]

// Eliminar de ambos extremos:
cola.pop_front();    // Some(0)
cola.pop_back();     // Some(2)

// Acceso:
cola.front();  cola.back();
cola.get(0);   cola.len();

El VecDeque es una cola de doble extremo (deque) — inserción y eliminación O(1) en ambos extremos. Úsalo cuando necesites una cola (FIFO) o pila con acceso a los dos extremos. El Vec solo es eficiente insertando/eliminando al final.

HashMap
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert("Ana", 95);
scores.insert("Ray", 87);

// Acceso:
let v = scores.get("Ana"); // Option<&i32>

// Entry API:
scores.entry("Eva").or_insert(100);
scores.entry("Ana").and_modify(|v| *v += 5);

// Iterar:
for (k, v) in &scores {
    println!("{k}: {v}");
}

scores.contains_key("Ana");
scores.remove("Ray");

El HashMap ofrece lookup O(1) por clave. La API entry() es idiomática: or_insert() inserta si no existe, and_modify() actualiza si existe — evita el double-lookup. El get() devuelve Option; el orden de iteración no es determinista.

HashSet y BTreeMap
use std::collections::{HashSet, BTreeMap};

// HashSet (únicos, sin orden):
let mut set = HashSet::new();
set.insert(1); set.insert(2); set.insert(2);
set.len();        // 2
set.contains(&1); // true

// Operaciones de conjuntos:
let union_set: HashSet<_> = a.union(&b).collect();
let inter: HashSet<_> = a.intersection(&b).collect();

// BTreeMap (ordenado por clave):
let mut bt = BTreeMap::new();
bt.insert("banana", 2);
bt.insert("apple", 1);
// Itera en orden: apple, banana

El HashSet garantiza unicidad con lookup O(1) y soporta operaciones de conjuntos (unión, intersección, diferencia). BTreeMap/BTreeSet mantienen los elementos ordenados (O(log n)). Elige HashMap para rendimiento, BTree cuando el orden importa.

BinaryHeap
use std::collections::BinaryHeap;

let mut heap = BinaryHeap::new();
heap.push(3);
heap.push(1);
heap.push(4);

// Max-heap (el mayor primero):
heap.peek();  // Some(&4)
heap.pop();   // Some(4)
heap.pop();   // Some(3)

// Min-heap con Reverse:
use std::cmp::Reverse;
let mut min = BinaryHeap::new();
min.push(Reverse(3));
min.push(Reverse(1));
min.pop();    // Some(Reverse(1))

El BinaryHeap es una cola de prioridad (max-heap por defecto — el mayor sale primero). El peek() ve la cima sin eliminar, el pop() la elimina. Para un min-heap, envuelve los valores en Reverse. Útil para top-K y planificación.

Iterators — adaptadores
let nums = vec![1, 2, 3, 4, 5, 6, 7, 8];

// map / filter:
let dobles: Vec<_> = nums.iter().map(|&x| x * 2).collect();
let pares: Vec<_> = nums.iter().filter(|&&x| x % 2 == 0).collect();

// filter_map (Option):
let validos: Vec<_> = datos.iter()
    .filter_map(|s| s.parse::<i32>().ok()).collect();

// flat_map / zip / chain:
let flat: Vec<_> = palabras.iter().flat_map(|s| s.chars()).collect();
let pares: Vec<_> = a.iter().zip(b.iter()).collect();
let all: Vec<_> = v1.iter().chain(v2.iter()).collect();

// take / skip:
nums.iter().take(3);
nums.iter().skip(2);

Los adaptadores son lazy — no se ejecutan hasta ser consumidos. El map transforma, el filter selecciona, el filter_map combina ambos para Option. El flat_map aplana resultados, el zip empareja y el chain concatena. Zero-cost.

Slices y windows
let datos = vec![1, 2, 3, 4, 5, 6, 7, 8];

// Slices:
let slice = &datos[2..5]; // [3, 4, 5]

// chunks:
for chunk in datos.chunks(3) {
    println!("{:?}", chunk); // [1,2,3] [4,5,6] [7,8]
}

// windows (sliding):
for w in datos.windows(3) {
    println!("{:?}", w); // [1,2,3] [2,3,4] ...
}

// split por predicado:
let v = vec![1, 0, 2, 0, 3];
for parte in v.split(|&x| x == 0) {
    println!("{:?}", parte);
}

Los slices (&vec[2..5]) son vistas zero-copy. El chunks() divide en bloques de tamaño fijo (el último puede ser menor); el windows() crea ventanas deslizantes superpuestas (útil para medias móviles). El split() divide por un predicado.

Iteradores personalizados
struct Fibonacci { a: u64, b: u64 }

impl Iterator for Fibonacci {
    type Item = u64;
    fn next(&mut self) -> Option<u64> {
        let proximo = self.a + self.b;
        self.a = self.b;
        self.b = proximo;
        Some(self.a)
    }
}

let fibs = Fibonacci { a: 0, b: 1 };
let primeros: Vec<_> = fibs.take(10).collect();

Implementa el trait Iterator (con type Item y next()) para crear iteradores personalizados. Ganas gratis todos los adaptadores (map, filter, take...). Devuelve None para terminar (o usa iteradores infinitos).

Iterators — consumo
let nums = vec![5, 2, 8, 1, 9, 3];

// collect / fold:
let v: Vec<_> = nums.iter().collect();
let suma = nums.iter().fold(0, |acc, &x| acc + x);
let suma2: i32 = nums.iter().sum();

// Búsqueda:
nums.iter().find(|&&x| x > 5);     // Option
nums.iter().position(|&x| x == 8); // Option<usize>
nums.iter().any(|&x| x > 8);       // bool
nums.iter().all(|&x| x > 0);       // bool

// Extremos y partition:
nums.iter().min();  nums.iter().max();
let (pares, impares): (Vec<_>, Vec<_>) =
    nums.iter().partition(|&&x| x % 2 == 0);

Los consumidores terminan la cadena y producen un resultado. El collect() convierte a Vec, HashMap, String, etc. El fold es el reduce general. find/position devuelven Option. El partition divide en dos grupos en una sola pasada.

Ordenación
let mut v = vec![3, 1, 4, 1, 5, 9];

// Orden natural:
v.sort();              // [1,1,3,4,5,9]

// Descendente:
v.sort_by(|a, b| b.cmp(a));

// Por clave:
let mut personas = vec![ana, rui, eva];
personas.sort_by_key(|p| p.edad);

// Más rápido (no estable):
v.sort_unstable();

// Eliminar duplicados consecutivos:
v.sort();
v.dedup();

El sort() es estable (preserva el orden de iguales); el sort_unstable() es más rápido pero no lo preserva. El sort_by_key() ordena por una clave extraída. El dedup() elimina duplicados consecutivos — ordena antes para eliminarlos todos.

Erros e Result


9 cards
Result y ?
use std::num::ParseIntError;

fn leer_numero(s: &str) -> Result<i32, ParseIntError> {
    let n = s.trim().parse::<i32>()?;
    Ok(n * 2)
}

// ? propaga el error (early return Err)
fn pipeline(s: &str) -> Result<i32, ParseIntError> {
    let a = leer_numero(s)?;
    let b = leer_numero("10")?;
    Ok(a + b)
}

Rust no tiene excepciones — usa Result<T, E> para errores recuperables. El operador ? propaga el error automáticamente (early return con Err) o extrae el valor en caso de Ok. Hace el código de errores limpio y explícito.

unwrap_or y combinators
// Defaults:
let n = "abc".parse::<i32>().unwrap_or(0);
let n = "abc".parse().unwrap_or_else(|_| 0);

// Encadenar operaciones:
let resultado = "42".parse::<i32>()
    .map(|n| n * 2)
    .map(|n| n + 1)
    .unwrap_or(0);   // 85

// and_then (operaciones que devuelven Option):
let n = Some("42")
    .and_then(|s| s.parse::<i32>().ok())
    .map(|n| n * 2);

Los combinators encadenan operaciones sin match verboso. El map transforma el valor, el and_then encadena operaciones que también devuelven Option/Result. El unwrap_or_else calcula el default de forma perezosa solo si es necesario.

Recuperación de errores
use std::fs::{self, File};
use std::io::ErrorKind;

// match en el tipo de error:
let f = match File::open("datos.txt") {
    Ok(f) => f,
    Err(e) if e.kind() == ErrorKind::NotFound => {
        File::create("datos.txt").unwrap()
    }
    Err(e) => panic!("error: {e}"),
};

// or_else (lazy):
let f = File::open("datos.txt")
    .or_else(|e| File::create("datos.txt"));

Para recuperarte de errores, usa match inspeccionando el ErrorKind (ej: NotFound) y toma una acción alternativa (como crear el fichero). El or_else ofrece una alternativa perezosa en caso de Err, manteniendo el encadenamiento funcional.

Option
// Option: el valor puede no existir
fn encontrar(v: &[i32], objetivo: i32) -> Option<usize> {
    v.iter().position(|&x| x == objetivo)
}

match encontrar(&datos, 5) {
    Some(i) => println!("Índice {i}"),
    None => println!("No encontrado"),
}

// Métodos útiles:
valor.unwrap_or(0);
valor.unwrap_or_else(|| calcular());
valor.map(|x| x * 2);
valor.is_some();  valor.is_none();

Option<T> (Some/None) sustituye al null — el compilador obliga a tratar la ausencia. El unwrap_or da un default, el map transforma el valor presente. Métodos como is_some()/is_none() verifican el estado.

anyhow
// anyhow = errores dinámicos (fácil)
use anyhow::{Result, Context};

fn leer_config() -> Result<String> {
    let s = std::fs::read_to_string("config.toml")
        .context("fallo al leer config")?;
    Ok(s)
}

fn main() -> Result<()> {
    let cfg = leer_config()?;
    println!("{cfg}");
    Ok(())
}

El crate anyhow proporciona un tipo de error dinámico (anyhow::Result) ideal para aplicaciones — acepta cualquier error vía ?. El .context() añade mensajes al error. Úsalo en binarios; en bibliotecas prefiere errores tipados.

Custom errors
#[derive(Debug)]
enum AppError {
    NotFound(String),
    InvalidInput(String),
    Io(std::io::Error),
}

// Conversión automática (para el ?):
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError::Io(e)
    }
}

fn leer() -> Result<String, AppError> {
    let s = std::fs::read_to_string("f.txt")?;
    Ok(s)
}

Define los errores como enums con variantes para cada caso. Implementa From<E> para conversión automática — así el ? convierte errores de bibliotecas en tu tipo. Implementa Display y Error para integración con el ecosistema.

thiserror
// thiserror = errores tipados (bibliotecas)
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("no encontrado: {0}")]
    NotFound(String),

    #[error("error de IO")]
    Io(#[from] std::io::Error),
}

fn buscar(id: u32) -> Result<(), AppError> {
    Err(AppError::NotFound(format!("item {id}")))
}

El crate thiserror genera las implementaciones de Error y Display automáticamente vía derive. El #[from] crea la conversión From para el ?. Es la elección estándar para bibliotecas que exponen errores tipados a los consumidores.

panic y unwrap
// panic! (error irrecuperable):
panic!("estado imposible");

// unwrap (panic si None/Err):
let n = "42".parse::<i32>().unwrap();
let v = Some(5).unwrap();

// expect (panic con mensaje):
let n = "42".parse::<i32>()
    .expect("debería ser número");

// Índices fuera del vector = panic:
let v = vec![1, 2, 3];
// let x = v[10]; // panic!
let x = v.get(10); // None (seguro)

panic! es para errores irrecuperables (aborta la thread). El unwrap() hace panic si es None/Err — úsalo solo en prototipos o cuando tengas certeza. El expect() es igual pero con mensaje descriptivo. Prefiere get() a la indexación directa.

main con Result
use std::fs;

// main puede devolver Result:
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let contenido = fs::read_to_string("datos.txt")?;
    println!("{contenido}");
    Ok(())
}

// Si hay error, lo imprime y sale con código 1
// Equivalente a try! en cada línea

La función main puede devolver Result — si es Err, el programa imprime el error y termina con código 1. Esto permite usar ? en el tope del programa sin match manual. Usa Box<dyn Error> para aceptar cualquier error.

Controlo de Fluxo


10 cards
If / Else
// if es una expresión (devuelve valor):
let x = if condicion { 10 } else { 20 };

// Cadena if/else if/else:
let msg = if nota >= 18 {
    "Excelente"
} else if nota >= 10 {
    "Aprobado"
} else {
    "Suspendido"
};

// Sin paréntesis en la condición:
if x > 0 {
    println!("Positivo");
}

En Rust el if es una expresión que devuelve un valor — puede asignarse directamente, eliminando el operador ternario. No usa paréntesis en la condición. Todas las ramas deben devolver el mismo tipo.

Loops
// loop (infinito, devuelve valor):
let r = loop {
    contador += 1;
    if contador == 10 {
        break contador * 2;
    }
};

// while:
let mut n = 5;
while n > 0 {
    n -= 1;
}

// for (iterador):
for i in 0..5 {
    println!("{i}");
}

// enumerate:
for (i, v) in lista.iter().enumerate() {
    println!("{i}: {v}");
}

Rust tiene tres loops: loop (infinito, puede devolver valor vía break), while (condición) y for (consume iteradores — el más común). El for nunca tiene off-by-one. El enumerate() da índice y valor.

Ranges
// Range exclusivo:
for i in 0..5 { }   // 0,1,2,3,4

// Range inclusivo:
for i in 0..=5 { }  // 0,1,2,3,4,5

// En slices:
let v = vec![1, 2, 3, 4, 5];
let medio = &v[1..4];  // [2,3,4]
let fin   = &v[3..];   // [4,5]

// Como iterador:
let suma: i32 = (1..=100).sum();  // 5050
let pares: Vec<_> = (0..10)
    .step_by(2).collect(); // [0,2,4,6,8]

Los ranges 0..5 (exclusivo) y 0..=5 (inclusivo) son iteradores lazy. Se usan en for, slicing y métodos como sum(). El step_by() define el paso de la iteración.

Match
// Debe ser exhaustivo:
let msg = match numero {
    1 => "uno",
    2 => "dos",
    3..=5 => "tres a cinco",
    _ => "otro",  // wildcard
};

// Destructuring:
match punto {
    (0, 0) => println!("Origen"),
    (x, 0) => println!("Eje X: {x}"),
    (x, y) => println!("({x}, {y})"),
}

// match devuelve valor

El match es el pattern matching de Rust — más potente que switch. El compilador exige exhaustividad (todos los casos o _), previniendo casos olvidados. Hace destructuring automático de tuples, structs y enums.

Labels y control
// Loop labels:
'outer: for i in 0..5 {
    for j in 0..5 {
        if i * j > 12 {
            break 'outer;
        }
        if j % 2 == 0 {
            continue 'outer;
        }
    }
}

// break con valor en loop anidado:
let r = 'calc: loop {
    for x in datos.iter() {
        if *x > 100 { break 'calc *x; }
    }
    break 0;
};

Los labels ('outer:) identifican loops para break/continue en loops anidados — sin labels, solo afecta al loop más interno. Combinado con break con valor, devuelve resultados de loops anidados sin flags booleanas.

Result en match
// Result: Ok(valor) o Err(error)
match "42".parse::<i32>() {
    Ok(n) => println!("Número: {n}"),
    Err(e) => println!("Error: {e}"),
}

// if let con Result:
if let Ok(n) = input.parse::<i32>() {
    println!("Válido: {n}");
}

// unwrap_or (default):
let n = "abc".parse::<i32>().unwrap_or(0);

// map (transforma Ok):
let doble = "21".parse::<i32>()
    .map(|n| n * 2);  // Ok(42)

El Result (Ok/Err) sustituye a las excepciones. El if let Ok trata solo el éxito. El unwrap_or da un valor por defecto en caso de error. El map transforma el valor dentro del Ok sin tocar el error.

Match guards
// Guard con if:
match x {
    n if n < 0 => println!("Negativo"),
    0 => println!("Cero"),
    n => println!("Positivo: {n}"),
}

// Guard con patrones:
match punto {
    (x, y) if x == y => println!("Diagonal"),
    (x, y) => println!("({x}, {y})"),
}

// Bindings:
match valor {
    n @ 1..=10 => println!("Pequeño: {n}"),
    n => println!("Grande: {n}"),
}

Los match guards (if tras el patrón) añaden condiciones arbitrarias a cada brazo. El binding n @ patron da un nombre a la variable mientras prueba el patrón — útil para ranges con nombre.

Destructuring patterns
// Structs:
let Punto { x, y } = punto;
let Punto { x: px, y: py } = punto;
let Punto { x, .. } = punto; // ignora y

// Enums:
match msg {
    Mensaje::Mover { x, y } => { }
    Mensaje::Escribir(s) => { }
    Mensaje::Color(r, g, b) => { }
}

// Referencias y anidados:
let &(a, b) = &(1, 2);
match valor {
    Some(Some(x)) => println!("{x}"),
    _ => {},
}

El destructuring extrae valores de structs y enums directamente. El patrón .. ignora campos no necesarios. Puedes renombrar con x: px. Los patrones anidados (Some(Some(x))) desenvuelven varias capas de Option en una línea.

Option y if let
// Option: Some(valor) o None
match valor {
    Some(v) => println!("Tiene: {v}"),
    None => println!("Vacío"),
}

// if let (solo un patrón):
if let Some(v) = valor {
    println!("Tiene: {v}");
}

// while let:
while let Some(item) = stack.pop() {
    println!("{item}");
}

// let-else (Rust 1.65+):
let Some(v) = valor else {
    return;
};

El Option (Some/None) sustituye al null. El if let es azúcar para un match con un solo patrón. El while let itera hasta que el patrón falle. El let-else extrae el valor o hace early return.

Expresiones y bloques
// Los bloques son expresiones:
let x = {
    let a = 3;
    let b = 4;
    a + b   // sin ; = retorno
};

// if/match/loop como expresión:
let max = if a > b { a } else { b };
let abs = match x {
    n if n < 0 => -n,
    n => n,
};
let val = loop { break 42; };

Rust es expression-based: casi todo devuelve un valor. La última línea de un bloque sin ; es el valor devuelto. Esto elimina variables mutables temporales — se usa if/match/loop como expresión. El ; descarta el valor.

Structs, Enums e Traits


11 cards
Structs
// Named fields:
struct User {
    nombre: String,
    edad: u32,
    activo: bool,
}

let user = User {
    nombre: String::from("Ana"),
    edad: 30,
    activo: true,
};

// Field init shorthand:
fn crear(nombre: String, edad: u32) -> User {
    User { nombre, edad, activo: true }
}

// Struct update:
let u2 = User { nombre: "Ray".into(), ..user };

Los structs son el principal mecanismo de composición de datos (no hay clases). El field init shorthand evita repetición cuando la variable tiene el mismo nombre que el campo. El spread ..user copia los campos no especificados de otra instancia.

Traits
// Definir trait:
trait Resumen {
    fn resumir(&self) -> String;

    // Default method:
    fn preview(&self) -> String {
        format!("{}...", &self.resumir()[..20])
    }
}

// Implementar:
impl Resumen for Articulo {
    fn resumir(&self) -> String {
        format!("{} por {}", self.título, self.autor)
    }
}

// Trait bound:
fn notificar(item: &impl Resumen) {
    println!("{}", item.resumir());
}

Los traits definen comportamiento compartido (como interfaces, pero más potentes). Pueden tener default methods con implementación. Usa impl Trait en parámetros para dispatch estático (coste cero) o dyn Trait para dinámico.

Operator overloading
use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct Punto(f64, f64);

impl Add for Punto {
    type Output = Punto;
    fn add(self, other: Punto) -> Punto {
        Punto(self.0 + other.0, self.1 + other.1)
    }
}

let p = Punto(1.0, 2.0) + Punto(3.0, 4.0);
// Punto(4.0, 6.0)

El operator overloading se hace implementando traits de std::ops (Add, Sub, Mul...). El type Output define el tipo del resultado. Permite usar +, -, * con tipos personalizados de forma natural.

Tuple y unit structs
// Tuple struct:
struct Punto(f64, f64);
let p = Punto(3.0, 4.0);
let x = p.0;

// Unit struct:
struct Marcador;

// Newtype pattern:
struct Metros(f64);
struct Segundos(f64);

fn velocidad(d: Metros, t: Segundos) -> f64 {
    d.0 / t.0
}

Los tuple structs crean tipos distintos con la misma estructura — Punto(f64,f64) es diferente de Color(f64,f64). Los unit structs no tienen datos (útiles como marcadores). El newtype pattern evita confundir tipos con la misma representación.

Trait objects (dyn)
// Colección heterogénea:
let items: Vec<Box<dyn Resumen>> = vec![
    Box::new(articulo),
    Box::new(tweet),
];

for item in &items {
    println!("{}", item.resumir());
}

// Parámetro dinámico:
fn mostrar_todo(lista: &[Box<dyn Resumen>]) {
    for item in lista {
        println!("{}", item.preview());
    }
}

Los trait objects (dyn Trait) permiten polimorfismo dinámico vía vtable — útil para colecciones heterogéneas (Vec<Box<dyn Trait>>). Tienen un pequeño coste en runtime; prefiere generics (impl Trait) cuando sea posible.

Enums como state machines
enum Estado {
    Borrador { contenido: String },
    Revision { contenido: String },
    Publicado { contenido: String },
}

impl Estado {
    fn enviar(self) -> Result<Self, String> {
        match self {
            Estado::Borrador { contenido } =>
                Ok(Estado::Revision { contenido }),
            _ => Err("Solo borradores".into()),
        }
    }
}

El typestate pattern usa enums para modelar máquinas de estados donde las transiciones inválidas son imposibles en compilación. Los métodos consumen self (ownership) y devuelven un nuevo estado — el antiguo deja de existir.

Enums
enum Mensaje {
    Salir,
    Mover { x: i32, y: i32 },
    Escribir(String),
    Color(u8, u8, u8),
}

impl Mensaje {
    fn procesar(&self) {
        match self {
            Mensaje::Salir => println!("Salir"),
            Mensaje::Mover { x, y } => println!("({x},{y})"),
            Mensaje::Escribir(s) => println!("{s}"),
            Mensaje::Color(r, g, b) => println!("#{r:02x}{g:02x}{b:02x}"),
        }
    }
}

Los enums en Rust son sum types (tagged unions) — cada variante puede tener datos diferentes. Combinados con match, garantizan exhaustividad. Option y Result son enums genéricos que hacen irrepresentables los estados inválidos.

Derive
// Derive común:
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Config {
    host: String,
    puerto: u16,
}

// Traits derivables:
// Debug, Clone, Copy, PartialEq, Eq
// PartialOrd, Ord, Hash, Default

// Uso:
let c = Config { host: "localhost".into(), puerto: 8080 };
println!("{:?}", c);   // Debug
let c2 = c.clone();    // Clone
assert_eq!(c, c2);     // PartialEq

El #[derive] genera implementaciones automáticas. Debug permite {:?}, Clone permite .clone(), PartialEq/Eq permiten ==. Copy es para tipos simples copiados por bits. Display no es derivable.

Lifetimes
// Lifetime en referencias:
fn mas_largo<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() { s1 } else { s2 }
}

// En structs:
struct Extracto<'a> {
    texto: &'a str,
}

// 'static:
let s: &'static str = "vive para siempre";

// Elision (no necesita anotar):
fn primero(s: &str) -> &str { &s[..1] }

Los lifetimes ('a) garantizan que las referencias no sobrevivan a los datos que apuntan — previenen dangling pointers sin GC. Las elision rules infieren lifetimes en la mayoría de los casos. Solo anota cuando haya ambigüedad. 'static vive todo el programa.

Generics en structs
struct Punto<T> {
    x: T,
    y: T,
}

impl<T: std::fmt::Display> Punto<T> {
    fn mostrar(&self) {
        println!("({}, {})", self.x, self.y);
    }
}

// Solo para f64:
impl Punto<f64> {
    fn distancia(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

Los structs genéricos funcionan con cualquier tipo — el compilador genera código especializado (monomorfización, coste cero). Puedes restringir con trait bounds en el impl y tener impls específicos para tipos concretos (Punto<f64>).

Associated types
trait Iterador {
    type Item;
    fn proximo(&mut self) -> Option<Self::Item>;
}

struct Contador { actual: u32 }

impl Iterador for Contador {
    type Item = u32;
    fn proximo(&mut self) -> Option<u32> {
        self.actual += 1;
        Some(self.actual)
    }
}

Los associated types (type Item) definen tipos dependientes dentro del trait — más ergonómico que generics extra. Cada implementación fija el tipo concreto. Es el patrón usado por el trait Iterator de la std (Item).

Concorrência e Async


10 cards
Threads
use std::thread;

// Spawn:
let handle = thread::spawn(|| {
    println!("¡En otra thread!");
});
handle.join().unwrap(); // espera

// move (toma ownership):
let datos = vec![1, 2, 3];
let h = thread::spawn(move || {
    println!("{:?}", datos);
});

// Varias threads:
let handles: Vec<_> = (0..5)
    .map(|i| thread::spawn(move || i * 2))
    .collect();

thread::spawn crea una thread del OS y devuelve un JoinHandle. El join() espera a que la thread termine y devuelve el resultado. Usa move para transferir el ownership de los datos a la thread. El compilador rechaza código con potenciales data races.

Atómicos
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;

let contador = Arc::new(AtomicUsize::new(0));

for _ in 0..10 {
    let c = Arc::clone(&contador);
    thread::spawn(move || {
        c.fetch_add(1, Ordering::SeqCst);
    });
}

// Sin Mutex (más ligero):
println!("{}", contador.load(Ordering::SeqCst));

Los tipos atómicos (AtomicUsize, AtomicBool...) permiten operaciones seguras entre threads sin Mutex — más ligeros para contadores y flags. El fetch_add incrementa atómicamente. El Ordering controla la consistencia de memoria.

Thread pool (rayon)
// rayon = paralelismo de datos fácil
use rayon::prelude::*;

let v: Vec<i32> = (0..1_000_000).collect();

// par_iter (paralelo automático):
let suma: i64 = v.par_iter()
    .map(|&x| x as i64 * x as i64)
    .sum();

// Ordenación paralela:
let mut datos = vec![3, 1, 4, 1, 5];
datos.par_sort();

// filter paralelo:
let pares: Vec<_> = v.par_iter()
    .filter(|&&x| x % 2 == 0)
    .collect();

El crate rayon ofrece paralelismo de datos con casi cero esfuerzo — cambia iter() por par_iter() y el trabajo se divide automáticamente entre los núcleos de la CPU. Ideal para CPU-bound (map, filter, sort en grandes colecciones).

Channels (mpsc)
use std::sync::mpsc;
use std::thread;

let (tx, rx) = mpsc::channel();

thread::spawn(move || {
    tx.send("hola").unwrap();
});

let msg = rx.recv().unwrap(); // bloquea
println!("{msg}");

// Múltiples productores:
let tx2 = tx.clone();

// Iterar mensajes:
for msg in rx {
    println!("{msg}");
}

Los channels siguen el principio "comparte memoria comunicando". El mpsc (multiple producer, single consumer) tiene tx (envío) y rx (recepción). El send() mueve el valor (ownership). El recv() bloquea hasta haber mensaje; iterar sobre rx termina cuando todos los tx caen.

Async/Await
// async fn devuelve un Future (lazy):
async fn buscar_datos(url: &str) -> Result<String, reqwest::Error> {
    let body = reqwest::get(url).await?.text().await?;
    Ok(body)
}

// .await ejecuta el future:
async fn procesar() {
    let datos = buscar_datos("https://api.ejemplo.com")
        .await.unwrap();
    println!("{datos}");
}

async fn devuelve un Future (lazy — no se ejecuta hasta .await). La sintaxis parece síncrona pero la ejecución es no bloqueante — ideal para I/O intensivo (HTTP, DB). Necesita un runtime (como tokio) para ejecutar los futures.

Scoped threads
use std::thread;

let datos = vec![1, 2, 3, 4, 5];
let mut resultados = vec![];

// scope permite borrow sin Arc:
thread::scope(|s| {
    for chunk in datos.chunks(2) {
        s.spawn(|| {
            // toma prestado chunk y resultados
            let suma: i32 = chunk.iter().sum();
            println!("suma: {suma}");
        });
    }
}); // todas las threads terminan aquí

thread::scope crea threads que pueden tomar prestados (&) datos del scope envolvente sin Arc ni move — el compilador garantiza que todas terminan antes de que los datos salgan del scope. Más simple y seguro para paralelismo con datos locales.

Arc y Mutex
use std::sync::{Arc, Mutex};
use std::thread;

let contador = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let c = Arc::clone(&contador);
    handles.push(thread::spawn(move || {
        let mut val = c.lock().unwrap();
        *val += 1;
    }));
}

for h in handles { h.join().unwrap(); }
println!("{}", *contador.lock().unwrap()); // 10

Arc (Atomic Rc) comparte datos entre threads; Mutex garantiza acceso exclusivo (una thread cada vez vía lock()). Juntos permiten estado compartido mutable seguro. El lock() devuelve un guard que libera el mutex al salir del scope.

Tokio runtime
// Cargo.toml:
// tokio = { version = "1", features = ["full"] }

#[tokio::main]
async fn main() {
    let resultado = procesar().await;
}

// Concurrencia con join!:
let (a, b) = tokio::join!(
    buscar_datos("url1"),
    buscar_datos("url2")
);

// Spawn de tasks:
let handle = tokio::spawn(async {
    trabajo_pesado().await
});
let r = handle.await.unwrap();

El tokio es el runtime async estándar — gestiona miles de tasks concurrentes en una pool de threads OS. El join! ejecuta futures en paralelo y espera a todos. El tokio::spawn crea tasks independientes (más ligeras que threads). El atributo #[tokio::main] configura el runtime.

Send y Sync
// Send: puede moverse entre threads
// Sync: puede compartirse (&T) entre threads

// La mayoría de los tipos es Send + Sync:
// i32, String, Vec<T>, Arc<T>

// Rc NO es Send ni Sync:
// let r = Rc::new(5);
// thread::spawn(move || r); // ERROR

// Mutex<T> es Sync, RefCell NO:
// (Mutex verifica en runtime, RefCell no es thread-safe)

// El compilador verifica automáticamente

Send marca tipos que pueden moverse entre threads; Sync los que pueden compartirse por referencia. La mayoría es ambos. Rc no es ninguno (usa Arc); RefCell no es Sync (usa Mutex). El compilador lo verifica todo automáticamente.

Async concurrente
use futures::future::join_all;

// Varias tasks en paralelo:
async fn buscar_todos(urls: Vec<String>) -> Vec<String> {
    let futures = urls.iter()
        .map(|u| buscar_datos(u));
    join_all(futures).await
        .into_iter()
        .filter_map(|r| r.ok())
        .collect()
}

// select! (primero en terminar):
tokio::select! {
    r = operacion_a() => println!("A: {r}"),
    r = operacion_b() => println!("B: {r}"),
}

El join_all ejecuta varios futures de forma concurrente y espera a todos. El select! espera al primero en terminar (carrera) — útil para timeouts y cancelación. Este enfoque escala a miles de operaciones I/O sin crear threads OS por cada una.

Cargo e Ferramentas


10 cards
Cargo
# Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

# Comandos:
cargo new proyecto     # crear
cargo build            # compilar
cargo run              # compilar + ejecutar
cargo test             # tests
cargo add serde        # añadir dep
cargo build --release  # optimizado

Cargo es el build system y gestor de paquetes — compila, gestiona dependencias, ejecuta tests y publica crates. Las dependencias van en el Cargo.toml. El --release activa optimizaciones. El cargo add añade dependencias automáticamente.

Tests
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sumar() {
        assert_eq!(sumar(2, 3), 5);
    }

    #[test]
    #[should_panic(expected = "error")]
    fn test_falla() {
        panic!("error esperado");
    }

    #[test]
    fn test_result() -> Result<(), String> {
        if 2 + 2 == 4 { Ok(()) }
        else { Err("matemática falló".into()) }
    }
}

Los tests van en #[cfg(test)] (solo compilados en test). El #[test] marca funciones de test. assert_eq!/assert! verifican condiciones. #[should_panic] testea panics esperados. Los tests pueden devolver Result para usar ?.

Features y perfiles
# Cargo.toml - features opcionales:
[features]
default = ["json"]
json = ["dep:serde_json"]
full = ["json", "yaml"]

# Uso en el código:
#[cfg(feature = "json")]
fn parse_json() { }

# Perfiles de build:
[profile.release]
opt-level = 3
lto = true
strip = true

Las features son funcionalidades opcionales activables (--features full) — permiten dependencias condicionales. Los perfiles controlan la optimización: dev (rápido de compilar) y release (optimizado). opt-level, lto y strip afinan el binario final.

Módulos
// src/main.rs o src/lib.rs
mod utils {
    pub fn helper() { }
    pub(crate) fn interno() { }
    fn privado() { }   // solo en este módulo
}

use utils::helper;

// Ficheros:
// src/utils.rs       -> mod utils
// src/utils/mod.rs   -> alternativa
// src/utils/db.rs    -> mod utils { mod db }

// Re-export:
pub use utils::helper;

Los módulos organizan el código y controlan la visibilidad. Todo es privado por defecto — pub expone, pub(crate) limita al crate. Los módulos mapean a ficheros: mod utils búsqueda src/utils.rs. El use importa rutas al scope.

Documentación
/// Suma dos números.
///
/// # Examples
/// ```
/// let r = sumar(2, 3);
/// assert_eq!(r, 5);
/// ```
pub fn sumar(a: i32, b: i32) -> i32 {
    a + b
}

//! Documentación del módulo/crate
//! (en el tope del fichero)

// cargo doc --open  (genera HTML)

Los doc comments (///) documentan ítems y soportan Markdown. Los bloques ``` son tests ejecutables (cargo test los ejecuta). //! documenta el módulo/crate. cargo doc --open genera la documentación HTML (como docs.rs).

Crates esenciales
# Web:
axum / actix-web   # frameworks web
reqwest            # cliente HTTP
serde / serde_json # serialización

# Async y datos:
tokio              # runtime async
sqlx / diesel      # bases de datos
rayon              # paralelismo

# Calidad:
anyhow / thiserror # errores
clap               # CLI args
tracing            # logging

El ecosistema Rust (crates.io) tiene bibliotecas maduras: serde para serialización, tokio para async, reqwest para HTTP, axum/actix-web para servidores. anyhow/thiserror para errores y clap para CLIs.

Visibilidad
pub struct Config {
    pub host: String,    // público
    pub(crate) puerto: u16, // solo en el crate
    token: String,       // privado
}

pub mod api {
    pub(super) fn interno() { } // módulo padre
    pub fn externo() { }
}

// Enum: las variantes siguen la visibilidad del enum
pub enum Estado {
    Activo,    // público
    Inactivo,  // público
}

La visibilidad es opt-in: pub (público), pub(crate) (dentro del crate), pub(super) (módulo padre) o privado (sin modificador). Los campos de struct son privados por defecto. Las variantes de enum siguen la visibilidad del propio enum.

Clippy y fmt
# Clippy (linter oficial):
cargo clippy
cargo clippy --fix   # corrige automáticamente

# Ejemplo de aviso:
# if x == true  ->  if x
# vec![].len() == 0  ->  is_empty()

# rustfmt (formateo):
cargo fmt            # formatea todo
cargo fmt --check    # solo verifica (CI)

# rustfmt.toml (config):
# max_width = 100
# tab_spaces = 4

El cargo clippy es el linter oficial con cientos de reglas que detectan código no idiomático y bugs potenciales. El cargo fmt formatea automáticamente según el estilo estándar de la comunidad. Ambos deben ejecutarse en el CI para consistencia.

Macros declarativas
// macro_rules! (pattern matching en tokens):
macro_rules! calcular {
    ($a:expr, +, $b:expr) => { $a + $b };
    ($a:expr, *, $b:expr) => { $a * $b };
}
let r = calcular!(3, +, 4); // 7

// Repetición:
macro_rules! vec_strs {
    ($($x:expr),*) => {
        vec![$($x.to_string()),*]
    };
}
let v = vec_strs!["a", "b", "c"];

macro_rules! hace pattern matching en tokens y expande a código en compilación. Los patrones $x:expr capturan expresiones; $($x),* repite cero o más veces. Las macros evitan repetición manteniendo type-safety (a diferencia de las macros de C).

Atributos
// Condicionales de compilación:
#[cfg(target_os = "windows")]
fn ruta() -> &str { "C:\\" }

#[cfg(feature = "extra")]
fn recurso_extra() { }

// Suprimir avisos:
#[allow(unused_variables)]
let x = 5;

#![allow(dead_code)]  // crate entero

// Derive:
#[derive(Debug, Clone)]
struct Punto { x: f64, y: f64 }

Los atributos (#[...]) dan metadatos al compilador. #[cfg] compila condicionalmente (por SO, feature...). #[allow] suprime avisos específicos. #[derive] genera implementaciones de traits. #![...] se aplica al ítem envolvente (ej: crate).