Guía de Rust: De 0 a 100

Guía de Rust: De 0 a 100

Seguridad de memoria sin recolector de basura. Concurrencia sin miedo. El lenguaje de sistemas del futuro, desde los fundamentos hasta producción.

FECHA:
ACTUALIZADO:
AUTOR:Jorge Beneyto Castelló
LECTURA:81 MINUTOS DE LECTURA

Guía de Rust: De 0 a 100

¿Qué es Rust?

Rust es un lenguaje compilado, tipado estáticamente, enfocado en seguridad de memoria sin GC, concurrencia sin carreras de datos y rendimiento C/C++. Creado por Graydon Hoare en 2006 en Mozilla Research, lanzado públicamente en 2010, estable desde 2015.

¿Qué lo hace único?

  • Ownership: el compilador verifica en compile-time que no haya use-after-free, double free o data races. Sin GC ni gestión manual.
  • Zero-cost abstractions: las abstracciones se compilan a código máquina tan eficiente como el escrito a mano.
  • Fearless concurrency: el borrow checker previene data races en compilación.
  • FFI sin runtime: integrable en cualquier runtime (embebido, kernel, WASM).

¿Dónde se usa?

  • Sistemas: Linux kernel (Rust-for-Linux 6.1+), Fuchsia OS
  • Navegadores: Firefox (Stylo, WebRender), Servo
  • Cloud: AWS (Firecracker VMM, Lambda runtime), Cloudflare (Pingora), Dropbox (filesystem)
  • CLI/DevTools: ripgrep, fd, bat, delta, zoxide, alacritty, starship
  • WebAssembly: Figma, Shopify, Atlassian
  • Blockchain: Solana, Polkadot, Near
  • Embebido: Tock OS, Embassy, ESP-RS

¿Quién lo usa? Microsoft (Azure IoT), Google (Android/Fuchsia), Meta, Amazon, Discord, Figma.

Fuente: Rust官方, Rust Blog, Rust in Android


Prerrequisitos

Antes de empezar, necesitas:

  • Conocimientos básicos de terminal: navegar directorios, ejecutar comandos, usar un editor de texto
  • El lenguaje/herramienta instalado en tu sistema (sigue la sección de Instalación si aún no lo tienes)

Si no cumples algún requisito, no te preocupes: cada sección te guiará paso a paso.

Instalación

rustup (todas las plataformas)

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
rustc --version    # rustc 1.85.0 (2026-02-20)
cargo --version

Esto instala: rustup (gestor de versiones), rustc (compilador), cargo (gestor de paquetes/build).

Windows

  • Ejecuta rustup-init.exe desde rustup.rs
  • Necesita Visual Studio Build Tools con workload “Desktop C++” (o MSYS2/MinGW)
  • Alternativa: winget install Rustlang.Rustup

macOS

xcode-select --install  # Xcode CLT necesario para linker
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Linux (Debian/Ubuntu)

curl ... | sh
sudo apt install build-essential pkg-config libssl-dev

Fedora: sudo dnf install gcc pkg-config openssl-devel

rustup avanzado

rustup update                     # Actualizar
rustup install nightly            # Canal nightly
rustup target add wasm32-unknown-unknown  # Cross-compile WASM
rustup component add rust-src     # Para rust-analyzer
rustup component add rustfmt clippy
rustup show                       # Toolchains instaladas
rustup default nightly            # Cambiar default

Docker multi-stage

FROM rust:1.85-slim-bookworm AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release 2>/dev/null || true
COPY src/ src/
RUN cargo build --release

FROM debian:bookworm-slim
COPY --from=builder /app/target/release/mi-app /usr/local/bin/
CMD ["mi-app"]

Alias útiles

alias rlb='cargo build --release'
alias rchk='cargo check'
alias rtest='cargo test'
alias rfmt='cargo fmt'
alias rclp='cargo clippy'
alias rrun='cargo run --'
alias radd='cargo add'
alias rdoc='cargo doc --open'
alias rclean='cargo clean'
alias rtree='cargo tree'
alias rbench='cargo bench'
alias ro='cargo outdated'
alias rup='cargo upgrade'

Fuente: rustup.rs, Docker Rust


Escala de aprendizaje 0–100 (7 niveles)

Nivel 0–15: Fundamentos

Conceptos: sintaxis básica, variables, tipos escalares, if/else, loops, funciones, cargo new, cargo run.

Proyecto: calculadora CLI — suma, resta, multiplica, divide con manejo de errores.

use std::io;

fn main() -> Result<(), String> {
    let mut input = String::new();
    println!("Primer número:");
    io::stdin().read_line(&mut input).unwrap();
    let a: f64 = input.trim().parse().map_err(|_| "número inválido")?;

    input.clear();
    println!("Operación (+, -, *, /):");
    io::stdin().read_line(&mut input).unwrap();
    let op = input.trim().to_string();

    input.clear();
    println!("Segundo número:");
    io::stdin().read_line(&mut input).unwrap();
    let b: f64 = input.trim().parse().map_err(|_| "número inválido")?;

    let result = match op.as_str() {
        "+" => a + b,
        "-" => a - b,
        "*" => a * b,
        "/" => if b != 0.0 { a / b } else { return Err("división por cero".into()) },
        _ => return Err("operación inválida".into()),
    };
    println!("Resultado: {result}");
    Ok(())
}

Nivel 15–30: Structs, enums, traits

Conceptos: structs, enums, pattern matching, traits, Option/Result, Vec, String.

Proyecto: gestor de tareas (TODO list) en memoria con persistencia JSON.

Nivel 30–45: Smart pointers e iterators

Conceptos: Box, Rc, Arc, closures, iterators (map, filter, fold), colecciones (HashMap, HashSet).

Proyecto: procesador de logs — lee archivo, parsea líneas, cuenta ocurrencias por nivel (INFO, WARN, ERROR), genera reporte.

Nivel 45–60: Testing, documentación y módulos

Conceptos: tests unitarios/de integración, doc tests, módulos, visibilidad, clippy, rustfmt.

Proyecto: librería de validación — funciones validadoras con tests exhaustivos, documentación, CI configurado.

Nivel 60–75: Web APIs asíncronas

Conceptos: async/await, Tokio, Axum/Actix, serialización JSON, SQLx/Diesel, WebSockets.

Proyecto: API REST de URLs acortadas — endpoints CRUD, BD PostgreSQL, tests de integración, Docker.

Nivel 75–90: Producción

Conceptos: release profiles (LTO, codegen-units), Docker multi-stage, CI/CD (GitHub Actions), logging (tracing), benchmarks (criterion), profiling (perf, flamegraph).

Proyecto: el proyecto del nivel anterior con CI/CD, Docker, benchmarks, profiling y monitoreo.

Nivel 90–100: Sistemas y arquitectura

Conceptos: unsafe, FFI, no_std, WASM, Pin, async traits, arquitectura hexagonal, patrones avanzados, sistemas distribuidos.

Proyecto: intérprete de un lenguaje pequeño (AST, lexer, parser, evaluador) — compila a WASM.


Primeros pasos y configuración del entorno

Si prefieres ver directamente el “Hola Mundo” en Rust, consulta El Atlas del Hola Mundo.

Crear proyecto

cargo new mi-proyecto
cd mi-proyecto

Estructura:

mi-proyecto/
├── Cargo.toml    # Manifiesto
└── src/
    └── main.rs   # Punto de entrada

Cargo.toml

[package]
name = "mi-proyecto"
version = "0.1.0"
edition = "2021"          # 2015, 2018, 2021, 2024

[dependencies]
# serde = { version = "1", features = ["derive"] }
# tokio = { version = "1", features = ["full"] }

[profile.release]
opt-level = 3
lto = true
codegen-units = 1
strip = true

Compilar y ejecutar

cargo build           # Debug
cargo build --release # Optimizado
cargo run             # Compila + ejecuta
cargo check           # Solo verifica tipos (más rápido)
cargo run -- arg1 arg2

rustfmt

rustup component add rustfmt
cargo fmt               # Formatea
cargo fmt --check       # Verifica (CI)

Config rustfmt.toml:

max_width = 100
tab_spaces = 4
edition = "2021"

Clippy

rustup component add clippy
cargo clippy                       # Lints
cargo clippy -- -W clippy::pedantic  # Pedante
cargo clippy --fix                 # Auto-fix

Editores recomendados

Los IDEs y editores más usados son VSCode, JetBrains y Neovim. Extensiones y configuraciones recomendadas.

Ediciones de Rust

EditionRustNovedades
20151.0Original
20181.31impl Trait, dyn Trait, NLL, ?, async/await, módulos modernos
20211.56IntoIterator for arrays, TryInto closures, bindings_after_at
20241.85impl Trait everywhere, unsafe extern blocks, cargo script, gen blocks, RPITIT

Migración: cargo fix --edition

Fuente: The Rust Book — Getting Started, Edition Guide


Paradigmas de programación

Rust es un lenguaje multi-paradigma con énfasis en funcional y sistémico. Puedes mezclar paradigmas según el problema.

ParadigmaSoporte en Rust
ImperativoStatements, loops, mutabilidad explícita
FuncionalInmutabilidad, iterators, closures, pattern matching, Option/Result
POOStructs + impl, traits, composición, encapsulación
ConcurrenteOwnership + Send/Sync, canales, Arc<Mutex>
SistémicoSin GC, control de memoria, zero-cost, FFI, no_std

Imperativo

Rust tiene statements y expressions. Casi todo es una expresión (retorna un valor):

let x = if cond { 10 } else { 20 };  // if es expresión
let y = loop { break 42; };          // loop retorna valor
let z = match n {
    1 => "uno",
    _ => "otro",
};

Funcional

Rust adopta varios conceptos funcionales:

  • Inmutabilidad por defecto: let crea variables inmutables. Solo let mut permite mutación.
  • Iterators: lazy, zero-cost, chainable (map, filter, fold, flat_map)
  • Closures: |args| expr, capturan entorno por referencia/move, traits Fn/FnMut/FnOnce
  • Pattern matching exhaustivo: match obliga a cubrir todos los casos
  • Option/Result como mónadas: map, and_then, or_else, unwrap_or_else permiten encadenar sin anidar
let pares: Vec<i32> = (0..10)
    .filter(|x| x % 2 == 0)
    .map(|x| x * 2)
    .collect();

let r = Some(10)
    .map(|x| x * 2)
    .filter(|x| x > 5)
    .and_then(|x| if x < 100 { Some(x) } else { None });

POO parcial

Rust no tiene clases ni herencia, pero ofrece:

  • Structs + impl: datos + métodos (como clases sin herencia)
  • Traits como interfaces: definen comportamiento compartido
  • Composición sobre herencia: agregar campos en lugar de extender
  • No clases: no hay class, extends, super, protected
  • Encapsulación: pub controla visibilidad de campos y métodos
trait Volador { fn volar(&self); }

struct Pajaro { nombre: String }
struct Avion { modelo: String }

impl Volador for Pajaro { fn volar(&self) { println!("{} bate alas", self.nombre); } }
impl Volador for Avion { fn volar(&self) { println!("{} enciende motores", self.modelo); } }

Concurrente

El ownership garantiza thread-safety en compilación:

  • El borrow checker previene data races: no puedes tener &mut simultáneos entre threads
  • Traits Send/Sync: el compilador verifica qué tipos son seguros entre threads
  • Rc<T> no es Send (contador no atómico), Arc<T> sí
  • Mutex<T> protege acceso concurrente con RAII
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 num = c.lock().unwrap();
        *num += 1;
    }));
}
for h in handles { h.join().unwrap(); }
println!("{}", contador.lock().unwrap()); // 10

Sistémico

Rust da control de memoria sin GC:

  • Sin garbage collector: la memoria se libera en compile-time con ownership
  • Zero-cost abstractions: iterators, closures, genéricos se compilan a código tan eficiente como el escrito a mano
  • Control manual: puedes elegir stack vs heap (Box), memoria compartida (Rc/Arc), representaciones con repr(C)
  • FFI sin runtime: integrable en C, Python, JS, WASM, kernel
// Zero-cost: este código se compila a un loop óptimo
let suma: u32 = (0..1000).filter(|x| x % 2 == 0).map(|x| x * x).sum();

Fuente: Rust Book — FP, Rust Book — OOP, Rust Book — Concurrency, Rustonomicon


Tipos de datos y variables

Sistema de tipos

¿Qué es?

Rust tiene tipado estático fuerte con inferencia: el compilador conoce el tipo de cada variable en compile-time y no realiza conversiones implícitas entre tipos incompatibles. Es como un molde rígido: cada pieza encaja exactamente en su lugar.

El sistema de tipos es nominal (los tipos se identifican por su nombre, no por su estructura) y lineal (cada valor tiene exactamente un propietario).

🔗 Wikipedia — Tipado estático, Rust Book — Tipos

Sintaxis básica

let x: i32 = 42;       // Anotación explícita
let y = 42;            // Inferencia: i32 por defecto
let z = 3.14;          // Inferencia: f64 por defecto

// Error: tipos no coinciden
// let suma = "42" + 1;  // ❌ No compila

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_tipado_estatico() {
        let x: i32 = 42;
        assert_eq!(x, 42);
    }

    #[test]
    fn test_inferencia() {
        let x = 42;
        let y = 3.14;
        assert_eq!(std::mem::size_of_val(&x), 4); // i32 = 4 bytes
        assert_eq!(std::mem::size_of_val(&y), 8); // f64 = 8 bytes
    }
}

💡 Memoria y rendimiento

  • Coste cero en runtime: el tipado se resuelve en compilación
  • Los tipos ocupan exactamente el tamaño declarado (sin overhead de cabecera)
  • La inferencia no añade tiempo de compilación significativo

✅ Buenas prácticas

  • ✅ Prefiere inferencia de tipos para código local y legible
  • ✅ Anota tipos explícitamente en APIs públicas y funciones
  • ⚠️ No uses i8/u8 para números pequeños sin necesidad real de ahorro de memoria
  • ❌ No abuses de as para conversiones — prefiere From/TryFrom

🏗️ Metodología

  • En bibliotecas: siempre anota tipos en la interfaz pública
  • En scripts/CLIs: inferencia es aceptable
  • En embebido: usa tipos explícitos para controlar el tamaño exacto

🔗 Para saber más

Escalares

¿Qué es?

Los tipos escalares representan valores simples y únicos. En Rust, todos los escalares tienen tamaño fijo conocido en compilación y viven en el stack.

TipoTamañoRango
i8/u88 bits-128..127 / 0..255
i16/u1616 bits-32768..32767 / 0..65535
i32/u3232 bits-2^31..2^31-1 / 0..2^32-1
i64/u6464 bits-2^63..2^63-1 / 0..2^64-1
i128/u128128 bits-2^127..2^127-1 / 0..2^128-1
isize/usizearq. nativadepende de la arquitectura
f32/f6432/64 bitsIEEE 754
bool8 bitstrue / false
char32 bitsUnicode escalar

🔗 Rust Book — Scalar Types

Sintaxis básica

let decimal = 98_222;
let hex = 0xff;
let octal = 0o77;
let binario = 0b1111_0000;
let byte = b'A';
let flotante = 3.14;
let cientifico = 1.5e10;
let activo: bool = true;
let letra: char = 'ñ';

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_escalares() {
        assert_eq!(98_222, 98222);
        assert_eq!(0xff, 255);
        assert_eq!(1.5e10 as u64, 15_000_000_000);
        assert_eq!(b'A', 65u8);
    }

    #[test]
    fn test_tamanos() {
        assert_eq!(std::mem::size_of::<i32>(), 4);
        assert_eq!(std::mem::size_of::<f64>(), 8);
        assert_eq!(std::mem::size_of::<char>(), 4);
        assert_eq!(std::mem::size_of::<bool>(), 1);
    }
}

💡 Memoria y rendimiento

  • Los escalares viven en el stack: acceso O(1), sin asignación dinámica
  • char ocupa 4 bytes (no 1 como en C) para soportar Unicode completo
  • bool ocupa 1 byte completo (no 1 bit) — es un trade-off de acceso a memoria

✅ Buenas prácticas

  • ✅ Usa i32/f64 por defecto como tipos escalares principales
  • ✅ Usa usize para índices de colecciones y tamaños
  • ⚠️ isize/usize varían según plataforma — no los serialices sin anotar el tamaño
  • ❌ No uses as para truncar valores sin verificar el rango

🏗️ Metodología

  • Por defecto: i32 para enteros, f64 para flotantes
  • Rendimiento extremo: usa i8-i64 según el rango real esperado
  • Embebido: prefiere u8/i16 para ahorrar registros y memoria

🔗 Para saber más

Compuestos

¿Qué es?

Los tipos compuestos agrupan múltiples valores en una sola entidad. Rust ofrece tuplas (producto de tipos) y arrays (colección homogénea de tamaño fijo). Ambos tienen tamaño conocido en compilación y viven en el stack.

🔗 Rust Book — Compound Types

Sintaxis básica

// Tupla: grupo heterogéneo de tamaño fijo
let persona: (&str, u8, bool) = ("Alice", 30, true);
let (nombre, edad, activo) = persona; // destructuring
println!("{}", persona.0); // acceso por índice

// Array: grupo homogéneo de tamaño fijo
let numeros: [i32; 5] = [1, 2, 3, 4, 5];
let ceros = [0; 100]; // [0, 0, 0, ..., 0]
println!("{}", numeros[2]); // acceso por índice

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_tuplas() {
        let t = (42, "hola");
        assert_eq!(t.0, 42);
        assert_eq!(t.1, "hola");

        let (a, b) = t;
        assert_eq!(a, 42);
    }

    #[test]
    fn test_arrays() {
        let arr = [1, 2, 3];
        assert_eq!(arr.len(), 3);
        assert_eq!(arr[1], 2);
    }

    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn test_array_out_of_bounds() {
        let arr = [1, 2, 3];
        let _ = arr[10]; // panic en runtime
    }
}

💡 Memoria y rendimiento

  • Tuplas y arrays viven en el stack: acceso directo, cero overhead
  • El tamaño es parte del tipo: [i32; 5] y [i32; 6] son tipos distintos
  • Arrays se almacenan contiguamente en memoria (cache-friendly)
  • Tuplas pueden tener padding por alineación entre campos

✅ Buenas prácticas

  • ✅ Usa arrays para colecciones de tamaño conocido en compilación
  • ✅ Usa tuplas para agrupar valores sin nombre (retorno múltiple)
  • ⚠️ Para colecciones dinámicas, prefiere Vec<T> sobre arrays
  • ❌ No accedas a arrays con índices sin verificar el límite

🏗️ Metodología

  • Tuplas: útiles para retornar múltiples valores sin crear un struct
  • Arrays: usa para buffers de tamaño fijo, lookup tables, datos embebidos
  • Cuando el tamaño es dinámico, refactoriza a Vec<T>

🔗 Para saber más

Structs

¿Qué es?

Un struct es un tipo compuesto con campos nombrados. Es la unidad fundamental de organización de datos en Rust, equivalente a las clases en otros lenguajes pero sin herencia.

🔗 Rust Book — Structs

Sintaxis básica

struct Usuario {
    nombre: String,
    email: String,
    activo: bool,
    edad: u8,
}

// Instanciación
let u = Usuario {
    nombre: String::from("Alice"),
    email: String::from("alice@example.com"),
    activo: true,
    edad: 30,
};

// Tuple struct (campos sin nombre)
struct Color(u8, u8, u8);
let rojo = Color(255, 0, 0);

// Unit struct (sin campos)
struct Marker;

// Spread syntax
let u2 = Usuario { nombre: String::from("Bob"), ..u };

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    struct Punto { x: i32, y: i32 }

    #[test]
    fn test_struct() {
        let p = Punto { x: 10, y: 20 };
        assert_eq!(p.x, 10);
        assert_eq!(p.y, 20);
    }

    #[test]
    fn test_mutabilidad() {
        let mut p = Punto { x: 0, y: 0 };
        p.x = 5;
        assert_eq!(p.x, 5);
    }
}

💡 Memoria y rendimiento

  • Los structs se almacenan en stack por defecto, sin overhead
  • El tamaño es la suma de sus campos más padding por alineación
  • #[repr(C)] cambia el layout para FFI (compatible con C)
  • #[repr(packed)] elimina padding pero penaliza acceso

✅ Buenas prácticas

  • ✅ Usa impl blocks para asociar métodos al struct
  • ✅ Implementa Display y Debug para logging y depuración
  • ⚠️ No expongas campos privados sin getters si necesitas invariantes
  • ❌ No abuses de pub en campos — prefiere métodos de acceso

🏗️ Metodología

  • Structs simples: datos planos sin validación (DTOs)
  • Structs con lógica: usa constructores (new()) que validen invariantes
  • Para configuración: usa el builder pattern con structs

🔗 Para saber más

Enums

¿Qué es?

Los enum en Rust son algebraicos: cada variante puede llevar datos asociados. Son la herramienta principal para modelar estados y opciones, y combinados con match proporcionan exhaustividad en compilación.

🔗 Rust Book — Enums

Sintaxis básica

enum Estado {
    Activo,
    Inactivo,
    Pendiente(String), // con datos
}

enum Mensaje {
    Texto(String),
    Coordenadas { x: f64, y: f64 }, // struct-like
    Cerrar,
}

// Uso
let msg = Mensaje::Texto(String::from("hola"));
match msg {
    Mensaje::Texto(s) => println!("{}", s),
    Mensaje::Coordenadas { x, y } => println!("{},{}", x, y),
    Mensaje::Cerrar => println!("adios"),
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    enum Resultado { Exito(i32), Error(String) }

    #[test]
    fn test_enum() {
        let r = Resultado::Exito(42);
        match r {
            Resultado::Exito(v) => assert_eq!(v, 42),
            _ => panic!("esperaba éxito"),
        }
    }

    #[test]
    fn test_option() {
        let x: Option<i32> = Some(10);
        assert_eq!(x.unwrap(), 10);
        assert_eq!(x.unwrap_or(0), 10);
        assert_eq!(None::<i32>.unwrap_or(0), 0);
    }
}

💡 Memoria y rendimiento

  • El tamaño de un enum es el de su variante más grande + discriminant (tag)
  • Option<&T> tiene tamaño 0 overhead (optimización de puntero nulo)
  • El discriminant se almacena como un entero pequeño (1-4 bytes normalmente)
  • Pattern matching en enum es tan rápido como un switch en C

✅ Buenas prácticas

  • ✅ Usa Option<T> en lugar de null pointers
  • ✅ Usa Result<T, E> para operaciones que pueden fallar
  • ⚠️ No uses unwrap() en producción — prefiere match o ?
  • ❌ No crees enums con variantes que deberían ser structs separados

🏗️ Metodología

  • Enums pequeños: modelan estados finitos (Activo/Inactivo/Pendiente)
  • Enums grandes: pueden representar ASTs, comandos, protocolos
  • Combina enums con traits para comportamientos polimórficos sin herencia

🔗 Para saber más

String vs &str

¿Qué es?

Rust tiene dos tipos principales de cadenas: String (propietaria, heap, mutable) y &str (prestada, vista de una cadena). Es una distinción fundamental para entender la gestión de memoria en Rust.

🔗 Rust Book — Strings

Sintaxis básica

let s: &str = "Hola";       // string literal (hardcodeado en binario)
let owned: String = "Hola".to_string();
let from_str: String = String::from("Hola");

// Conversiones
let vista: &str = &owned;           // String → &str
let otra_owned: String = vista.to_string(); // &str → String
let slice: &str = &owned[0..4];     // slicing (cuidado con bytes vs chars)

// Iteración segura
for c in "ñññ".chars() {
    println!("{c}");
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_string_types() {
        let s: &str = "hello";
        let owned: String = s.to_string();
        assert_eq!(owned, "hello");
        assert_eq!(&owned[..], s);
    }

    #[test]
    fn test_string_concatenacion() {
        let mut s = String::from("hola ");
        s.push_str("mundo");
        s.push('!');
        assert_eq!(s, "hola mundo!");
    }

    #[test]
    fn test_chars_vs_bytes() {
        assert_eq!("ñ".len(), 2);   // bytes
        assert_eq!("ñ".chars().count(), 1); // caracteres
    }
}

💡 Memoria y rendimiento

  • &str apunta a una secuencia contigua de bytes UTF-8 (len, capacidad, datos)
  • &str puede estar en stack (vista), heap (vista de String), o data segment (literales)
  • String tiene 3 palabras: puntero, longitud, capacidad (24 bytes en 64-bit)
  • Slicing con índices byte puede romper caracteres multi-byte — usa chars() para safe iteration

✅ Buenas prácticas

  • ✅ Usa &str para parámetros de función (más flexible: acepta String y &str)
  • ✅ Usa String cuando necesites propietario y mutabilidad
  • ⚠️ No indices bytes directamente en UTF-8 — prefiere métodos de char/str
  • ❌ No uses String donde baste &str — es un 50% más grande en memoria

🏗️ Metodología

  • APIs públicas: parámetros &str, retorno String
  • Procesamiento de texto: String para construcción, &str para análisis
  • Constantes y literales: siempre &str (vive en el binario)

🔗 Para saber más

Vec<T> vs &[T]

¿Qué es?

Vec<T> es un array dinámico propietario (heap, redimensionable). &[T] es una vista prestada de una secuencia contigua de elementos. La relación es análoga a String vs &str.

🔗 Rust Book — Vectors

Sintaxis básica

let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);

let v2 = vec![1, 2, 3]; // macro

// Slice (vista prestada)
let slice: &[i32] = &v[0..2];
let slice_completo: &[i32] = &v;

// Iteración
for x in &v { println!("{x}"); }
for x in v.iter() { println!("{x}"); }

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_vec() {
        let mut v = vec![1, 2, 3];
        v.push(4);
        assert_eq!(v, vec![1, 2, 3, 4]);
        assert_eq!(v.pop(), Some(4));
    }

    #[test]
    fn test_slice() {
        let v = vec![1, 2, 3, 4, 5];
        let s = &v[1..4];
        assert_eq!(s, &[2, 3, 4]);
    }

    #[test]
    fn test_vec_capacity() {
        let mut v = Vec::with_capacity(10);
        assert!(v.capacity() >= 10);
        v.push(1);
        assert_eq!(v.len(), 1);
    }
}

💡 Memoria y rendimiento

  • Vec<T> tiene 3 palabras (puntero, len, capacity) — 24 bytes overhead
  • El crecimiento es exponencial (factor ~1.5-2) para amortiguar reallocaciones
  • &[T] es 2 palabras (puntero, len) — 16 bytes, no tiene capacidad
  • Acceso por índice es O(1) en ambos

✅ Buenas prácticas

  • ✅ Usa Vec::with_capacity(n) cuando sepas el tamaño aproximado
  • ✅ Prefiere &[T] en parámetros de función sobre &Vec<T>
  • ⚠️ Redimensionar un Vec muy grande puede ser costoso
  • ❌ No retornes referencias a datos locales de la función

🏗️ Metodología

  • Vec<T>: colección dinámica principal, casi siempre la elección correcta
  • &[T]: lectura de secuencias sin ownership
  • Arrays [T; N]: cuando el tamaño se conoce en compilación

🔗 Para saber más

Mutabilidad: const vs let vs let mut

¿Qué es?

Rust distingue tres niveles de inmutabilidad: const (constante de compilación), let (variable inmutable), let mut (variable mutable). Es una elección deliberada: la inmutabilidad por defecto evita efectos secundarios no deseados.

🔗 Rust Book — Variables

Sintaxis básica

const MAX_PUNTOS: u32 = 100_000;  // Compile-time, tipo obligatorio
let x = 5;                         // Inmutable: no se puede reasignar
let mut y = 5;                     // Mutable
y = 10;                            // ✓ permitido
// x = 10;                         // ❌ error: variable inmutable

// Shadowing: re-declarar con mismo nombre
let nombre = "  Hola  ";
let nombre = nombre.trim(); // nuevo binding

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    const CONST_VAL: i32 = 42;

    #[test]
    fn test_const() {
        assert_eq!(CONST_VAL, 42);
    }

    #[test]
    fn test_inmutabilidad() {
        let x = 5;
        // x = 10; // no compilaría
        assert_eq!(x, 5);
    }

    #[test]
    fn test_mutabilidad() {
        let mut x = 5;
        x += 1;
        assert_eq!(x, 6);
    }

    #[test]
    fn test_shadowing() {
        let x = 5;
        let x = x * 2; // shadow
        assert_eq!(x, 10);
    }
}

💡 Memoria y rendimiento

  • const se inlinea en el punto de uso: cero overhead en runtime
  • let vs let mut no tiene diferencia de rendimiento: la mutabilidad es una restricción del compilador
  • El shadowing permite reusar el nombre sin mutar la variable original

✅ Buenas prácticas

  • ✅ Usa const para valores mágicos y configuración en compilación
  • ✅ Prefiere let (inmutable) por defecto; solo usa let mut cuando sea necesario
  • ⚠️ No abuses del shadowing: puede confundir en scopes grandes
  • ❌ No uses mutabilidad global (static mut) sin unsafe

🏗️ Metodología

  • const: valores fijos conocidos en compilación
  • let: variables que no cambian tras la inicialización (preferido)
  • let mut: estado local que evoluciona (contadores, acumuladores)

🔗 Para saber más


Control de flujo y modularidad

Condicionales: if/else (expresión)

¿Qué es?

En Rust, if/else no es un statement — es una expresión que retorna un valor. Esto permite asignar el resultado de un condicional directamente a una variable. Es una de las influencias funcionales del lenguaje.

🔗 Rust Book — if Expressions

Sintaxis básica

let x = 5;
let resultado = if x > 0 { "positivo" } else { "no positivo" };

// if/else if/else como expresión
let categoria = if x > 0 {
    "positivo"
} else if x < 0 {
    "negativo"
} else {
    "cero"
};

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_if_expresion() {
        let cond = true;
        let val = if cond { 10 } else { 20 };
        assert_eq!(val, 10);
    }

    #[test]
    fn test_if_encadenado() {
        let x = 0;
        let res = if x > 0 { "pos" } else if x < 0 { "neg" } else { "cero" };
        assert_eq!(res, "cero");
    }
}

💡 Memoria y rendimiento

  • Las expresiones if se compilan a saltos condicionales como en C
  • No hay overhead: el compilador elige la rama en runtime con una instrucción cmp+jmp
  • Ambas ramas deben ser del mismo tipo (el compilador lo exige)

✅ Buenas prácticas

  • ✅ Usa if como expresión cuando el resultado se asigna directamente
  • ✅ Prefiere match para 3+ ramas sobre if/else if/else
  • ⚠️ No anides if dentro de if — refactoriza a match o guard clauses
  • ❌ No uses if sin else cuando se asigna el resultado

🏗️ Metodología

  • 2 ramas: if/else
  • 3+ ramas: match
  • Validación previa: guard clauses con if cond { return } al inicio

🔗 Para saber más

Loops: loop, while, for

¿Qué es?

Rust ofrece tres tipos de bucles: loop (infinito con break), while (condicional), for (iteración sobre colecciones). loop es único de Rust porque puede retornar un valor con break.

🔗 Rust Book — Loops

Sintaxis básica

// loop: infinito hasta break, puede retornar valor
let contador = loop {
    break 42;
};

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

// for: iteración idiomática
let v = vec![10, 20, 30];
for elemento in &v {
    println!("{elemento}");
}

// Rango
for i in 0..5 { println!("{i}"); }       // 0..4
for i in 0..=5 { println!("{i}"); }      // 0..5

// Enumerate
for (i, val) in v.iter().enumerate() {
    println!("v[{i}] = {val}");
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_loop_retorna_valor() {
        let r = loop { break 42; };
        assert_eq!(r, 42);
    }

    #[test]
    fn test_for_range() {
        let mut suma = 0;
        for i in 0..=5 { suma += i; }
        assert_eq!(suma, 15); // 0+1+2+3+4+5
    }

    #[test]
    fn test_for_iter() {
        let v = vec![1, 2, 3];
        let mut suma = 0;
        for x in &v { suma += x; }
        assert_eq!(suma, 6);
    }

    #[test]
    fn test_break_label() {
        let mut count = 0;
        'outer: loop {
            loop {
                count += 1;
                if count == 3 { break 'outer; }
            }
        }
        assert_eq!(count, 3);
    }
}

💡 Memoria y rendimiento

  • for sobre iteradores se compila a código tan eficiente como while manual (zero-cost)
  • Los iteradores son lazy: no crean colecciones intermedias
  • loop con break retornando valor no tiene overhead extra

✅ Buenas prácticas

  • ✅ Usa for por defecto para iterar colecciones — es más idiomático
  • ✅ Usa while para bucles con condición mutable
  • ✅ Usa loop cuando necesites retornar un valor con break
  • ⚠️ No uses while con índices cuando for con iterador es más claro
  • ❌ No abuses de break con labels — suele indicar que el bucle hace demasiadas cosas

🏗️ Metodología

  • for i in iter: 90% de los bucles
  • while cond: bucles con condición mutable no ligada a una colección
  • loop: drivers, event loops, servidores

🔗 Para saber más

Match (exhaustivo obligatorio)

¿Qué es?

match es la herramienta de control de flujo más poderosa de Rust. Compara un valor contra una serie de patrones y debe cubrir todos los casos posibles (exhaustividad). El compilador rechazará cualquier match no exhaustivo.

🔗 Rust Book — Match

Sintaxis básica

let numero = 2;
let texto = match numero {
    1 => "uno",
    2 => "dos",
    3 => "tres",
    _ => "otro", // comodín: cubre el resto
};

// Con enum
enum Color { Rojo, Verde, Azul }
let c = Color::Verde;
let nombre = match c {
    Color::Rojo => "rojo",
    Color::Verde => "verde",
    Color::Azul => "azul",
    // No necesitamos _ porque cubrimos todas las variantes
};

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_match_exhaustivo() {
        let x = 3;
        let res = match x {
            1 => "uno",
            2 => "dos",
            _ => "muchos",
        };
        assert_eq!(res, "muchos");
    }

    #[test]
    fn test_match_option() {
        let val = Some(42);
        let result = match val {
            Some(v) => v * 2,
            None => 0,
        };
        assert_eq!(result, 84);
    }
}

💡 Memoria y rendimiento

  • match se compila a tabla de saltos o comparaciones encadenadas, como un switch en C
  • El compilador optimiza el orden de las ramas para minimizar comparaciones
  • En enums con discriminant pequeño, el salto es O(1) (tabla)

✅ Buenas prácticas

  • ✅ Usa match para 3+ ramas en lugar de if/else if
  • ✅ Usa _ como comodín para el caso por defecto
  • ⚠️ El orden importa: las ramas se evalúan de arriba abajo
  • ❌ No uses match cuando if let es más conciso (un solo patrón)

🏗️ Metodología

  • Match exhaustivo: obligatorio para enums (el compilador exige cubrir todo)
  • Match con wildcard: útil cuando solo importan algunos casos específicos
  • Match con guards: añade condiciones adicionales a los patrones

🔗 Para saber más

if let / while let

¿Qué es?

if let y while let son azúcar sintáctico para match cuando solo te interesa un patrón. Son ideales para trabajar con Option y Result sin el boilerplate del match completo.

🔗 Rust Book — if let

Sintaxis básica

let valor = Some(42);

// En lugar de:
match valor {
    Some(v) => println!("{v}"),
    _ => {},
}

// if let simplifica:
if let Some(v) = valor {
    println!("{v}");
}

// while let: extraer mientras el patrón coincida
let mut pila = vec![1, 2, 3];
while let Some(tope) = pila.pop() {
    println!("{tope}");
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_if_let_option() {
        let val = Some(42);
        let mut result = 0;
        if let Some(v) = val {
            result = v;
        }
        assert_eq!(result, 42);
    }

    #[test]
    fn test_while_let() {
        let mut v = vec![1, 2, 3];
        let mut sum = 0;
        while let Some(x) = v.pop() {
            sum += x;
        }
        assert_eq!(sum, 6);
    }
}

💡 Memoria y rendimiento

  • if let se compila exactamente igual que un match con un solo brazo
  • No hay penalización por usar el azúcar sintáctico
  • while let tiene el mismo rendimiento que loop { match } { break }

✅ Buenas prácticas

  • ✅ Usa if let cuando solo te interese un patrón
  • ✅ Usa while let para bucles que extraen de un Option/Iterator
  • ⚠️ Si necesitas también el caso else, usa if let ... else
  • ❌ No uses if let cuando necesites cubrir múltiples patrones — usa match

🏗️ Metodología

  • if let Some(x): la forma idiomática de trabajar con Option
  • if let Ok(x): la forma idiomática de trabajar con Result (cuando no importa el error)
  • while let Some(x): extraer elementos de un iterador sin for

🔗 Para saber más

Módulos y visibilidad

¿Qué es?

El sistema de módulos de Rust organiza el código en namespaces y controla la visibilidad con pub. Cada archivo es un módulo, y los directorios pueden ser módulos con mod.rs o nombre.rs.

🔗 Rust Book — Modules

Sintaxis básica

// src/main.rs
mod utils; // busca src/utils.rs

fn main() {
    utils::saludar();
}

// src/utils.rs
pub fn saludar() {
    println!("Hola!");
}

// Visibilidad
pub fn publica() {}        // accesible fuera del módulo
fn privada() {}            // solo dentro del módulo
pub(crate) fn interna() {} // dentro del crate
pub(super) fn padre() {}   // solo en el módulo padre

🧪 Cómo probarlo

// src/lib.rs
pub mod math {
    pub fn suma(a: i32, b: i32) -> i32 { a + b }
    fn privada() {} // no accesible fuera
}

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

    #[test]
    fn test_modulo() {
        assert_eq!(math::suma(2, 2), 4);
    }
}

💡 Memoria y rendimiento

  • Los módulos son puramente en tiempo de compilación: cero coste en runtime
  • No afectan al layout de memoria ni al rendimiento
  • El compilador inlinea funciones a través de fronteras de módulos si es beneficioso

✅ Buenas prácticas

  • ✅ Usa pub(crate) para APIs internas del crate
  • ✅ Organiza módulos por funcionalidad, no por tipo (ej: mod database mejor que mod structs)
  • ⚠️ No expongas campos privados de structs — usa métodos getter
  • ❌ No crees módulos con una sola función — agrupa por dominio

🏗️ Metodología

  • Proyecto pequeño (< 5 archivos): módulos planos en src/
  • Proyecto mediano: módulos anidados por funcionalidad
  • Proyecto grande: subcrates o workspaces

🔗 Para saber más

Cargo.toml y workspaces

¿Qué es?

Cargo.toml es el manifiesto del proyecto. Los workspaces permiten gestionar múltiples crates relacionados en un solo repositorio, compartiendo dependencias y lockfile.

🔗 Cargo Book — Workspaces

Sintaxis básica

[package]
name = "mi-proyecto"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }

Workspace:

# Cargo.toml (raíz)
[workspace]
members = ["crates/*"]
resolver = "2"

🧪 Cómo probarlo

cargo check                      # Verifica todo el workspace
cargo test -p nombre-del-crate   # Testea un crate específico
cargo build --workspace          # Compila todo

💡 Memoria y rendimiento

  • Cargo compila en paralelo dentro de un workspace
  • Las dependencias comunes se comparten entre crates
  • target/ se comparte en el workspace (un solo directorio de build)

✅ Buenas prácticas

  • ✅ Usa workspaces para separar librería, binarios y tests
  • ✅ Usa [profile.release] con lto = true y codegen-units = 1 para optimizar
  • ⚠️ No pongas todo en un solo crate — separa por responsabilidad
  • ❌ No abuses de workspaces para proyectos pequeños (añade complejidad)

🏗️ Metodología

  • Proyecto pequeño: un solo crate
  • Librería + binario: workspace con crates/ separados
  • Monorepo: workspace con múltiples paquetes inter-dependientes

🔗 Para saber más


Sistema de archivos (File System I/O)

std::fs (lectura/escritura básica)

¿Qué es?

std::fs es el módulo estándar para operaciones con el sistema de archivos: leer, escribir, crear, borrar archivos y directorios. Toda la E/S en Rust es bloqueante por defecto y devuelve Result para manejar errores.

🔗 Rust std::fs docs

Sintaxis básica

use std::fs;
use std::io;

fn main() -> io::Result<()> {
    // Leer archivo completo
    let contenido = fs::read_to_string("datos.txt")?;
    println!("{}", contenido);

    // Escribir archivo (sobrescribe)
    fs::write("salida.txt", "Hola mundo")?;

    // Leer binario
    let bytes = fs::read("imagen.png")?;

    // Crear directorio
    fs::create_dir("nueva_carpeta")?;
    // create_dir_all crea toda la jerarquía
    fs::create_dir_all("a/b/c")?;

    // Listar directorio
    for entrada in fs::read_dir(".")? {
        let entrada = entrada?;
        println!("{}", entrada.path().display());
    }

    Ok(())
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    use std::fs;
    use std::io;

    #[test]
    fn test_write_and_read() -> io::Result<()> {
        let path = "test_archivo.txt";
        fs::write(path, "test contenido")?;
        let contenido = fs::read_to_string(path)?;
        assert_eq!(contenido, "test contenido");
        fs::remove_file(path)?;
        Ok(())
    }

    #[test]
    fn test_create_and_remove_dir() -> io::Result<()> {
        fs::create_dir("test_dir")?;
        assert!(fs::metadata("test_dir")?.is_dir());
        fs::remove_dir("test_dir")?;
        Ok(())
    }
}

💡 Memoria y rendimiento

  • read_to_string carga todo el archivo en memoria — no apto para archivos grandes
  • read carga como Vec<u8>: el archivo completo en heap
  • Para archivos grandes, usa BufReader/BufWriter (lectura/escritura por fragmentos)
  • Las operaciones de FS son lentas (orden de ms) — considéralas I/O bound

✅ Buenas prácticas

  • ✅ Usa read_to_string solo para archivos pequeños (config, logs pequeños)
  • ✅ Usa write para sobrescritura completa de archivos pequeños
  • ⚠️ Siempre maneja io::Result — no asumas que un archivo existe
  • ❌ No uses read_to_string para archivos de más de unos pocos MB

🏗️ Metodología

  • Archivos pequeños (< 1 MB): read_to_string / write
  • Archivos grandes: BufReader + BufWriter
  • Directorios: read_dir con filter/map para procesar

🔗 Para saber más

BufReader / BufWriter

¿Qué es?

BufReader y BufWriter añaden un buffer interno para reducir las llamadas al sistema (syscalls). En lugar de leer byte a byte, leen en bloques (por defecto 8KB). Esencial para procesar archivos grandes de forma eficiente.

🔗 Rust std::io::BufReader

Sintaxis básica

use std::fs::File;
use std::io::{BufRead, BufReader, Write, BufWriter};

// Leer línea por línea (eficiente)
let archivo = File::open("datos.txt")?;
let reader = BufReader::new(archivo);
for linea in reader.lines() {
    let linea = linea?;
    println!("{}", linea);
}

// Escribir con buffer
let archivo = File::create("salida.txt")?;
let mut writer = BufWriter::new(archivo);
writeln!(writer, "Hola mundo")?;

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    use std::fs::File;
    use std::io::{BufRead, BufReader, Write, BufWriter};

    #[test]
    fn test_bufreader() -> std::io::Result<()> {
        let file = File::open("Cargo.toml")?;
        let reader = BufReader::new(file);
        let mut lineas = 0;
        for _ in reader.lines() { lineas += 1; }
        assert!(lineas > 0);
        Ok(())
    }

    #[test]
    fn test_bufwriter() -> std::io::Result<()> {
        let file = File::create("test_buf.txt")?;
        let mut writer = BufWriter::new(file);
        writeln!(writer, "línea 1")?;
        writeln!(writer, "línea 2")?;
        writer.flush()?;
        Ok(())
    }
}

💡 Memoria y rendimiento

  • BufReader reduce las syscalls de N a N/buffer_size (8KB por defecto)
  • Sin buffer: cada read() es una syscall (~1µs de overhead)
  • Con buffer: se leen 8KB de una vez, se sirven desde memoria
  • BufWriter hace lo mismo en escritura: acumula hasta 8KB antes de escribir

✅ Buenas prácticas

  • ✅ Usa BufReader para archivos de más de unos pocos KB
  • ✅ Usa read_until o lines para procesamiento línea a línea
  • ⚠️ flush() o el Drop de BufWriter escribe el buffer restante
  • ❌ No envuelvas un archivo pequeño en BufReader si solo haces una lectura

🏗️ Metodología

  • BufReader: archivos de texto grandes (logs, CSVs, archivos de datos)
  • BufWriter: archivos de salida grandes (resultados, exportaciones)
  • Sin buffer: lecturas de una sola vez con read_to_string (archivos pequeños)

🔗 Para saber más

std::path::Path y PathBuf

¿Qué es?

Path es una slice de un path de archivo (como &str para strings). PathBuf es la versión propietaria (como String). Proporcionan métodos multiplataforma para manipular rutas.

🔗 Rust std::path docs

Sintaxis básica

use std::path::{Path, PathBuf};

// PathBuf (propietario, mutable)
let mut ruta = PathBuf::from("/usr/local/bin");
ruta.push("mi-app");
assert_eq!(ruta, Path::new("/usr/local/bin/mi-app"));

// Path (vista prestada)
let p = Path::new("/home/user/documentos/archivo.txt");
println!("padre: {:?}", p.parent());
println!("nombre: {:?}", p.file_name());
println!("extensión: {:?}", p.extension());
println!("sin extensión: {:?}", p.file_stem());

// Métodos útiles
assert!(p.is_absolute());
assert!(!p.is_relative());
assert_eq!(p.to_str(), Some("/home/user/documentos/archivo.txt"));

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    use std::path::{Path, PathBuf};

    #[test]
    fn test_path_manipulation() {
        let mut pb = PathBuf::from("/a");
        pb.push("b");
        pb.push("c.txt");
        assert_eq!(pb, Path::new("/a/b/c.txt"));
    }

    #[test]
    fn test_path_components() {
        let p = Path::new("/usr/local/bin");
        assert_eq!(p.parent(), Some(Path::new("/usr/local")));
        assert_eq!(p.file_name(), Some(std::ffi::OsStr::new("bin")));
    }

    #[test]
    fn test_path_extension() {
        let p = Path::new("datos.csv");
        assert_eq!(p.extension(), Some(std::ffi::OsStr::new("csv")));
        assert_eq!(p.file_stem(), Some(std::ffi::OsStr::new("datos")));
    }
}

💡 Memoria y rendimiento

  • Path es una slice (2 palabras: puntero + len)
  • PathBuf es 3 palabras (puntero, len, capacity) como String
  • Las operaciones de path no acceden al disco — solo manipulan strings
  • Usa PathBuf::with_capacity si sabes la longitud máxima

✅ Buenas prácticas

  • ✅ Usa Path en parámetros de función (más flexible que &str para rutas)
  • ✅ Usa PathBuf para construir y almacenar rutas
  • ⚠️ Usa to_str() en lugar de to_string_lossy() solo si estás seguro de que es UTF-8 válido
  • ❌ No concatenes rutas con formato de strings — usa push o join

🏗️ Metodología

  • Path: referencia a una ruta (parámetros de funciones)
  • PathBuf: almacenar y construir rutas
  • join vs push: join crea un PathBuf nuevo, push modifica in-place

🔗 Para saber más

Procesar CSV y escribir JSON

¿Qué es?

El ecosistema Rust tiene crates especializados para formatos de datos. csv para CSV y serde_json para JSON son los estándares de la industria. Combinados con serde permiten serialización/deserialización automática.

🔗 serde.rs, csv crate

Sintaxis básica

use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::BufReader;

#[derive(Debug, Serialize, Deserialize)]
struct Usuario {
    nombre: String,
    edad: u8,
    email: String,
}

// Leer CSV
let archivo = File::open("usuarios.csv")?;
let reader = BufReader::new(archivo);
let mut csv_reader = csv::Reader::from_reader(reader);
for result in csv_reader.deserialize() {
    let usuario: Usuario = result?;
    println!("{:?}", usuario);
}

// Escribir JSON
let usuarios = vec![
    Usuario { nombre: "Alice".into(), edad: 30, email: "alice@ex.com".into() },
];
let json = serde_json::to_string_pretty(&usuarios)?;
fs::write("usuarios.json", json)?;

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    use serde::{Deserialize, Serialize};

    #[derive(Serialize, Deserialize, PartialEq, Debug)]
    struct Punto { x: i32, y: i32 }

    #[test]
    fn test_json_serialize() {
        let p = Punto { x: 1, y: 2 };
        let json = serde_json::to_string(&p).unwrap();
        assert_eq!(json, r#"{"x":1,"y":2}"#);
    }

    #[test]
    fn test_json_deserialize() {
        let json = r#"{"x":3,"y":4}"#;
        let p: Punto = serde_json::from_str(json).unwrap();
        assert_eq!(p, Punto { x: 3, y: 4 });
    }
}

💡 Memoria y rendimiento

  • serde_json serializa/deserializa en memoria: O(n) en el tamaño de los datos
  • csv::Reader es streaming: no carga todo el archivo en memoria
  • serde usa derive macros que se ejecutan en compilación (zero-cost)
  • Para archivos enormes, usa csv + serde en modo streaming

✅ Buenas prácticas

  • ✅ Añade serde con cargo add serde --features derive
  • ✅ Usa #[serde(rename_all = "snake_case")] para convenciones
  • ⚠️ No serialices tipos con referencias internas — serde necesita owned data
  • ❌ No uses serde_json::from_str en archivos enormes — prefiere from_reader

🏗️ Metodología

  • CSV: datos tabulares grandes, logs, exportaciones de bases de datos
  • JSON: APIs, configuración, intercambio de datos entre servicios
  • Para rendimiento extremo: considera bincode, msgpack, o protobuf

🔗 Para saber más


Algoritmos y estructuras de datos

Ordenación

¿Qué es?

Rust proporciona ordenación estable (sort) e inestable (sort_unstable) en slices. La ordenación por defecto usa el orden natural del tipo (a través del trait Ord), y se puede personalizar con sort_by.

🔗 Rust std::slice::sort

Sintaxis básica

let mut nums = vec![3, 1, 4, 1, 5];
nums.sort();
assert_eq!(nums, vec![1, 1, 3, 4, 5]);

let mut floats = vec![3.2, 1.5, 4.1];
floats.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());

// Ordenación personalizada
let mut items = vec![(3, "c"), (1, "a"), (2, "b")];
items.sort_by(|a, b| a.0.cmp(&b.0));

// sort por campo
items.sort_by_key(|k| k.0);

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_sort() {
        let mut v = vec![3, 1, 4, 1, 5];
        v.sort();
        assert_eq!(v, vec![1, 1, 3, 4, 5]);
    }

    #[test]
    fn test_sort_reverse() {
        let mut v = vec![1, 2, 3];
        v.sort_by(|a, b| b.cmp(a));
        assert_eq!(v, vec![3, 2, 1]);
    }

    #[test]
    fn test_sort_by_key() {
        let mut v = [(3, "c"), (1, "a"), (2, "b")];
        v.sort_by_key(|k| k.0);
        assert_eq!(v, [(1, "a"), (2, "b"), (3, "c")]);
    }
}

💡 Memoria y rendimiento

  • sort_unstable es más rápido pero no preserva el orden de elementos iguales
  • sort (estable) usa merge sort optimizado: O(n log n) peor caso, O(n) mejor caso
  • sort_unstable usa pattern-defeating quicksort: O(n log n) promedio
  • sort_unstable no asigna memoria extra; sort puede asignar un buffer temporal

✅ Buenas prácticas

  • ✅ Usa sort_unstable por defecto — es más rápido y no asigna memoria
  • ✅ Usa sort solo cuando necesites estabilidad (orden relativo preservado)
  • ⚠️ f32/f64 no implementan Ord (NaN no es ordenable) — usa sort_by con partial_cmp
  • ❌ No implementes tu propia ordenación a menos que tengas datos muy concretos

🏗️ Metodología

  • sort: estabilidad necesaria (ej: ordenar por fecha, luego por nombre)
  • sort_unstable: rendimiento crítico, estabilidad no necesaria
  • sort_by_key: ordenar por un campo calculado sin crear struct temporal

🔗 Para saber más

Búsqueda

¿Qué es?

Rust ofrece búsqueda binaria en slices ordenados con binary_search, y búsqueda lineal con los iteradores find y position. La búsqueda binaria es O(log n) pero requiere datos ordenados.

🔗 Rust std::slice::binary_search

Sintaxis básica

let nums = vec![1, 3, 5, 7, 9];

// Búsqueda binaria (O(log n))
match nums.binary_search(&5) {
    Ok(i) => println!("encontrado en índice {i}"),
    Err(i) => println!("debería estar en índice {i}"),
}

// Búsqueda lineal con iteradores
let encontrado = nums.iter().find(|&&x| x == 5);
let posicion = nums.iter().position(|&x| x == 5);

// Búsqueda en vectores no ordenados
let items = vec!["a", "b", "c"];
assert!(items.contains(&"b"));

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_binary_search() {
        let v = vec![1, 3, 5, 7, 9];
        assert_eq!(v.binary_search(&5), Ok(2));
        assert_eq!(v.binary_search(&4), Err(2));
    }

    #[test]
    fn test_find() {
        let v = vec![10, 20, 30, 40];
        let result: Option<&i32> = v.iter().find(|&&x| x > 25);
        assert_eq!(result, Some(&30));
    }

    #[test]
    fn test_contains() {
        let v = vec!["manzana", "pera", "uva"];
        assert!(v.contains(&"pera"));
        assert!(!v.contains(&"platano"));
    }
}

💡 Memoria y rendimiento

  • binary_search: O(log n), requiere datos ordenados, no asigna memoria
  • find/position: O(n), early exit al encontrar el primer match
  • contains: O(n) en slices/vec (lineal), O(1) en HashSet
  • Para búsquedas frecuentes, considera convertir a HashSet o BTreeSet

✅ Buenas prácticas

  • ✅ Usa binary_search en datos ordenados (más rápido que find)
  • ✅ Usa find para búsquedas con condición compleja
  • ⚠️ binary_search requiere el tipo Ord — no funciona con floats directamente
  • ❌ No conviertas a HashSet para una sola búsqueda — el overhead de hash es mayor

🏗️ Metodología

  • Una búsqueda: find (lineal)
  • Muchas búsquedas en datos estáticos: ordenar + binary_search
  • Muchas búsquedas en datos dinámicos: HashSet / BTreeSet

🔗 Para saber más

Iterator API completa

¿Qué es?

Los iteradores en Rust son lazy, zero-cost y componibles. El compilador optimiza cadenas completas de iteradores a código máquina tan eficiente como un loop manual. La API de Iterator proporciona docenas de métodos adaptadores.

🔗 Rust std::iter::Iterator

Sintaxis básica

let nums = vec![1, 2, 3, 4, 5, 6];

// Chain de iteradores (zero-cost)
let resultado: Vec<i32> = nums.iter()
    .filter(|x| *x % 2 == 0)     // pares: 2, 4, 6
    .map(|x| x * 2)               // doble: 4, 8, 12
    .take(2)                      // solo 2: 4, 8
    .collect();

// fold (reduce)
let suma = nums.iter().fold(0, |acc, x| acc + x);

// any / all
let hay_pares = nums.iter().any(|x| x % 2 == 0);
let todos_pares = nums.iter().all(|x| x % 2 == 0);

// flat_map
let nested = vec![vec![1, 2], vec![3, 4]];
let flat: Vec<i32> = nested.into_iter().flatten().collect();

// chain: concatenar dos iteradores
let a = vec![1, 2];
let b = vec![3, 4];
let ab: Vec<i32> = a.into_iter().chain(b).collect();

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_iterator_chain() {
        let v: Vec<i32> = (1..=5)
            .filter(|x| x % 2 == 0)
            .map(|x| x * 10)
            .collect();
        assert_eq!(v, vec![20, 40]);
    }

    #[test]
    fn test_fold() {
        let sum: i32 = (1..=100).fold(0, |acc, x| acc + x);
        assert_eq!(sum, 5050);
    }

    #[test]
    fn test_any_all() {
        let v = vec![1, 2, 3, 4, 5];
        assert!(v.iter().any(|x| x > 3));
        assert!(!v.iter().all(|x| x > 3));
    }

    #[test]
    fn test_flat_map() {
        let v = vec![vec![1, 2], vec![3, 4]];
        let flat: Vec<i32> = v.into_iter().flatten().collect();
        assert_eq!(flat, vec![1, 2, 3, 4]);
    }
}

💡 Memoria y rendimiento

  • Los adaptadores de iterador son cero coste: el compilador los fusiona en un solo loop
  • collect es el único punto donde se asigna memoria
  • Los iteradores son lazy: map/filter no ejecutan nada hasta que se consume
  • Usa size_hint para que collect pre-asigne capacidad exacta

✅ Buenas prácticas

  • ✅ Prefiere chains de iteradores sobre loops manuales con lógica compleja
  • ✅ Usa collect con tipos concretos (Vec, HashMap, HashSet)
  • ⚠️ No abuses de collect intermedios — rompe la lazy evaluation
  • ❌ No conviertas a Vec solo para iterar — usa el iterador directamente

🏗️ Metodología

  • map/filter: transformaciones de colecciones (90% de los casos)
  • fold/reduce: agregaciones y acumulaciones
  • flat_map/flatten: desanidar estructuras jerárquicas
  • chain: concatenar varias fuentes de datos

🔗 Para saber más

Big O de colecciones estándar

OperaciónVecVecDequeLinkedListHashMapBTreeMapHashSetBTreeSet
AccesoO(1)O(1)O(n)O(1)*O(log n)——
Inserción finalO(1)**O(1)O(1)————
Inserción inicioO(n)O(1)O(1)————
BúsquedaO(n)O(n)O(n)O(1)*O(log n)O(1)*O(log n)
EliminaciónO(n)O(n)O(1)O(1)*O(log n)O(1)*O(log n)
OrdenaciónO(n log n)O(n log n)—————

*Amortizado, asumiendo buen hash function **Amortizado O(1); O(n) cuando hay que redimensionar

🔗 Rust std::collections

✅ Buenas prácticas

  • ✅ Vec: la colección por defecto para secuencias
  • ✅ HashMap: mapeo clave-valor con O(1) promedio
  • ✅ BTreeMap: mapeo ordenado, O(log n) consistente
  • ⚠️ LinkedList casi nunca es necesario en Rust — Vec o VecDeque son mejores
  • ❌ No uses HashMap con claves que no implementan Eq + Hash

🔗 Para saber más


Conceptos clave del lenguaje

Ownership, Borrowing y Lifetimes

¿Qué es?

Ownership es el sistema de gestión de memoria de Rust: cada valor tiene un único propietario, y se libera automáticamente cuando el propietario sale de scope. Borrowing permite prestar una referencia sin transferir la propiedad. Lifetimes son anotaciones que verifican que las referencias vivan lo suficiente.

🔗 Rust Book — Ownership

Sintaxis básica

// Ownership: cada valor tiene un único dueño
let s1 = String::from("hola");
let s2 = s1; // s1 se mueve a s2
// println!("{}", s1); // ❌ error: s1 ya no es válido

// Borrowing: prestar sin mover
let s3 = String::from("mundo");
let len = calcular_largo(&s3); // &s3 presta una referencia
println!("{}", s3); // s3 sigue siendo válido

fn calcular_largo(s: &str) -> usize { s.len() }

// Mutable borrowing: una sola referencia mutable a la vez
let mut s = String::from("hola");
let r1 = &mut s;
// let r2 = &mut s; // ❌ error: dos refs mut simultáneas no permitidas

// Lifetimes: las referencias deben vivir lo suficiente
fn el_mas_largo<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_ownership_move() {
        let s1 = String::from("hola");
        let s2 = s1;
        // s1 ya no es válido aquí
        assert_eq!(s2, "hola");
    }

    #[test]
    fn test_borrowing() {
        let s = String::from("mundo");
        let r = &s;
        assert_eq!(*r, "mundo");
        assert_eq!(s, "mundo"); // s sigue vivo
    }

    #[test]
    fn test_mutable_borrow() {
        let mut s = String::from("hola");
        let r = &mut s;
        r.push_str(" mundo");
        assert_eq!(*r, "hola mundo");
    }

    #[test]
    fn test_lifetime_elision() {
        fn first_word(s: &str) -> &str {
            s.split_whitespace().next().unwrap_or("")
        }
        assert_eq!(first_word("hola mundo"), "hola");
    }
}

💡 Memoria y rendimiento

  • Ownership no tiene overhead en runtime: el compilador inserta free/drop en compile-time
  • Las referencias (&T) son punteros de tamaño palabra (8 bytes en 64-bit)
  • El borrow checker se ejecuta en compilación: cero impacto en rendimiento
  • Los lifetimes son puramente en compilación: no existen en runtime

✅ Buenas prácticas

  • ✅ Prefiere referencias (&T) sobre ownership cuando no necesites propietario
  • ✅ Usa &mut T solo cuando necesites modificar
  • ⚠️ No luches contra el borrow checker — suele indicar un diseño mejorable
  • ❌ No abuses de clone() para evitar problemas de ownership — aprende a prestar

🏗️ Metodología

  • Funciones que solo leen datos: &T (referencia compartida)
  • Funciones que modifican: &mut T (referencia mutable)
  • Funciones que consumen: T (ownership) — el caller pierde acceso
  • Funciones que crean datos: retorna T

🔗 Para saber más

Option y Result

¿Qué es?

Option<T> representa un valor que puede ser Some(T) o None (alternativa a null pointers). Result<T, E> representa una operación que puede ser Ok(T) o Err(E) (alternativa a excepciones). Ambos son enums y el compilador exige que los manejes.

🔗 Rust std::option, Rust std::result

Sintaxis básica

// Option
fn dividir(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

let resultado = dividir(10.0, 2.0);
match resultado {
    Some(v) => println!("{v}"),
    None => println!("error"),
}

// Combinadores
let doblado = dividir(10.0, 2.0).map(|x| x * 2.0);
let con_default = dividir(10.0, 0.0).unwrap_or(0.0);

// Result
fn leer_archivo(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

// ? operator (propagación de errores)
fn procesar() -> Result<(), std::io::Error> {
    let contenido = leer_archivo("datos.txt")?; // Si error, retorna
    println!("{}", contenido);
    Ok(())
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    fn mitad(x: i32) -> Option<i32> {
        if x % 2 == 0 { Some(x / 2) } else { None }
    }

    #[test]
    fn test_option_some() {
        assert_eq!(mitad(4), Some(2));
    }

    #[test]
    fn test_option_none() {
        assert_eq!(mitad(3), None);
    }

    #[test]
    fn test_option_combinators() {
        let r = mitad(10).map(|x| x * 3);
        assert_eq!(r, Some(15));

        let r = mitad(3).unwrap_or(-1);
        assert_eq!(r, -1);
    }

    #[test]
    fn test_result() {
        let ok: Result<i32, &str> = Ok(42);
        assert_eq!(ok.unwrap(), 42);

        let err: Result<i32, &str> = Err("fallo");
        assert_eq!(err.unwrap_or(0), 0);
    }
}

💡 Memoria y rendimiento

  • Option<&T> tiene tamaño 0 overhead (optimización de puntero nulo: niche)
  • Option<Box<T>> también es del tamaño de un puntero (niche optimization)
  • Result<T, E> ocupa tamaño máximo entre T y E + discriminant
  • ? operator se compila a early return condicional — tan eficiente como un goto

✅ Buenas prácticas

  • ✅ Usa Option para valores opcionales (mejor que null)
  • ✅ Usa Result para operaciones que pueden fallar
  • ✅ Usa ? para propagar errores — evita unwrap en producción
  • ⚠️ No uses unwrap() o expect() sin una razón muy justificada
  • ❌ No uses None como Some(error_sin_info) — usa Result con errores concretos

🏗️ Metodología

  • Option -> operaciones que pueden no tener resultado (ej: buscar en colección)
  • Result -> operaciones que pueden fallar con causa (ej: E/S, parseo)
  • ? -> propagación de errores en funciones que retornan Result
  • Combinadores (map, and_then, or_else) -> transformaciones sin match explícito

🔗 Para saber más

Error handling: thiserror y anyhow

¿Qué es?

thiserror permite definir errores personalizados con derive macros. anyhow proporciona un tipo Error dinámico para errores ad-hoc. thiserror es para librerías (errores concretos), anyhow para aplicaciones (errores genéricos).

🔗 thiserror crate, anyhow crate

Sintaxis básica

use thiserror::Error;

#[derive(Error, Debug)]
pub enum MiError {
    #[error("archivo no encontrado: {0}")]
    NoEncontrado(String),
    #[error("error de red: {0}")]
    Red(#[from] std::io::Error),
    #[error("formato inválido en línea {linea}")]
    Formato { linea: usize },
}

// Uso con anyhow (para aplicaciones)
use anyhow::{Result, Context};

fn procesar() -> Result<()> {
    let data = std::fs::read_to_string("datos.txt")
        .context("no se pudo leer el archivo de datos")?;
    // ...
    Ok(())
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    use thiserror::Error;

    #[derive(Error, Debug, PartialEq)]
    enum CalcError {
        #[error("división por cero")]
        DivisionPorCero,
        #[error("desbordamiento")]
        Desbordamiento,
    }

    fn dividir(a: i32, b: i32) -> Result<i32, CalcError> {
        if b == 0 { Err(CalcError::DivisionPorCero) }
        else { Ok(a / b) }
    }

    #[test]
    fn test_error_personalizado() {
        assert_eq!(dividir(10, 0), Err(CalcError::DivisionPorCero));
        assert_eq!(dividir(10, 2), Ok(5));
    }

    #[test]
    fn test_error_mensaje() {
        let err = CalcError::DivisionPorCero;
        assert_eq!(err.to_string(), "división por cero");
    }
}

💡 Memoria y rendimiento

  • thiserror genera tipos concretos en compilación: cero overhead en runtime
  • anyhow::Error es un trait object: asigna en heap y tiene indirección
  • anyhow es ideal para prototipos y aplicaciones; thiserror para librerías
  • La macro #[from] genera impl From automático (sin overhead extra)

✅ Buenas prácticas

  • ✅ Usa thiserror en librerías: errores concretos, documentados, exhaustivos
  • ✅ Usa anyhow en binarios: errores ad-hoc con contexto
  • ⚠️ No mezcles thiserror y anyhow en el mismo módulo sin necesidad
  • ❌ No definas errores genéricos como String — usa enums con variantes específicas

🏗️ Metodología

  • Librerías: thiserror devuelve Result<T, MiError> específico
  • Aplicaciones: anyhow devuelve anyhow::Result<T> genérico
  • La aplicación puede convertir errores de librerías con Context

🔗 Para saber más

Traits, derives, bounds, impl Trait, dyn Trait

¿Qué es?

Los traits son la herramienta de abstracción de Rust (equivalente a interfaces en otros lenguajes). Definen comportamiento compartido. derive genera implementaciones automáticas. impl Trait y dyn Trait ofrecen dos formas de polimorfismo.

🔗 Rust Book — Traits

Sintaxis básica

// Definir un trait
trait Area {
    fn area(&self) -> f64;
}

// Implementar
struct Circulo { radio: f64 }
impl Area for Circulo {
    fn area(&self) -> f64 { std::f64::consts::PI * self.radio * self.radio }
}

// Derive automático
#[derive(Debug, Clone, PartialEq)]
struct Punto { x: i32, y: i32 }

// impl Trait (monomorfización)
fn imprimir_area(forma: &impl Area) {
    println!("{}", forma.area());
}

// Trait bounds con genéricos
fn area_doble<T: Area>(forma: &T) -> f64 {
    forma.area() * 2.0
}

// dyn Trait (trait objects, dispatch dinámico)
fn area_dinamica(forma: &dyn Area) -> f64 {
    forma.area()
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    trait Sumable { fn sumar(&self, other: &Self) -> Self; }

    impl Sumable for i32 {
        fn sumar(&self, other: &i32) -> i32 { self + other }
    }

    fn sumar_gen<T: Sumable>(a: &T, b: &T) -> T {
        a.sumar(b)
    }

    #[test]
    fn test_trait() {
        assert_eq!(sumar_gen(&5, &3), 8);
    }

    #[test]
    fn test_derive_debug() {
        #[derive(Debug)]
        struct X { a: i32 }
        let x = X { a: 42 };
        assert_eq!(format!("{:?}", x), "X { a: 42 }");
    }
}

💡 Memoria y rendimiento

  • impl Trait y genéricos con bounds: monomorfización — código duplicado por tipo, máximo rendimiento
  • dyn Trait: vtable — indirección, un sola implementación, menor rendimiento
  • Los derives se expanden en compile-time: no afectan a runtime
  • Los trait objects tienen tamaño de 2 palabras (data ptr + vtable ptr)

✅ Buenas prácticas

  • ✅ Prefiere genéricos con impl Trait o bounds para máximo rendimiento
  • ✅ Usa dyn Trait cuando necesites heterogeneidad (colecciones de diferentes tipos)
  • ✅ Deriva Debug, Clone, PartialEq siempre que tenga sentido
  • ⚠️ No abuses de dyn Trait en hot paths — la indirección tiene coste
  • ❌ No implementes traits externos para tipos externos (orphan rule)

🏗️ Metodología

  • Genéricos + bounds: APIs genéricas, librerías, hot paths
  • impl Trait: parámetros de función simples
  • dyn Trait: colecciones heterogéneas, plugins, inyección de dependencias

🔗 Para saber más

Smart Pointers: Box, Rc, Arc, Cell, RefCell, Mutex

¿Qué es?

Los smart pointers envuelven valores con comportamiento adicional. Box<T> asigna en heap, Rc<T> permite ownership compartido (single-thread), Arc<T> es thread-safe, Cell/RefCell proporcionan mutabilidad interior.

🔗 Rust Book — Smart Pointers

Sintaxis básica

use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::cell::RefCell;

// Box: heap allocation
let b = Box::new(42);
let lista = Box::new(Lista::Nodo(1, Box::new(Lista::Nodo(2, Box::new(Lista::Fin)))));

// Rc: referencia contada (single-thread)
let a = Rc::new(42);
let b = Rc::clone(&a); // incrementa contador
println!("{}", Rc::strong_count(&a)); // 2

// Arc: referencia contada thread-safe
let a = Arc::new(42);
let b = Arc::clone(&a);

// RefCell: mutabilidad interior en tiempo de ejecución
let celda = RefCell::new(42);
*celda.borrow_mut() = 100;
println!("{}", celda.borrow());

// Mutex: acceso exclusivo entre threads
let m = Mutex::new(42);
{
    let mut dato = m.lock().unwrap();
    *dato = 100;
} // drop libera el lock

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    use std::rc::Rc;
    use std::sync::{Arc, Mutex};

    #[test]
    fn test_box() {
        let b = Box::new(42);
        assert_eq!(*b, 42);
    }

    #[test]
    fn test_rc() {
        let r = Rc::new(5);
        assert_eq!(Rc::strong_count(&r), 1);
        let _r2 = Rc::clone(&r);
        assert_eq!(Rc::strong_count(&r), 2);
    }

    #[test]
    fn test_mutex() {
        let m = Mutex::new(10);
        {
            let mut v = m.lock().unwrap();
            *v += 5;
        }
        assert_eq!(*m.lock().unwrap(), 15);
    }
}

💡 Memoria y rendimiento

  • Box<T>: 1 palabra en stack (puntero), datos en heap. Sin overhead de contador
  • Rc<T>: 2 palabras (puntero + contador). El contador es usize — no atómico
  • Arc<T>: 2 palabras (puntero + contador atómico). Más caro en incrementos/decrementos
  • RefCell<T>: verifica borrows en runtime (no en compilación) — paniquea si violas reglas
  • Mutex<T>: bloquea el hilo si está en uso. Overhead de syscall en contención

✅ Buenas prácticas

  • ✅ Usa Box para tipos de tamaño dinámico (enums recursivos, trait objects)
  • ✅ Usa Rc/Arc para ownership compartido (solo cuando no puedas tener un único owner)
  • ⚠️ RefCell rompe las garantías de borrow checking en compile-time — úsalo con cuidado
  • ❌ No uses Rc cuando necesites threads — usa Arc
  • ❌ No uses Arc en single-thread — Rc es más ligero

🏗️ Metodología

  • Un único propietario: valor directo o Box
  • Múltiples propietarios (single-thread): Rc
  • Múltiples propietarios (multi-thread): Arc
  • Mutabilidad interior: Cell (Copy), RefCell (no-Copy), Mutex (threads)

🔗 Para saber más


POO y patrones de diseño

Structs + impl (composición sobre herencia)

¿Qué es?

Rust no tiene clases ni herencia. En su lugar, usa structs para datos e impl blocks para métodos, con composición como alternativa a la herencia. Los traits proporcionan polimorfismo sin jerarquías de clases.

🔗 Rust Book — OOP

Sintaxis básica

struct Multimedia {
    nombre: String,
    duracion: u32,
}

impl Multimedia {
    fn new(nombre: &str, duracion: u32) -> Self {
        Self { nombre: nombre.to_string(), duracion }
    }

    fn describir(&self) -> String {
        format!("{} ({}s)", self.nombre, self.duracion)
    }
}

// Composición en lugar de herencia
struct Video {
    base: Multimedia,
    resolucion: (u32, u32),
}

struct Audio {
    base: Multimedia,
    bitrate: u32,
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    struct Punto { x: i32, y: i32 }

    impl Punto {
        fn new(x: i32, y: i32) -> Self { Self { x, y } }
        fn distancia(&self, otro: &Punto) -> f64 {
            ((self.x - otro.x).pow(2) + (self.y - otro.y).pow(2)) as f64
        }
    }

    #[test]
    fn test_struct_methods() {
        let a = Punto::new(0, 0);
        let b = Punto::new(3, 4);
        assert_eq!(a.distancia(&b), 5.0);
    }
}

💡 Memoria y rendimiento

  • Composición sin herencia: no hay vtable overhead (como en C++)
  • Layout lineal en memoria (más cache-friendly que jerarquías con herencia)
  • impl blocks se compilan a funciones estáticas: cero overhead

✅ Buenas prácticas

  • ✅ Prefiere composición sobre herencia
  • ✅ Usa traits para definir comportamiento compartido en lugar de clases base
  • ⚠️ No intentes emular herencia con structs anidados a menos que sea necesario
  • ❌ No crees jerarquías profundas con composición — refactoriza en traits

🏗️ Metodología

  • Structs planos: datos + métodos (90% de los casos)
  • Composición: struct A contiene struct B (relación “tiene un”)
  • Traits: comportamiento compartido entre tipos no relacionados

🔗 Para saber más

Patrones de diseño en Rust

¿Qué es?

Los patrones de diseño se adaptan al paradigma del lenguaje. Rust tiene implementaciones idiomáticas para los patrones clásicos, aprovechando ownership, traits y enums.

PatrónEn RustEjemplo
StrategyFunciones/closures + traitsBox<dyn Fn()> o genéricos
ObserverCanales (std::sync::mpsc) o callbackstokio::sync::broadcast
FactoryFunciones constructoras o From traitT::new(), From<T>
SingletonNo recommended; módulos como singletononce_cell::sync::Lazy
BuilderStruct builder con métodos chainstruct ConfigBuilder
AdapterWrapper struct + From/Into traitsstruct MiTipo(Wrapped)

Sintaxis básica: Builder

#[derive(Debug)]
struct Config {
    host: String,
    puerto: u16,
    timeout: u32,
}

struct ConfigBuilder {
    host: String,
    puerto: u16,
    timeout: u32,
}

impl ConfigBuilder {
    fn new() -> Self {
        Self { host: "localhost".into(), puerto: 8080, timeout: 30 }
    }

    fn host(mut self, host: &str) -> Self { self.host = host.into(); self }
    fn puerto(mut self, puerto: u16) -> Self { self.puerto = puerto; self }
    fn build(self) -> Config {
        Config { host: self.host, puerto: self.puerto, timeout: self.timeout }
    }
}

let config = ConfigBuilder::new()
    .host("example.com")
    .puerto(443)
    .build();

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    trait Estrategia { fn ejecutar(&self, a: i32, b: i32) -> i32; }

    struct Suma;
    impl Estrategia for Suma { fn ejecutar(&self, a: i32, b: i32) -> i32 { a + b } }

    struct Resta;
    impl Estrategia for Resta { fn ejecutar(&self, a: i32, b: i32) -> i32 { a - b } }

    fn calcular(estrategia: &dyn Estrategia, a: i32, b: i32) -> i32 {
        estrategia.ejecutar(a, b)
    }

    #[test]
    fn test_strategy_pattern() {
        assert_eq!(calcular(&Suma, 10, 5), 15);
        assert_eq!(calcular(&Resta, 10, 5), 5);
    }

    #[test]
    fn test_builder() {
        #[derive(Debug, PartialEq)]
        struct Conf { host: String, port: u16 }

        let c = Conf { host: "test".into(), port: 80 };
        assert_eq!(c, Conf { host: "test".into(), port: 80 });
    }
}

💡 Memoria y rendimiento

  • Builder pattern: cero overhead si usa tipos concretos (no Box)
  • Strategy con genéricos: monomorfización, máximo rendimiento
  • Strategy con dyn Fn: indirección de vtable, overhead mínimo
  • Los patrones en Rust suelen resolverse con tipos concretos y traits

✅ Buenas prácticas

  • ✅ Usa el builder pattern para constructores con muchos parámetros
  • ✅ Prefiere From/Into traits sobre new() conversiones
  • ⚠️ Singleton no es idiomático en Rust — usa módulos con lazy_static o once_cell
  • ❌ No implements Factory pattern con Box<dyn> a menos que necesites heterogeneidad

🔗 Para saber más


Polimorfismo en detalle

Polimorfismo paramétrico (genéricos)

¿Qué es?

Rust ofrece polimorfismo paramétrico vía genéricos. A diferencia de Java (erasure), Rust monomorfiza los genéricos: genera código máquina específico para cada combinación de tipos usada.

🔗 Rust Book — Generics

Sintaxis básica

// Función genérica
fn el_mayor<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

// Struct genérico
struct Par<T, U> { primero: T, segundo: U }

// impl genérico
impl<T> Par<T, T> {
    fn ambos(self) -> (T, T) { (self.primero, self.segundo) }
}

// Const genéricos (Rust 1.51+)
fn suma_array<const N: usize>(arr: &[i32; N]) -> i32 {
    arr.iter().sum()
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    fn id<T>(x: T) -> T { x }

    #[test]
    fn test_generic_fn() {
        assert_eq!(id(42), 42);
        assert_eq!(id("hola"), "hola");
    }

    #[test]
    fn test_generic_struct() {
        let p = (1, "a");
        assert_eq!(p, (1, "a"));
    }
}

💡 Memoria y rendimiento

  • Monomorfización: el compilador duplica el código por cada tipo usado
  • Sin overhead en runtime: cada instancia tiene su propia implementación
  • El tamaño del binario puede crecer — dyn Trait lo evita (trade-off)
  • Const genéricos: completamente en compilación, zero-cost

✅ Buenas prácticas

  • ✅ Prefiere monomorfización en hot paths para máximo rendimiento
  • ✅ Usa const genéricos para arrays de tamaño fijo
  • ⚠️ La monomorfización puede inflar el binario — agrupa tipos similares si es crítico
  • ❌ No pongas demasiados bounds en genéricos sin necesidad

🔗 Para saber más

Polimorfismo ad-hoc (traits)

¿Qué es?

El polimorfismo ad-hoc permite que diferentes tipos respondan a la misma interfaz de formas diferentes. En Rust se implementa con traits y operator overloading.

🔗 Rust Book — Traits

Sintaxis básica

use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct Punto { x: i32, y: i32 }

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

// Overloading con operador +
let a = Punto { x: 1, y: 2 };
let b = Punto { x: 3, y: 4 };
let c = a + b; // usa nuestro impl Add

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    use std::ops::Add;

    #[derive(Debug, PartialEq)]
    struct Metros(f64);

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

    #[test]
    fn test_operator_overloading() {
        assert_eq!(Metros(10.0) + Metros(5.0), Metros(15.0));
    }
}

💡 Memoria y rendimiento

  • Operator overloading via traits: se resuelve en compilación (monomorfización)
  • Igual de eficiente que una función manual
  • Copy + Add evita clonaciones innecesarias

✅ Buenas prácticas

  • ✅ Implementa Add, Mul, etc. para tipos matemáticos (vectores, matrices)
  • ⚠️ No abuses del operator overloading — si no es obvio, usa métodos con nombre
  • ❌ No implementes Add para tipos donde + no tenga sentido semántico

🔗 Para saber más


Interacción con contenido multimedia

image crate (PNG, JPEG, WebP, GIF)

¿Qué es?

La crate image es la librería estándar de Rust para procesamiento de imágenes. Soporta decenas de formatos, lectura/escritura, redimensionado, recorte y conversión.

🔗 image crate

Sintaxis básica

use image: "img/guia_0_100_rust/guia_0_100_rust_cover-1200.webp"

// Cargar y obtener información
let img = image: "img/guia_0_100_rust/guia_0_100_rust_cover-1200.webp"
println!("{}x{}", img.width(), img.height());

// Redimensionar
let redimensionada = img.resize(800, 600, image: "img/guia_0_100_rust/guia_0_100_rust_cover-1200.webp"
redimensionada.save("foto_800x600.jpg")?;

// Convertir formato
let img = image: "img/guia_0_100_rust/guia_0_100_rust_cover-1200.webp"
img.save_with_format("foto.webp", ImageFormat::WebP)?;

// Recortar
let recortada = img.crop_imm(100, 100, 200, 200);
recortada.save("recorte.png")?;

// Escala de grises
let gris = img.grayscale();
gris.save("gris.jpg")?;

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    use image: "img/guia_0_100_rust/guia_0_100_rust_cover-1200.webp"

    #[test]
    fn test_crear_imagen() {
        let mut img = RgbImage::new(2, 2);
        img.put_pixel(0, 0, Rgb([255, 0, 0]));
        img.put_pixel(1, 0, Rgb([0, 255, 0]));
        assert_eq!(img.width(), 2);
        assert_eq!(img.height(), 2);
    }
}

💡 Memoria y rendimiento

  • Las imágenes se cargan en memoria como Vec<u8> sin comprimir
  • Una imagen 4K (3840x2160) RGBA ocupa ~32 MB en RAM
  • El decodificador es streaming para JPEG (línea por línea)
  • PNG/WebP se decodifican completamente antes de acceder a los píxeles

✅ Buenas prácticas

  • ✅ Usa `image: “img/guia_0_100_rust/guia_0_100_rust_cover-1200.webp”
  • ✅ Usa save_with_format para controlar el formato de salida
  • ⚠️ No redimensiones con Nearest para reducciones grandes (aliasing)
  • ❌ No cargues imágenes enormes sin verificar el tamaño primero

🔗 Para saber más

Procesamiento de audio

¿Qué es?

Para procesamiento de audio en Rust, las crates principales son hound (WAV), rodio (reproducción) y symphonia (decodificación de múltiples formatos).

🔗 hound crate, rodio crate

Sintaxis básica

// Leer WAV con hound
use hound::WavReader;

let mut reader = WavReader::open("audio.wav")?;
let spec = reader.spec();
println!("{:?}", spec); // 44100 Hz, 16 bits, stereo

// Acceder a muestras
let muestras: Vec<i16> = reader.samples::<i16>()
    .filter_map(|s| s.ok())
    .collect();

// Reproducir con rodio
use rodio::{OutputStream, Source, source::SineWave};
let (_stream, handle) = OutputStream::try_default()?;
let source = SineWave::new(440.0); // La A4
handle.play_raw(source.convert_samples());
std::thread::sleep(std::time::Duration::from_secs(1));

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    use hound::{WavSpec, WavWriter, WavReader};
    use std::io::Cursor;

    #[test]
    fn test_write_read_wav() {
        let spec = WavSpec {
            channels: 1, sample_rate: 44100,
            bits_per_sample: 16, sample_format: hound::SampleFormat::Int,
        };
        let mut buf = Cursor::new(Vec::new());
        {
            let mut writer = WavWriter::new(&mut buf, spec).unwrap();
            for s in 0..100 { writer.write_sample(s as i16).unwrap(); }
        }
        buf.set_position(0);
        let reader = WavReader::new(buf).unwrap();
        assert_eq!(reader.len(), 100);
    }
}

💡 Memoria y rendimiento

  • WAV sin comprimir: ~10 MB/minuto a 44100 Hz/16 bits/stereo
  • hound lee/escribe muestras en streaming: no carga todo en memoria
  • rodio usa buffers internos para reproducción continua

✅ Buenas prácticas

  • ✅ Usa hound para leer/escribir WAV directamente
  • ✅ Usa rodio para reproducción en tiempo real
  • ⚠️ El audio en tiempo real necesita manejo cuidadoso de buffers (evita underruns)
  • ❌ No proceses audio en el hilo principal de UI — usa un hilo separado

🔗 Para saber más


Bases de datos

Diesel (ORM)

¿Qué es?

Diesel es el ORM más maduro de Rust. Es seguro en tipos, generación de esquemas en compilación, y compatible con PostgreSQL, MySQL y SQLite. Define la estructura de la base de datos en Rust y genera consultas verificadas en compilación.

🔗 Diesel docs

Sintaxis básica

[dependencies]
diesel = { version = "2", features = ["postgres"] }
diesel_cli = { version = "2", features = ["postgres"] }
// schema.rs (generado por diesel_cli)
table! {
    usuarios (id) {
        id -> Int4,
        nombre -> Varchar,
        email -> Varchar,
        activo -> Bool,
    }
}

// models.rs
#[derive(Queryable, Selectable)]
#[diesel(table_name = crate::schema::usuarios)]
#[diesel(check_for_backend(diesel::pg::Pg))]
struct Usuario {
    id: i32,
    nombre: String,
    email: String,
    activo: bool,
}

// Consultas
use crate::schema::usuarios::dsl::*;
let results = usuarios.filter(activo.eq(true))
    .limit(10)
    .load::<Usuario>(&mut conn)?;

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_query_builder() {
        use diesel::prelude::*;
        let query = diesel::dsl::select(1.into_sql::<diesel::sql_types::Integer>());
        assert!(query.is_some());
    }
}

💡 Memoria y rendimiento

  • Diesel usa query builders en compilación — cero overhead en runtime
  • Las consultas se verifican contra el esquema en compilación
  • La conexión a BD es pooling: reusa conexiones para evitar overhead de handshake

✅ Buenas prácticas

  • ✅ Usa diesel_cli para generar schema.rs automáticamente
  • ✅ Usa migrations para cambios de esquema versionados
  • ⚠️ No exponer queries SQL crudas sin verificar — usa el DSL siempre que puedas
  • ❌ No compartas una conexión entre threads sin pool

🔗 Para saber más

SQLx (async, compile-time checked)

¿Qué es?

SQLx es un crate async para SQL que verifica las queries en compilación (contra una BD real o con un archivo de datos). Soporta PostgreSQL, MySQL, SQLite. No es ORM: escribes SQL directamente.

🔗 SQLx docs

Sintaxis básica

use sqlx::postgres::PgPool;

let pool = PgPool::connect("postgres://user:pass@localhost/db").await?;

// Query verificada en compilación
let usuarios: Vec<(i32, String)> = sqlx::query_as(
    "SELECT id, nombre FROM usuarios WHERE activo = $1"
)
    .bind(true)
    .fetch_all(&pool)
    .await?;

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    use sqlx::PgPool;

    #[sqlx::test]
    async fn test_insert(pool: PgPool) -> sqlx::Result<()> {
        sqlx::query("INSERT INTO test (val) VALUES ($1)")
            .bind(42)
            .execute(&pool)
            .await?;
        Ok(())
    }
}

💡 Memoria y rendimiento

  • SQLx usa connection pooling por defecto: reusa conexiones
  • Las queries verificadas en compilación tienen costo de compilación adicional
  • En runtime: mínimo overhead, similar a drivers nativos

✅ Buenas prácticas

  • ✅ Prefiere SQLx para proyectos async (Tokio/Axum)
  • ✅ Usa sqlx::test para tests con BD real o docker
  • ⚠️ Las queries verificadas en compilación necesitan DATABASE_URL en build
  • ❌ No mezcles ORM y queries crudas sin una capa de abstracción

🔗 Para saber más


WebSockets y mensajería

tokio-tungstenite (WebSocket)

¿Qué es?

tokio-tungstenite proporciona soporte WebSocket asíncrono sobre Tokio. Permite comunicación bidireccional en tiempo real entre cliente y servidor.

🔗 tokio-tungstenite

Sintaxis básica

use tokio_tungstenite::connect_async;
use futures_util::StreamExt;

let url = url::Url::parse("ws://echo.websocket.org").unwrap();
let (ws_stream, _) = connect_async(url).await?;
let (_, mut read) = ws_stream.split();

while let Some(msg) = read.next().await {
    let msg = msg?;
    if let tokio_tungstenite::tungstenite::Message::Text(text) = msg {
        println!("recibido: {text}");
    }
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_websocket_url() {
        let url = url::Url::parse("ws://localhost:8080/ws").unwrap();
        assert_eq!(url.scheme(), "ws");
        assert_eq!(url.host_str(), Some("localhost"));
    }
}

💡 Memoria y rendimiento

  • Las conexiones WebSocket son persistentes: sin overhead de handshake por mensaje
  • tokio-tungstenite maneja miles de conexiones concurrentes con una sola tarea por conexión
  • El overhead por conexión es ~2-4 KB (TCP buffer + estructura de estado)

✅ Buenas prácticas

  • ✅ Usa WebSocket para comunicación en tiempo real (chats, notificaciones, streaming)
  • ⚠️ Siempre maneja reconexión y heartbeats
  • ❌ No uses WebSocket para APIs REST tradicionales — HTTP es más simple y cacheable

🔗 Para saber más


Concurrencia y paralelismo

Tokio: async/await

¿Qué es?

Tokio es el runtime asíncrono de Rust. Proporciona un scheduler work-stealing para tareas async, E/S no bloqueante, timers, canales y sincronización. Es el estándar de facto para aplicaciones de red.

🔗 Tokio docs

Sintaxis básica

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let tarea1 = tokio::spawn(async {
        sleep(Duration::from_millis(100)).await;
        "tarea 1"
    });

    let tarea2 = tokio::spawn(async {
        sleep(Duration::from_millis(50)).await;
        "tarea 2"
    });

    let r1 = tarea1.await.unwrap();
    let r2 = tarea2.await.unwrap();
    println!("{r1} y {r2}");
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[tokio::test]
    async fn test_async() {
        let result = tokio::spawn(async { 42 }).await.unwrap();
        assert_eq!(result, 42);
    }

    #[tokio::test]
    async fn test_sleep() {
        let start = std::time::Instant::now();
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
        assert!(start.elapsed() >= std::time::Duration::from_millis(10));
    }
}

💡 Memoria y rendimiento

  • Tokio usa un scheduler work-stealing: distribuye tareas entre hilos del thread pool
  • Cada tarea async ocupa ~1-2 KB de overhead (frente a ~8 KB de un hilo OS)
  • El cambio de contexto es ~0.1 µs (frente a ~1 µs para hilos de OS)
  • Para tareas CPU-bound, usa spawn_blocking (no bloquees el pool async)

✅ Buenas prácticas

  • ✅ Usa #[tokio::main] para entry points async
  • ✅ Usa tokio::spawn para tareas concurrentes
  • ⚠️ No bloquees tareas con std::thread::sleep — usa tokio::time::sleep
  • ❌ No mezcles async y sync sin spawn_blocking

🏗️ Metodología

  • I/O bound (red, disco): async con Tokio
  • CPU bound (cálculo pesado): spawn_blocking o hilos separados
  • Mixto: async con canales a workers CPU-bound

🔗 Para saber más

Rayon: paralelismo CPU-bound

¿Qué es?

Rayon convierte fácilmente iteraciones secuenciales en paralelo con .par_iter(). Usa un thread pool work-stealing para dividir el trabajo automáticamente.

🔗 Rayon docs

Sintaxis básica

use rayon::prelude::*;

fn main() {
    let nums: Vec<u64> = (0..1_000_000).collect();

    // Secuencial
    let suma: u64 = nums.iter().sum();

    // Paralelo (cambia .iter() por .par_iter())
    let suma_par: u64 = nums.par_iter().sum();

    // Map-reduce paralelo
    let resultado: u64 = (0..1_000_000u64)
        .into_par_iter()
        .map(|x| x * x)
        .sum();
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    use rayon::prelude::*;

    #[test]
    fn test_par_iter() {
        let v: Vec<i32> = (0..100).collect();
        let sum: i32 = v.par_iter().sum();
        assert_eq!(sum, 4950);
    }
}

💡 Memoria y rendimiento

  • Rayon divide el trabajo en chunks y los distribuye entre hilos
  • El thread pool se inicializa una vez: número de hilos = núcleos lógicos
  • Overhead de división: ~1 µs por chunk — solo rentable para trabajo significativo
  • Recomendado para operaciones CPU-bound pesadas (cálculos, procesamiento de imágenes)

✅ Buenas prácticas

  • ✅ Usa par_iter() para operaciones CPU-bound con colecciones grandes
  • ✅ Usa par_sort() en lugar de sort() para vectores grandes (>10K elementos)
  • ⚠️ No uses Rayon para E/S (red, archivos) — prefiere Tokio o hilos
  • ❌ No anides par_iter() dentro de par_iter() — el pool se satura

🔗 Para saber más

Tabla de decisión: threading vs async vs Rayon

threadingasync (Tokio)Rayon
PropósitoCPU-bound o I/O blockingI/O-bound no bloqueanteCPU-bound paralelo
Overhead por tarea~8 KB (pila)~1-2 KB~0 (pool reusado)
Context switch~1 µs~0.1 µsN/A (work-stealing)
EscalaCientosCientos de milesNúcleos de CPU
FacilidadManual (join, sync)Fácil (async/await)Muy fácil (par_iter)
Uso típicoCálculo pesado, FFIServidores web, BDProcesamiento de datos

Testing y calidad

Frameworks de testing en Rust

Rust tiene soporte nativo de testing con #[test]. Además, existen herramientas especializadas:

FrameworkPropósitoAsync?CLI
#[test] (built-in)Unit testsNo (necesita #[tokio::test])cargo test
cargo testIntegración + doc testsSícargo test --test
proptestProperty-based testingNocargo test
criterionBenchmarksNocargo bench
tarpaulinCoberturaSícargo tarpaulin

🔗 Rust Book — Testing

Sintaxis básica

// Unit test
pub fn suma(a: i32, b: i32) -> i32 { a + b }

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

    #[test]
    fn test_suma() {
        assert_eq!(suma(2, 2), 4);
    }

    #[test]
    fn test_suma_negativos() {
        assert_eq!(suma(-1, 1), 0);
    }

    #[test]
    #[should_panic(expected = "desbordamiento")]
    fn test_panic() {
        panic!("desbordamiento");
    }
}

// Integration test (tests/integration.rs)
use mi_crate::suma;

#[test]
fn test_integracion() {
    assert_eq!(suma(2, 2), 4);
}

// Doc test
/// ```
/// let r = mi_crate::suma(2, 2);
/// assert_eq!(r, 4);
/// ```
pub fn suma(a: i32, b: i32) -> i32 { a + b }

🧪 Cómo probarlo

# Ejecutar todos los tests
cargo test

# Tests específicos
cargo test test_suma
cargo test tests::test_suma -- --nocapture

# Con filtro
cargo test --test integration_test
cargo test --lib

# Doc tests
cargo test --doc

# Benchmarks (criterion)
cargo bench

# Cobertura
cargo install cargo-tarpaulin
cargo tarpaulin --out Html

💡 Memoria y rendimiento

  • Los tests se ejecutan en paralelo por defecto (cargo test -- --test-threads=1 para serial)
  • Doc tests verifican que los ejemplos en la documentación funcionan
  • Los tests de integración van en tests/ y son archivos separados del crate
  • Cada test recibe un hilo separado; el overhead es mínimo

✅ Buenas prácticas

  • ✅ Escribe tests para toda función pública
  • ✅ Usa doc tests para documentación con ejemplos ejecutables
  • ⚠️ No dejes println! en tests que ejecutas en CI (usa --nocapture solo localmente)
  • ❌ No escribas tests que dependan del orden de ejecución

🏗️ Metodología de testing

  • Unit tests: junto al código (#[cfg(test)] mod tests)
  • Integration tests: en tests/ — prueban el crate como API pública
  • Doc tests: documentación con ejemplos ejecutables (siempre funcionan)
  • Property-based: con proptest para verificar invariantes con datos aleatorios
  • Fuzzing: con cargo-fuzz para testing de seguridad

🔗 Para saber más


Conceptos avanzados

unsafe, FFI y no_std

¿Qué es?

unsafe permite operaciones que el borrow checker no puede verificar: desreferenciar punteros crudos, llamar a funciones C (FFI), acceder a static mut, e implementar traits unsafe. no_std elimina la stdlib para entornos embebidos.

🔗 Rustonomicon

Sintaxis básica

// unsafe: desreferenciar puntero crudo
let x = 42;
let r = &x as *const i32;
unsafe {
    println!("{}", *r);
}

// FFI: llamar a C
extern "C" {
    fn abs(x: i32) -> i32;
}

unsafe { println!("{}", abs(-5)); }

// no_std (para embebido)
// #![no_std]
// use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[test]
    fn test_ffi_wrapper() {
        extern "C" fn suma_c(a: i32, b: i32) -> i32 { a + b }
        let result = unsafe { suma_c(2, 3) };
        assert_eq!(result, 5);
    }
}

💡 Memoria y rendimiento

  • unsafe no desactiva el borrow checker — solo permite operaciones adicionales
  • FFI tiene overhead de llamada (ABI C) ~5-10 ns
  • no_std elimina ~100 KB del binario para entornos embebidos
  • Las llamadas FFI no pueden ser inlineadas

✅ Buenas prácticas

  • ✅ Minimiza unsafe: encapsúlalo en funciones seguras
  • ✅ Usa #[no_mangle] y extern "C" para exportar funciones a C
  • ⚠️ unsafe no significa “sin checks” — documenta cada invariante
  • ❌ No uses unsafe sin una razón muy concreta y documentada

🔗 Para saber más

Pin y async traits

¿Qué es?

Pin garantiza que un valor no se mueva en memoria, necesario para self-referential structs en async. Los async traits (Rust 1.75+) permiten métodos async en traits, eliminando la necesidad de cajas como async-trait.

🔗 Rust Book — Pin

Sintaxis básica

use std::pin::Pin;

// Pin: valor que no se moverá
let mut dato = Box::new(42);
let pinned = Pin::new(&mut dato);

// Async trait (Rust 1.75+)
#[async_trait]
trait Servicio {
    async fn obtener(&self) -> String;
}

struct MiServicio;
#[async_trait]
impl Servicio for MiServicio {
    async fn obtener(&self) -> String {
        "datos".to_string()
    }
}

🧪 Cómo probarlo

#[cfg(test)]
mod tests {
    #[tokio::test]
    async fn test_async_trait() {
        trait Simple {
            async fn get(&self) -> i32;
        }
        struct X;
        impl Simple for X {
            async fn get(&self) -> i32 { 42 }
        }
        assert_eq!(X.get().await, 42);
    }
}

💡 Memoria y rendimiento

  • Pin es un wrapper de compilación: cero overhead en runtime
  • self-referential structs ocupan el tamaño del valor + padding
  • async traits generan state machines por implementación: monomorfización

✅ Buenas prácticas

  • ✅ Usa async traits nativos (Rust 1.75+) en lugar de async-trait crate
  • ✅ Pin es interno a los generadores async — no necesitas usarlo directamente
  • ⚠️ No implementes tu propio Pin a menos que estés haciendo unsafe
  • ❌ No muevas un valor después de haberlo pinned

🔗 Para saber más

WASM (WebAssembly)

¿Qué es?

Rust compila a WebAssembly, permitiendo ejecutar código de alto rendimiento en el navegador. Con wasm-pack puedes empaquetar y publicar en npm.

🔗 Rust + WASM

Sintaxis básica

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

// En JavaScript:
// import { fibonacci } from './pkg';
// console.log(fibonacci(10));

🧪 Cómo probarlo

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

    #[wasm_bindgen_test]
    fn test_fib() {
        assert_eq!(super::fibonacci(10), 55);
    }
}

💡 Memoria y rendimiento

  • WASM ejecuta en una VM sandboxed dentro del navegador
  • El binario WASM suele ser 10-50 KB para aplicaciones pequeñas
  • WASM no tiene acceso directo a DOM (necesita JS glue)
  • Las llamadas WASM ↔ JS tienen overhead de ~1-10 µs

✅ Buenas prácticas

  • ✅ Usa wasm-pack para build, test y publish
  • ✅ Prefiere WASM para código CPU-bound (cálculos, juegos, procesamiento)
  • ⚠️ No uses WASM para tareas triviales de DOM — JS es más rápido para eso
  • ❌ No compartas grandes estructuras entre JS y WASM sin serialización eficiente

🔗 Para saber más


Proyecto final integrador: Procesador de logs

Un procesador de logs que combina file system, algoritmos, módulos, testing y red.

use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::collections::HashMap;

#[derive(Debug, PartialEq)]
enum Nivel { INFO, WARN, ERROR }

#[derive(Debug)]
struct LogEntry {
    nivel: Nivel,
    mensaje: String,
    linea: usize,
}

fn parse_nivel(s: &str) -> Option<Nivel> {
    match s {
        "INFO" => Some(Nivel::INFO),
        "WARN" => Some(Nivel::WARN),
        "ERROR" => Some(Nivel::ERROR),
        _ => None,
    }
}

fn procesar_log<P: AsRef<Path>>(path: P) -> Result<Vec<LogEntry>, String> {
    let file = fs::File::open(path).map_err(|e| format!("error abriendo: {e}"))?;
    let reader = BufReader::new(file);

    reader.lines()
        .enumerate()
        .filter_map(|(i, line)| {
            let line = match line {
                Ok(l) => l,
                Err(_) => return None,
            };
            let parts: Vec<&str> = line.splitn(2, "]: ").collect();
            if parts.len() < 2 { return None; }
            let nivel_str = parts[0].trim_start_matches('[');
            let mensaje = parts[1].to_string();
            parse_nivel(nivel_str).map(|nivel| LogEntry { nivel, mensaje, linea: i + 1 })
        })
        .collect::<Vec<_>>()
        .into_iter()
        .collect::<Result<Vec<_>, _>>()
        .or_else(|_| Ok(vec![]))
}

fn reportar(entries: &[LogEntry]) -> HashMap<&str, usize> {
    let mut stats: HashMap<&str, usize> = HashMap::new();
    for entry in entries {
        let key = match entry.nivel {
            Nivel::INFO => "INFO",
            Nivel::WARN => "WARN",
            Nivel::ERROR => "ERROR",
        };
        *stats.entry(key).or_insert(0) += 1;
    }
    stats
}

fn main() -> Result<(), String> {
    let entries = procesar_log("app.log")?;
    let stats = reportar(&entries);
    for (nivel, count) in &stats {
        println!("{nivel}: {count}");
    }
    Ok(())
}

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

    #[test]
    fn test_parse_nivel() {
        assert_eq!(parse_nivel("INFO"), Some(Nivel::INFO));
        assert_eq!(parse_nivel("DEBUG"), None);
    }

    #[test]
    fn test_procesar_log() {
        let path = "test.log";
        fs::write(path, "[INFO]: inicio
[ERROR]: fallo
").unwrap();
        let entries = procesar_log(path).unwrap();
        assert_eq!(entries.len(), 2);
        assert_eq!(entries[0].nivel, Nivel::INFO);
        assert_eq!(entries[1].nivel, Nivel::ERROR);
        fs::remove_file(path).unwrap();
    }

    #[test]
    fn test_reportar() {
        let entries = vec![
            LogEntry { nivel: Nivel::INFO, mensaje: "ok".into(), linea: 1 },
            LogEntry { nivel: Nivel::ERROR, mensaje: "fail".into(), linea: 2 },
            LogEntry { nivel: Nivel::INFO, mensaje: "otro".into(), linea: 3 },
        ];
        let stats = reportar(&entries);
        assert_eq!(*stats.get("INFO").unwrap(), 2);
        assert_eq!(*stats.get("ERROR").unwrap(), 1);
    }
}

Despliegue a producción

Build para producción

cargo build --release

El binario optimizado estará en target/release/.

Docker multi-stage

FROM rust:1.85-slim AS builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM scratch
COPY --from=builder /app/target/release/mi-app .
CMD ["./mi-app"]

Hosting

  • Fly.io — despliegue con flyctl launch
  • Railway — conecta tu repo y despliega automáticamente
  • Shuttle — plataforma serverless para Rust

CI/CD (GitHub Actions)

name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cargo build --release
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

Canales y recursos en español

Canales de YouTube

Comunidades

Repositorios GitHub destacados


Hacks y tips de productividad

1. cargo-watch (auto-rebuild en cambios)

cargo install cargo-watch
cargo watch -x check -x test
cargo watch -x run  # para servidores

2. cargo-expand (ver macros expandidas)

cargo install cargo-expand && cargo expand

3. cargo-edit (gestión de dependencias desde CLI)

cargo add serde --features derive
cargo rm old-crate
cargo upgrade

4. cargo-outdated (dependencias desactualizadas)

cargo install cargo-outdated && cargo outdated

5. cargo-audit (vulnerabilidades de seguridad)

cargo install cargo-audit && cargo audit

6. Rust Analyzer tricks

  • Alt+Enter (VSCode) → ver code actions (auto-import, add derive, add impl)
  • :RustAnalyzerExpandMacro en Neovim → expandir macros
  • Config rust-analyzer.checkOnSave.command = "clippy" para lints automáticos

7. Replace match pesados con if let

// ❌ Verboso
match opt {
    Some(v) => println!("{v}"),
    None => {}
}

// ✅ Concisa
if let Some(v) = opt { println!("{v}"); }

8. Unit structs como type-level markers

struct Metros;
struct Segundos;

fn velocidad<T>(d: f64, t: f64) -> f64 { d / t }
// El parámetro T es un marker — no ocupa memoria

9. Debuggear con dbg!

let x = 42;
let y = dbg!(x * 2);  // imprime "[src/main.rs:2] x * 2 = 84"

10. Testeo de errores con #[should_panic]

#[test]
#[should_panic(expected = "índice fuera de rango")]
fn test_panico() {
    let v = vec![1, 2, 3];
    let _ = v[10];
}

11. Perfilamiento con flamegraphs

cargo install flamegraph
cargo flamegraph --bin mi-app

12. Profile-guided optimization (PGO)

# Compila con instrumentación
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release
# Ejecuta con datos reales
./target/release/mi-app
# Recompila con los datos de perfil
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data" cargo build --release

⚠️ Errores comunes

Estos son los errores de compilación que más se repiten al empezar con Rust. El borrow checker y el sistema de tipos son estrictos, pero sus mensajes suelen indicar exactamente dónde y cómo arreglarlo.

ErrorCausaSolución
error[E0308]: mismatched typesPasas o devuelves un tipo distinto al esperadoRevisa los tipos declarados y convierte o adapta el valor (.into(), as, etc.)
error[E0382]: use of moved valueUsas una variable después de moverla con vec!/String etc.Clona el valor (clone()) o haz referencia con & en vez de pasarlo por valor
error[E0502]: cannot borrow X as immutable because it is also borrowed as mutableTienes un &mut y un & del mismo dato a la vezSepara los accesos en bloques o usa clone() para romper la doble dependencia
error[E0599]: no method named 'x' foundLlamas a un método que no existe o no está en ámbito (falta use)Importa el trait con use o revisa el nombre del método y del tipo
error: linker 'cc' not foundFalta un enlazador o toolchain de C en tu sistemaInstala las build tools (en Linux build-essential, en macOS Xcode CLT, en Windows MSVC)
the msvc target is not availableInstalaste el toolchain GNU y necesitas el MSVC en Windowsrustup target add x86_64-pc-windows-msvc o instala el compilador con rustup toolchain install stable-msvc
cargo: command not foundCargo no está en el PATH o Rust no está instaladoInstala/repasa rustup y añade ~/.cargo/bin al PATH; verifica con rustup --version
error: failed to run custom build commandUna dependencia con bindings de C falla al compilarInstala las dependencias del sistema (librerías de desarrollo) o la herramienta que pide la crate

🔗 Guías relacionadas


Referencias y documentación oficial

  1. Rust Programming Language (oficial)
  2. Rust Book (The Book)
  3. Rust by Example
  4. Rustonomicon
  5. Rust Reference
  6. Rust Edition Guide
  7. Cargo Book
  8. Rust API Guidelines
  9. Rust Design Patterns
  10. Tokio Tutorial
  11. Rust WASM Book
  12. Rust Embedded Book
  13. Rust Cookbook
  14. Awesome Rust (GitHub)
  15. This Week in Rust (newsletter)
  16. Rust Blog (oficial)
  17. Inside Rust Blog
  18. Rust Playground
  19. crates.io
  20. docs.rs
COMPARTIR:
COMENTARIOS:

📋 Contenido